← all games
arena-shooter-fable — GAME.md
# Neon Break — top-down arena shooter
Survive 3 waves of enemies in a walled neon arena. WASD to move, mouse to aim
and shoot, Space/Shift to dash, R to restart after a win or loss.
On touch devices a twin-stick overlay appears instead: left stick moves, right
stick aims and fires, a DASH button sits above it, and a tap restarts the end
screen.
## Scenes
- `ArenaScene` (`src/scenes/ArenaScene.ts`) — the only scene. Registers layers
`floor` (-10), `world` (0), `fx` (10), `hud` (100, screen space); the UI
plugin adds its own screen-space `ui` layer. Builds the floor graphics, four
static walls, the player, camera (follows player, clamped to the arena),
HUD, banner, reticle, particle emitter pool, and a `director` entity running
`WaveDirector`. Restart replaces the scene with a fresh `ArenaScene`.
## Entity types
- `Player` (`src/entities/Player.ts`) — dynamic body, circle collider r12.
`PlayerController` handles movement (285 px/s), pointer aim, fire
(9 shots/s), dash (780 px/s for 0.16 s, 1.3 s cooldown, i-frames while
dashing), 5 HP with 1 s invulnerability + sprite flicker after a hit.
Contact damage is polled via `collider.getOverlapping()` when vulnerable.
- `Enemy` (`src/entities/Enemy.ts`) — kinds `grunt` / `sprinter` / `brute`
(stats in `src/config.ts`). `PhysicsSteeringAgent` (impulse drive): grunts
and brutes `seek` the player, sprinters `pursue` with prediction; all use
`separation` against other enemies. `EnemyBrain` owns HP, hit flash,
bullet knockback, and death (particles, shake, `EnemyDiedEvent`).
- `Bullet` (`src/entities/Bullet.ts`) — sensor circle collider, ccd, 0.9 s
lifetime; `onTrigger` damages enemies / dies on walls.
- `Hud` (`src/entities/Hud.ts`) — three UI corner panels: HP cells (top-left),
wave + hostiles remaining (top-right), dash cooldown bar (bottom-left).
- `Banner` (`src/entities/Banner.ts`) — center-screen announcements on the
`hud` layer ("WAVE 2", "YOU DIED", "ARENA CLEARED").
- `Reticle` (`src/entities/Reticle.ts`) — world-space crosshair at the pointer
(the CSS cursor is hidden over the canvas). Under stick aiming it projects
165 px ahead of the player instead, since there is no pointer to sit under.
- `touch-controls` — `VirtualControls` from `@yagejs-addons/virtual-controls`,
spawned by `ArenaScene.spawnTouchControls()`. Two `follow`-mode sticks
(`move` → left axes + the WASD actions, `aim` → right axes) and one `dash`
button. `visible: "auto"` shows it only on coarse-pointer devices.
- `fx-*` emitter entities — one `ParticleEmitterComponent` per effect flavor,
managed by `FxPool` (`src/fx.ts`).
## Game flow
`WaveDirector` (`src/WaveDirector.ts`) on the `director` entity: schedules the
3 waves from `WAVES` in `src/config.ts` (counts, speed multiplier, spawn
interval), telegraphs each spawn with a tinted shrinking ring (0.75 s), tracks
remaining enemies via `EnemyDiedEvent`, and owns win/lose banners and the R
restart. Spawn points sit on the arena edge at least 260 px from the player.
## Input actions (`src/main.ts`)
`up/down/left/right` (WASD + arrows), `fire` (MouseLeft), `dash`
(Space, ShiftLeft), `restart` (KeyR). The touch overlay drives these same
actions plus the `left`/`right` gamepad axes — gameplay code reads
`getStick()` and the action map, never the overlay directly.
Aim precedence in `PlayerController`: right stick when deflected, otherwise
the pointer — and the pointer is ignored entirely on coarse-pointer devices,
so a stray tap never re-aims. Firing needs either a deflected aim stick or a
real mouse button.
## Assets
None on disk. All sprites are runtime textures baked in `src/textures.ts`
(`tex.player`, `tex.grunt`, `tex.sprinter`, `tex.brute`, `tex.bullet`,
`tex.dot`, `tex.ring`). SFX are synthesized in `src/sfx.ts` via WebAudio.
## Effects
Bloom on `world` and `fx` layers; scene-scope shockwave (brute kills, player
death); screen-scope vignette + chromatic-aberration damage pulse installed
once at boot (`src/screenFx.ts`); `hitFlash` per sprite on player and enemies.
## Save payload
Not used. `PlayerController`, `EnemyBrain`, and `WaveDirector` implement
`serialize()` so `Inspector.getComponentData` can read hp / wave / phase for
verification.