# VINYL GAUNTLET — Implementation Plan A top-down arcade maze shooter that clones the mechanics of **Gauntlet II (1986)**, re-skinned as record-collecting / DJ / trainspotting culture. You dig through record fairs and warehouse clubs, fight off swarming clubbers, and reach the DJ Booth to trainspot the track and clear the level. **Execute the steps IN ORDER. Each step ends with an acceptance check — do not start the next step until the check passes in the browser. Do not add features not listed here. Placeholder geometric shapes throughout; no sprite art until Step 7.** ## Tech stack - Vite + vanilla JS (no TypeScript, no framework) - Phaser 3 (`npm install phaser`), Arcade Physics, zero gravity, top-down - One canvas 960x720. Tile size 32px. - No external assets: shapes are `add.rectangle`/`add.circle` converted to physics bodies (or generated textures via `make.graphics`). Voice via `window.speechSynthesis`. The single sound effect (airhorn) via WebAudio oscillator — no audio files. ## File layout (keep it this small) ``` index.html src/main.js — Phaser config + scene registration src/GameScene.js — the whole game (it's fine, it's an arcade game) src/levels.js — level maps as 2D arrays + legend src/announcer.js — speechSynthesis wrapper + line table ``` ## Core constants (put at top of GameScene.js, tune later) ```js const TILE = 32; 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; shrinks with crowd density const PLAYER_SPEED = 180; const POSER_SPEED = 120; const NERD_SPEED = 70; const SOUNDGUY_SPEED = 55; const VINYL_SPEED = 400; const FIRE_COOLDOWN_MS = 180; const TRAINSPOT_MS = 3000; ``` --- ## Step 1 — Project, tilemap, movement - `npm create vite@latest . -- --template vanilla`, then `npm install phaser`. - Phaser config: 960x720, arcade physics, `gravity: {y: 0}`, pixelArt true. - **Levels are 2D arrays of single characters** in `src/levels.js`: ``` W = wall (crate stack, brown) . = floor (dark grey #1a1a1a) P = player start E = espresso (green square) K = lanyard/key (yellow square) D = velvet rope door (red block) G = generator/discount bin (purple) X = DJ booth exit (magenta, pulsing alpha tween) S = sound guy spawn A = airhorn pickup (white square) ``` Export `LEVELS = [level1, level2, level3]` — author 3 maps by hand, each ~30x25 tiles, Gauntlet-style: rooms, corridors, a keyed door between player and exit, 2–4 generators. Level 1 easy (1 generator, no sound guy), level 3 nasty. - Build the level in `buildLevel(index)`: walls into a `staticGroup`, everything else into groups/objects. This function must fully tear down the previous level first (clear groups, timers, colliders) — it gets called on every level change and restart. - Player: cyan circle, 8-way WASD movement, normalized diagonal speed, collides with walls. Camera follows player with lerp; world bounds = map size. **Check:** player drives around level 1, can't pass through walls, camera follows. ## Step 2 — Vibe drain, pickups, doors, HUD - `this.vibe = VIBE_MAX`. A 1-second looped timer subtracts `VIBE_DRAIN_PER_SEC`. - HUD (fixed to camera): `VIBE 1987 LANYARDS 1 AIRHORNS 1 LEVEL 1`. Vibe text turns yellow below 500, red below 200. - Espresso overlap: +`ESPRESSO_VIBE` (cap at `VIBE_MAX`), destroy pickup. - Lanyard overlap: `lanyards++`, destroy pickup. - Velvet rope: static red blocks that collide like walls; on collide, if `lanyards > 0` then `lanyards--` and destroy the block. - Vibe <= 0 → GAME_OVER state: freeze physics, "TRAINWRECK" centered, press R to restart at level 1 with fresh vibe. - Game states as a plain string field: `TITLE | PLAYING | GAME_OVER | LEVEL_CLEAR`. Title screen = level shown behind a "VINYL GAUNTLET — press any key" overlay. **Check:** vibe visibly ticks down, espresso refills it, a lanyard opens a rope, letting vibe hit 0 shows game over, R restarts. ## Step 3 — Shooting, generators, the swarm - **Firing:** arrow keys fire vinyl (small black circle with white center dot) in 8 directions (combinations of arrows), independent of movement direction — this is the Gauntlet control scheme. Rate-limit with `FIRE_COOLDOWN_MS`. Vinyl dies on wall impact or after 1200ms. - **Object pooling (CRITICAL):** one `physics.add.group` for vinyl (size 30), one for enemies (size 80). Spawn with `group.get()`, kill with `setActive(false).setVisible(false)` + disable body. Never `new`/`destroy` per shot. - **Generators (Discount Bins):** purple squares, `GEN_HP` hits. Each has its own looped spawn timer (`GEN_SPAWN_MS`): if it's within ~700px of the player, spawn an enemy on an adjacent floor tile. Vinyl hit → hp--, brief white flash tween; at 0, destroy generator and its timer. - **Two spawned enemy types** (generator picks 70/30): - **Poser** (red triangle): fast, `moveToObject` toward player every 400ms. - **Gear Nerd** (orange square): slow, same homing, bigger touch damage — the "ghost" that corners you and drains you with synth talk. - **Crowd density:** `this.density = 1`, +0.05 every 10s on the current level; effective spawn interval = `GEN_SPAWN_MS / density`. Resets each level. **Check:** shooting feels like Gauntlet (move one way, shoot another), bins pour out enemies until destroyed, 50+ enemies on screen holds 60fps. ## Step 4 — Combat, the Sound Guy, the Airhorn - Vinyl overlaps enemy → both deactivated (back to pool). - Enemy overlaps player → enemy deactivated, vibe -= its touch damage, 100ms red tint flash on player. (Enemy dies on contact = authentic Gauntlet grunt behavior and prevents per-frame drain stacking.) - **The Sound Guy** (Gauntlet's Death): black square with a white "SND" label, one per level at the `S` tile (levels 2+). No wall collider — phases through everything. Homes at `SOUNDGUY_SPEED`. Immune to vinyl (vinyl passes through). While overlapping the player, drain `SOUNDGUY_DRAIN` per 250ms tick — NOT per frame. Only the airhorn removes him. - **Airhorn (smart bomb):** spacebar, if `airhorns > 0`: white full-screen flash (200ms), camera shake, deactivate every active enemy AND the sound guy (generators survive), play a two-tone WebAudio oscillator blast (BWAA-BWAA). `A` pickups grant +1; player starts with 1. **Check:** shooting clears swarms, sound guy walks through walls and is only stoppable by airhorn, airhorn wipes the screen. ## Step 5 — Trainspotting exit + level flow - **DJ Booth (`X`):** while the player overlaps it, a progress bar fills over `TRAINSPOT_MS` ("TRAINSPOTTING..."). Taking ANY damage or leaving the tile resets the bar to zero. This is the level's tension peak — enemies keep coming. - Bar full → LEVEL_CLEAR: freeze 1.5s, "TUNE IDENTIFIED", then `buildLevel(next)`. Vibe, lanyards, airhorns CARRY OVER (Gauntlet rule — the drain is the difficulty curve). After the last level, loop back to level 1 with `density` starting higher. **Check:** standing at the booth under pressure fills the bar, getting hit resets it, level 2 loads with vibe carried over. ## Step 6 — The Announcer `src/announcer.js`: wrapper around `speechSynthesis` — cancel any current utterance before speaking, pick the most robotic-sounding available voice, `rate 0.9, pitch 0.6`. Per-line cooldowns (min 8s per line key) so it never spams. Lines: | Trigger | Line | |---|---| | vibe crosses below 500 | "Selector needs espresso badly." | | vibe crosses below 200 | "Selector... is about to die." | | game over | "Trainwreck. Absolute trainwreck." | | shot a generator to death | "Discount bin... destroyed." | | sound guy spawns | "The sound guy... is coming." | | airhorn used | (no speech — the oscillator blast IS the line) | | door opened | "Velvet rope... unlocked." | | level clear | "Tune identified. Selector levels up." | | touching booth, bar filling | "Trainspotting..." (once per attempt) | **Check:** lines fire at the right moments, never overlap or spam. ## Step 7 — Juice pass (only after 1–6 are flawless) Small, in this order, stop when it feels good: 1. Vibe < 200: red vignette (a camera-fixed rectangle with low alpha). 2. Enemy death: 3-particle puff (pooled). 3. Booth pulses brighter as the trainspot bar fills. 4. Only now, optionally swap shapes for sprites — the game must already be fun as rectangles. ## Explicit NON-GOALS (do not build, do not scaffold for) Multiplayer. Digging minigame. Finite ammo / vinyl-as-currency. Ranged enemies. Level editors or Tiled/JSON maps. Vibe-based movement penalties. Saving/persistence. Mobile controls. More than 3 enemy types. Any menu beyond title/game-over text. If any of these seem needed, they aren't — finish the loop first.