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

183 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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,
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.