diff --git a/assets/process_sprites.py b/assets/process_sprites.py index b68d026..352981d 100644 --- a/assets/process_sprites.py +++ b/assets/process_sprites.py @@ -19,7 +19,14 @@ OUT.mkdir(parents=True, exist_ok=True) # game texture key -> (source file, size px, is_tile) TABLE = { "floor": ("floor_parquet.png", 96, True), + "floor_carpet": ("floor_carpet.png", 96, True), + "floor_checker": ("floor_checker.png", 96, True), "wall": ("wall_crate.png", 48, True), + "wall_shelf": ("wall_shelf.png", 48, True), + "wall_poster": ("wall_poster.png", 48, True), + "pirate": ("enemy_record_pirate.png", 48, False), + "ghoul": ("enemy_crate_ghoul.png", 48, False), + "gold": ("pickup_goldrecord.png", 38, False), "player": ("player_digger.png", 48, False), "poser": ("enemy_poser.png", 48, False), "nerd": ("enemy_gearnerd.png", 48, False), diff --git a/public/sprites/floor_carpet.png b/public/sprites/floor_carpet.png new file mode 100644 index 0000000..8d57136 Binary files /dev/null and b/public/sprites/floor_carpet.png differ diff --git a/public/sprites/floor_checker.png b/public/sprites/floor_checker.png new file mode 100644 index 0000000..fb28ef6 Binary files /dev/null and b/public/sprites/floor_checker.png differ diff --git a/public/sprites/ghoul.png b/public/sprites/ghoul.png new file mode 100644 index 0000000..5ad8397 Binary files /dev/null and b/public/sprites/ghoul.png differ diff --git a/public/sprites/gold.png b/public/sprites/gold.png new file mode 100644 index 0000000..9e24347 Binary files /dev/null and b/public/sprites/gold.png differ diff --git a/public/sprites/pirate.png b/public/sprites/pirate.png new file mode 100644 index 0000000..28e86d2 Binary files /dev/null and b/public/sprites/pirate.png differ diff --git a/public/sprites/wall_poster.png b/public/sprites/wall_poster.png new file mode 100644 index 0000000..1ce4388 Binary files /dev/null and b/public/sprites/wall_poster.png differ diff --git a/public/sprites/wall_shelf.png b/public/sprites/wall_shelf.png new file mode 100644 index 0000000..c583159 Binary files /dev/null and b/public/sprites/wall_shelf.png differ diff --git a/src/GameScene.js b/src/GameScene.js index 040785e..086cf8b 100644 --- a/src/GameScene.js +++ b/src/GameScene.js @@ -6,25 +6,48 @@ const TILE = 48; const VIBE_MAX = 2000; const VIBE_DRAIN_PER_SEC = 1; // the Gauntlet heartbeat const ESPRESSO_VIBE = 500; -const POSER_TOUCH_DAMAGE = 30; -const NERD_TOUCH_DAMAGE = 80; const SOUNDGUY_DRAIN = 10; // per 250ms tick while overlapping const GEN_HP = 3; const GEN_SPAWN_MS = 2500; // base; effective interval shrinks with density const GEN_RANGE = 1050; // px; scaled with TILE so range stays ~same in tiles const PLAYER_SPEED = 270; -const POSER_SPEED = 180; -const NERD_SPEED = 105; const SOUNDGUY_SPEED = 82; const VINYL_SPEED = 600; const VINYL_LIFE_MS = 1200; const FIRE_COOLDOWN_MS = 180; const TRAINSPOT_MS = 3000; +const GOLD_SCORE = 100; +const KILL_SCORE = 10; +const CLEAR_SCORE = 500; // texture keys loaded from public/sprites/.png (see assets/process_sprites.py) const TEXTURE_KEYS = [ - 'floor', 'wall', 'player', 'poser', 'nerd', 'soundguy', 'generator', - 'exit', 'door', 'espresso', 'lanyard', 'airhorn', 'vinyl', + 'floor', 'floor_carpet', 'floor_checker', + 'wall', 'wall_shelf', 'wall_poster', + 'player', 'poser', 'nerd', 'pirate', 'ghoul', 'soundguy', 'generator', + 'exit', 'door', 'espresso', 'lanyard', 'airhorn', 'vinyl', 'gold', +]; + +// Gauntlet II classes: stat tweaks only, no new mechanics. +const CLASSES = { + digger: { name: 'THE DIGGER', desc: '+25% speed', speed: 1.25, dmg: 1, lanyards: 0, airhorns: 1, sgDrain: 1, tint: 0xffffff }, + selector: { name: 'THE SELECTOR', desc: 'double damage', speed: 1, dmg: 2, lanyards: 0, airhorns: 1, sgDrain: 1, tint: 0xffe080 }, + promoter: { name: 'THE PROMOTER', desc: 'starts loaded', speed: 1, dmg: 1, lanyards: 1, airhorns: 2, sgDrain: 1, tint: 0xffa0ff }, + soundtech: { name: 'THE SOUND TECH', desc: 'sound guy resistant', speed: 1, dmg: 1, lanyards: 0, airhorns: 1, sgDrain: 0.4, tint: 0xa0ffa0 }, +}; + +const ENEMY_TYPES = { + poser: { tex: 'poser', speed: 180, touch: 30, hp: 1 }, + nerd: { tex: 'nerd', speed: 105, touch: 80, hp: 1 }, + pirate: { tex: 'pirate', speed: 210, touch: 20, hp: 1 }, // steals pickups, flees home + ghoul: { tex: 'ghoul', speed: 70, touch: 120, hp: 3 }, // slow tank +}; + +// Level themes cycle with level index; mix = weighted generator spawn table. +const THEMES = [ + { name: 'RECORD SHOP', floor: 'floor', wall: 'wall', mix: [['poser', 0.7], ['nerd', 0.3]] }, + { name: 'RECORD FAIR', floor: 'floor_carpet', wall: 'wall_shelf', mix: [['poser', 0.5], ['nerd', 0.25], ['pirate', 0.25]] }, + { name: 'CLUB NIGHT', floor: 'floor_checker', wall: 'wall_poster', mix: [['poser', 0.4], ['nerd', 0.2], ['pirate', 0.2], ['ghoul', 0.2]] }, ]; export default class GameScene extends Phaser.Scene { @@ -40,6 +63,7 @@ export default class GameScene extends Phaser.Scene { create() { this.cameras.main.setBackgroundColor(0x1a1a1a); + this.cls = CLASSES.digger; // Persistent groups — created once, refilled each level so colliders survive. this.walls = this.physics.add.staticGroup(); @@ -62,15 +86,23 @@ export default class GameScene extends Phaser.Scene { this.physics.add.collider(this.vinyl, this.walls, this.killVinyl, null, this); this.physics.add.overlap(this.vinyl, this.enemies, this.hitEnemy, null, this); this.physics.add.overlap(this.vinyl, this.generators, this.hitGenerator, null, this); + this.physics.add.overlap(this.vinyl, this.pickups, this.vinylHitsPickup, null, this); this.physics.add.overlap(this.player, this.enemies, this.touchEnemy, null, this); this.physics.add.overlap(this.player, this.soundGuys, this.touchSoundGuy, null, this); this.physics.add.overlap(this.player, this.pickups, this.grabPickup, null, this); + this.physics.add.overlap(this.enemies, this.pickups, this.pirateGrab, null, this); // Input. this.cursors = this.input.keyboard.createCursorKeys(); this.wasd = this.input.keyboard.addKeys('W,A,S,D'); this.keySpace = this.input.keyboard.addKey('SPACE'); this.keyR = this.input.keyboard.addKey('R'); + this.input.keyboard.on('keydown', (ev) => { + if (this.state !== 'TITLE') return; + const i = ['1', '2', '3', '4'].indexOf(ev.key); + if (i >= 0) this.startGame(Object.keys(CLASSES)[i]); + }); + this.setupTouch(); // HUD + overlays (fixed to camera). this.hud = this.add @@ -78,7 +110,7 @@ export default class GameScene extends Phaser.Scene { .setScrollFactor(0) .setDepth(100); this.overlay = this.add - .text(480, 340, '', { + .text(480, 190, '', { fontFamily: 'monospace', fontSize: '40px', color: '#ff00ff', @@ -87,6 +119,32 @@ export default class GameScene extends Phaser.Scene { .setOrigin(0.5) .setScrollFactor(0) .setDepth(100); + this.sub = this.add + .text(480, 330, '', { + fontFamily: 'monospace', + fontSize: '20px', + color: '#ffffff', + align: 'center', + }) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(100); + this.classButtons = Object.entries(CLASSES).map(([key, c], i) => { + const b = this.add + .text(480, 470 + i * 45, `${i + 1} ${c.name} — ${c.desc}`, { + fontFamily: 'monospace', + fontSize: '22px', + color: '#00e5ff', + backgroundColor: '#00000080', + padding: { x: 12, y: 4 }, + }) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(100) + .setInteractive({ useHandCursor: true }); + b.on('pointerdown', () => this.startGame(key)); + return b; + }); this.barBg = this.add .rectangle(480, 680, 306, 26, 0x000000) .setScrollFactor(0) @@ -98,6 +156,12 @@ export default class GameScene extends Phaser.Scene { .setScrollFactor(0) .setDepth(100) .setVisible(false); + // burning-out vignette (vibe < 25%) + this.vignette = this.add + .rectangle(480, 360, 960, 720, 0xff0000, 1) + .setScrollFactor(0) + .setDepth(90) + .setAlpha(0); // Vibe heartbeat. this.time.addEvent({ @@ -111,20 +175,55 @@ export default class GameScene extends Phaser.Scene { }, }); - this.input.keyboard.on('keydown', () => { - if (this.state === 'TITLE') this.startGame(); - }); - - this.state = 'TITLE'; this.resetStats(); this.buildLevel(0); - this.showOverlay('VINYL GAUNTLET\n\npress any key'); + this.showTitle(); + } + + setupTouch() { + this.input.addPointer(2); + this.touchMove = null; + this.touchFire = null; + const overHorn = (p) => this.hornBtn && Phaser.Math.Distance.Between(p.x, p.y, 870, 630) < 60; + this.input.on('pointerdown', (p) => { + if (this.state !== 'PLAYING' || overHorn(p)) return; + const stick = { id: p.id, ox: p.x, oy: p.y, dx: 0, dy: 0 }; + if (p.x < 480) this.touchMove = stick; + else this.touchFire = stick; + }); + this.input.on('pointermove', (p) => { + for (const s of [this.touchMove, this.touchFire]) { + if (s && s.id === p.id) { + s.dx = p.x - s.ox; + s.dy = p.y - s.oy; + } + } + }); + this.input.on('pointerup', (p) => { + if (this.touchMove && this.touchMove.id === p.id) this.touchMove = null; + if (this.touchFire && this.touchFire.id === p.id) this.touchFire = null; + }); + if (this.sys.game.device.input.touch) { + this.hornBtn = this.add + .circle(870, 630, 42, 0xffffff, 0.25) + .setScrollFactor(0) + .setDepth(100) + .setStrokeStyle(2, 0xffffff) + .setInteractive(); + this.add + .text(870, 630, 'HORN', { fontFamily: 'monospace', fontSize: '14px', color: '#ffffff' }) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(101); + this.hornBtn.on('pointerdown', () => this.doAirhorn()); + } } resetStats() { this.vibe = VIBE_MAX; - this.lanyards = 0; - this.airhorns = 1; + this.score = 0; + this.lanyards = this.cls.lanyards; + this.airhorns = this.cls.airhorns; this.densityBase = 1; this.nextFire = 0; this.soundGuyTick = 0; @@ -138,6 +237,7 @@ export default class GameScene extends Phaser.Scene { if (row.length !== w0) throw new Error(`level ${index}: ragged row (${row.length} vs ${w0})`); this.levelIndex = index; + this.theme = THEMES[index % THEMES.length]; this.walls.clear(true, true); this.doors.clear(true, true); this.pickups.clear(true, true); @@ -158,10 +258,10 @@ export default class GameScene extends Phaser.Scene { // Tiled floor behind everything. if (this.floor) this.floor.destroy(); this.floor = this.add - .tileSprite(0, 0, worldW, worldH, 'floor') + .tileSprite(0, 0, worldW, worldH, this.theme.floor) .setOrigin(0) .setDepth(-10) - .setTint(0x555555); // darken so the brown crate walls read as raised + .setTint(0x555555); // darken so walls read as raised for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { @@ -170,7 +270,7 @@ export default class GameScene extends Phaser.Scene { const y = r * TILE + TILE / 2; switch (ch) { case 'W': - this.walls.create(x, y, 'wall'); + this.walls.create(x, y, this.theme.wall); break; case 'D': this.doors.create(x, y, 'door'); @@ -187,6 +287,9 @@ export default class GameScene extends Phaser.Scene { case 'A': this.pickups.create(x, y, 'airhorn').setData('type', 'airhorn'); break; + case 'R': + this.pickups.create(x, y, 'gold').setData('type', 'gold'); + break; case 'G': { const g = this.generators.create(x, y, 'generator'); g.setData('hp', GEN_HP); @@ -221,7 +324,7 @@ export default class GameScene extends Phaser.Scene { update(time, delta) { if (this.state === 'TITLE') return; if (this.state === 'GAME_OVER') { - if (Phaser.Input.Keyboard.JustDown(this.keyR)) this.startGame(); + if (Phaser.Input.Keyboard.JustDown(this.keyR)) this.showTitle(); return; } if (this.state === 'LEVEL_CLEAR') return; @@ -233,7 +336,7 @@ export default class GameScene extends Phaser.Scene { this.updateGenerators(delta); this.updateSoundGuys(); this.density = this.densityBase + 0.05 * Math.floor((time - this.levelStartTime) / 10000); - this.handleAirhorn(); + if (Phaser.Input.Keyboard.JustDown(this.keySpace)) this.doAirhorn(); this.handleTrainspot(delta); this.updateHud(); } @@ -247,7 +350,16 @@ export default class GameScene extends Phaser.Scene { vx *= inv; vy *= inv; } - this.player.setVelocity(vx * PLAYER_SPEED, vy * PLAYER_SPEED); + if (vx === 0 && vy === 0 && this.touchMove) { + const m = Math.hypot(this.touchMove.dx, this.touchMove.dy); + if (m > 12) { + vx = this.touchMove.dx / m; + vy = this.touchMove.dy / m; + } + } + // burning out (< 25% vibe) = sluggish + const spd = PLAYER_SPEED * this.cls.speed * (this.vibe < VIBE_MAX * 0.25 ? 0.85 : 1); + this.player.setVelocity(vx * spd, vy * spd); } handleFiring(time) { @@ -255,18 +367,27 @@ export default class GameScene extends Phaser.Scene { const c = this.cursors; let dx = (c.right.isDown ? 1 : 0) - (c.left.isDown ? 1 : 0); let dy = (c.down.isDown ? 1 : 0) - (c.up.isDown ? 1 : 0); - if (dx === 0 && dy === 0) return; if (dx !== 0 && dy !== 0) { const inv = 1 / Math.sqrt(2); dx *= inv; dy *= inv; } + if (dx === 0 && dy === 0 && this.touchFire) { + const m = Math.hypot(this.touchFire.dx, this.touchFire.dy); + if (m > 15) { + dx = this.touchFire.dx / m; + dy = this.touchFire.dy / m; + } + } + if (dx === 0 && dy === 0) return; const v = this.vinyl.get(this.player.x, this.player.y, 'vinyl'); if (!v) return; v.setActive(true).setVisible(true); v.body.enable = true; v.setPosition(this.player.x, this.player.y); - v.setVelocity(dx * VINYL_SPEED, dy * VINYL_SPEED); + // peak time (> 75% vibe) = hotter throws + const vs = VINYL_SPEED * (this.vibe > VIBE_MAX * 0.75 ? 1.2 : 1); + v.setVelocity(dx * vs, dy * vs); v.setData('dieAt', time + VINYL_LIFE_MS); this.nextFire = time + FIRE_COOLDOWN_MS; } @@ -277,9 +398,36 @@ export default class GameScene extends Phaser.Scene { }); } + nearestPickup(from) { + let best = null; + let bestD = Infinity; + this.pickups.getChildren().forEach((p) => { + const d = Phaser.Math.Distance.Between(from.x, from.y, p.x, p.y); + if (d < bestD) { + bestD = d; + best = p; + } + }); + return best; + } + updateEnemies() { this.enemies.getChildren().forEach((e) => { - if (e.active) this.physics.moveToObject(e, this.player, e.getData('speed')); + if (!e.active) return; + let target = this.player; + if (e.getData('type') === 'pirate') { + if (e.getData('carry')) { + const home = e.getData('home'); + if (Phaser.Math.Distance.Between(e.x, e.y, home.x, home.y) < 24) { + this.kill(e); // escaped with the loot + return; + } + target = home; + } else { + target = this.nearestPickup(e) || this.player; + } + } + this.physics.moveToObject(e, target, e.getData('speed')); }); } @@ -296,17 +444,32 @@ export default class GameScene extends Phaser.Scene { }); } - spawnEnemy(g) { - const poser = Math.random() < 0.7; - const e = this.enemies.get(g.x, g.y, poser ? 'poser' : 'nerd'); + spawnEnemy(g, forceType) { + let type = forceType; + if (!type) { + let roll = Math.random(); + for (const [t, w] of this.theme.mix) { + roll -= w; + type = t; + if (roll <= 0) break; + } + } + const def = ENEMY_TYPES[type]; + const e = this.enemies.get(g.x, g.y, def.tex); if (!e) return; // pool full — skip - e.setTexture(poser ? 'poser' : 'nerd'); + e.setTexture(def.tex); e.setActive(true).setVisible(true); + e.clearTint(); e.body.enable = true; e.body.setSize(34, 34, true); // body smaller than 48px sprite e.setPosition(g.x, g.y); - e.setData('touch', poser ? POSER_TOUCH_DAMAGE : NERD_TOUCH_DAMAGE); - e.setData('speed', poser ? POSER_SPEED : NERD_SPEED); + e.setData('type', type); + e.setData('touch', def.touch); + e.setData('speed', def.speed); + e.setData('hp', def.hp); + e.setData('carry', null); + e.setData('home', { x: g.x, y: g.y }); + if (type === 'pirate') this.say(LINES.pirate); } updateSoundGuys() { @@ -315,14 +478,13 @@ export default class GameScene extends Phaser.Scene { }); } - handleAirhorn() { - if (!Phaser.Input.Keyboard.JustDown(this.keySpace)) return; - if (this.airhorns <= 0) return; + doAirhorn() { + if (this.state !== 'PLAYING' || this.airhorns <= 0) return; this.airhorns -= 1; this.cameras.main.flash(200, 255, 255, 255); this.cameras.main.shake(200, 0.01); this.enemies.getChildren().forEach((e) => { - if (e.active) this.kill(e); + if (e.active) this.dropLoot(e) || this.kill(e); }); this.soundGuys.clear(true, true); this.airhornBlast(); @@ -346,9 +508,10 @@ export default class GameScene extends Phaser.Scene { updateHud() { const v = Math.max(0, Math.ceil(this.vibe)); this.hud.setText( - `VIBE ${v} LANYARDS ${this.lanyards} AIRHORNS ${this.airhorns} LEVEL ${this.levelIndex + 1}`, + `SCORE ${this.score} VIBE ${v} LANYARDS ${this.lanyards} AIRHORNS ${this.airhorns} LEVEL ${this.levelIndex + 1}`, ); this.hud.setColor(this.vibe < 200 ? '#ff4040' : this.vibe < 500 ? '#ffd11a' : '#ffffff'); + this.vignette.setAlpha(this.vibe < VIBE_MAX * 0.25 ? 0.12 : 0); } // --- collision callbacks --- @@ -357,16 +520,35 @@ export default class GameScene extends Phaser.Scene { this.kill(v); } + // pirate killed while carrying: put the pickup back + a gold record bonus + dropLoot(e) { + const carry = e.getData('carry'); + if (!carry) return false; + this.pickups.create(e.x, e.y, carry.tex).setData('type', carry.type); + this.pickups.create(e.x + TILE, e.y, 'gold').setData('type', 'gold'); + e.setData('carry', null); + this.kill(e); + return true; + } + hitEnemy(v, e) { if (!v.active || !e.active) return; this.kill(v); - this.kill(e); + const hp = e.getData('hp') - this.cls.dmg; + if (hp > 0) { + e.setData('hp', hp); + e.setTintFill(0xffffff); + this.time.delayedCall(60, () => e.active && e.clearTint()); + return; + } + this.score += KILL_SCORE; + if (!this.dropLoot(e)) this.kill(e); } hitGenerator(v, g) { if (!v.active || !g.active) return; this.kill(v); - const hp = g.getData('hp') - 1; + const hp = g.getData('hp') - this.cls.dmg; g.setData('hp', hp); g.setTintFill(0xffffff); this.time.delayedCall(60, () => g.active && g.clearTint()); @@ -376,17 +558,31 @@ export default class GameScene extends Phaser.Scene { } } + vinylHitsPickup(v, item) { + // Gauntlet's "Elf shot the food!" — only food is destructible + if (!v.active || item.getData('type') !== 'espresso') return; + this.kill(v); + item.destroy(); + this.say(LINES.shotEspresso); + } + + pirateGrab(e, item) { + if (!e.active || e.getData('type') !== 'pirate' || e.getData('carry')) return; + e.setData('carry', { type: item.getData('type'), tex: item.texture.key }); + item.destroy(); + } + touchEnemy(p, e) { if (!e.active) return; const dmg = e.getData('touch'); - this.kill(e); + if (!this.dropLoot(e)) this.kill(e); this.takeDamage(dmg); } touchSoundGuy() { if (this.time.now - this.soundGuyTick > 250) { this.soundGuyTick = this.time.now; - this.takeDamage(SOUNDGUY_DRAIN); + this.takeDamage(SOUNDGUY_DRAIN * this.cls.sgDrain); } } @@ -395,6 +591,10 @@ export default class GameScene extends Phaser.Scene { if (type === 'espresso') this.vibe = Math.min(VIBE_MAX, this.vibe + ESPRESSO_VIBE); else if (type === 'lanyard') this.lanyards += 1; else if (type === 'airhorn') this.airhorns += 1; + else if (type === 'gold') { + this.score += GOLD_SCORE; + this.say(LINES.gold); + } item.destroy(); } @@ -419,8 +619,8 @@ export default class GameScene extends Phaser.Scene { takeDamage(amount) { if (this.state !== 'PLAYING') return; this.vibe -= amount; - this.player.setTint(0xff0000); - this.time.delayedCall(100, () => this.player.clearTint()); + this.player.setTintFill(0xff0000); + this.time.delayedCall(100, () => this.player.setTint(this.cls.tint)); if (this.trainspotProgress > 0) this.trainspotProgress = 0; // trainspot breaks on any hit this.checkThresholds(); if (this.vibe <= 0) this.gameOver(); @@ -434,37 +634,75 @@ export default class GameScene extends Phaser.Scene { levelClear() { if (this.state !== 'PLAYING') return; this.state = 'LEVEL_CLEAR'; + this.score += CLEAR_SCORE; this.physics.pause(); this.barBg.setVisible(false); this.barFill.setVisible(false); + const next = (this.levelIndex + 1) % LEVELS.length; this.showOverlay('TUNE IDENTIFIED'); + this.sub.setText(`next: ${THEMES[next % THEMES.length].name}`).setVisible(true); this.say(LINES.levelClear); this.time.delayedCall(1500, () => { this.physics.resume(); - let next = this.levelIndex + 1; - if (next >= LEVELS.length) { - next = 0; - this.densityBase += 0.5; // loop back harder - } + if (this.levelIndex + 1 >= LEVELS.length) this.densityBase += 0.5; // loop back harder this.hideOverlay(); this.state = 'PLAYING'; this.buildLevel(next); }); } + // --- high scores (localStorage) --- + + loadScores() { + try { + return JSON.parse(localStorage.getItem('vg_hiscores')) || []; + } catch { + return []; + } + } + + maybeSaveScore() { + if (this.score <= 0) return; + const scores = this.loadScores(); + if (scores.length >= 5 && this.score <= scores[scores.length - 1].score) return; + // ponytail: window.prompt beats building a text-entry widget; revisit if it grates + const raw = window.prompt(`High score: ${this.score}! Your initials:`, 'AAA'); + const initials = (raw || 'AAA').toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 3) || 'AAA'; + scores.push({ initials, score: this.score }); + scores.sort((a, b) => b.score - a.score); + localStorage.setItem('vg_hiscores', JSON.stringify(scores.slice(0, 5))); + } + gameOver() { if (this.state !== 'PLAYING') return; this.state = 'GAME_OVER'; this.player.setVelocity(0, 0); this.physics.pause(); this.say(LINES.gameOver); - this.showOverlay('TRAINWRECK\n\npress R'); + this.maybeSaveScore(); + this.showOverlay('TRAINWRECK'); + this.sub.setText(`score ${this.score}\n\npress R`).setVisible(true); } - startGame() { + showTitle() { + this.state = 'TITLE'; + this.physics.pause(); + this.buildLevel(0); + const hs = this.loadScores() + .map((s, i) => `${i + 1}. ${s.initials} ${s.score}`) + .join('\n'); + this.showOverlay('VINYL GAUNTLET'); + this.sub.setText(hs ? `HIGH SCORES\n${hs}` : 'dig deep. name every tune.').setVisible(true); + this.classButtons.forEach((b) => b.setVisible(true)); + } + + startGame(clsKey) { + this.cls = CLASSES[clsKey] || CLASSES.digger; + this.player.setTint(this.cls.tint); this.physics.resume(); this.resetStats(); this.hideOverlay(); + this.classButtons.forEach((b) => b.setVisible(false)); this.state = 'PLAYING'; this.buildLevel(0); } @@ -475,6 +713,7 @@ export default class GameScene extends Phaser.Scene { hideOverlay() { this.overlay.setVisible(false); + this.sub.setVisible(false); } say(line) { diff --git a/src/announcer.js b/src/announcer.js index 674284f..f6bd603 100644 --- a/src/announcer.js +++ b/src/announcer.js @@ -44,4 +44,7 @@ export const LINES = { doorOpen: ['doorOpen', 'Velvet rope unlocked.'], levelClear: ['levelClear', 'Tune identified. Selector levels up.'], trainspot: ['trainspot', 'Trainspotting.'], + shotEspresso: ['shotEspresso', 'Selector shot the espresso.'], + pirate: ['pirate', 'Record pirate. Protect the wax.'], + gold: ['gold', 'Wax acquired.'], }; diff --git a/src/levels.js b/src/levels.js index 2946e6d..f0d0736 100644 --- a/src/levels.js +++ b/src/levels.js @@ -33,8 +33,8 @@ const level1 = [ edge('..........WWWWW'), edge(''), edge(''), - edge('.....................E'), - edge('.........................X'), + edge('..............R......E'), + edge('.....R...................X'), edge(''), wall, ]; @@ -55,8 +55,8 @@ const level2 = [ edge('....WW....WWWW....WW'), edge('....WW....W..G....'), edge('....WW....W.......E'), - edge('..........WWWWWW..'), - edge('.E................'), + edge('..........WWWWWW..R'), + edge('.E......R.........'), edge('...................X'), wall, ]; @@ -77,8 +77,8 @@ const level3 = [ edge('.WWWW.WWWW.WWWW.WWWW'), edge('.W.G....E....G....E.'), edge('.WWWW.WWWW.WWWW.WWW'), - edge('.......S...........'), - edge('.E.................'), + edge('.......S.......R....'), + edge('.E....R............'), edge('.................X'), wall, ]; diff --git a/src/main.js b/src/main.js index 6fcee0c..8f43282 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ window.game = new Phaser.Game({ parent: 'game', backgroundColor: '#0a0a0a', pixelArt: true, + scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, // phones get a fitted canvas physics: { default: 'arcade', arcade: { gravity: { x: 0, y: 0 }, debug: false },