vinylgauntlet/plan.md
type-two 0a71e07078 Vinyl Gauntlet: core game (Gauntlet II re-skin) + FLUX asset queue
Top-down arcade maze shooter in Phaser 3. Steps 1-6 of plan.md complete and
verified: vibe drain, generators/swarm, pooling, combat, Sound Guy, airhorn,
trainspot exit, level flow, announcer. assets/ holds the FLUX prompt manifest
+ MODELBEAST enqueuer for the Step 7 sprite pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:17:19 +10:00

8.9 KiB
Raw Blame History

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)

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, 24 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 16 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.