← all games
stealth-fable-max — GAME.md
# MOTH — game manifest
Top-down stealth vignette. One night, one museum, steal the Gilded Moth diadem and reach the van. YAGE 0.9.0 + `@yagejs-addons/abilities` 0.1.0 + `@yagejs-addons/inventory` 0.1.0. Zero asset files: all visuals are code-drawn (pixi Graphics + runtime textures), all audio is WebAudio synthesis.
## Scenes
| Scene | File | Purpose |
|---|---|---|
| `TitleScene` | `src/scenes/TitleScene.ts` | Title card + control legend. First click unlocks WebAudio, then replaces itself with `GameScene` (iris transition). |
| `GameScene` | `src/scenes/GameScene.ts` | The heist. Builds the level, spawns player/guards/props/HUD, owns run state and the caught/respawn sequence. Registers scene-scoped services: `GameStateKey`, `LightingKey`, `PatrolGridKey` (A* grid). Registers the `moth` inspector extension (removed on exit). |
| `SummaryScene` | `src/scenes/SummaryScene.ts` | Extraction summary pushed over a paused `GameScene`: haul list, stats, rating, restart. |
Layer stack in `GameScene` (world-space unless noted): `floor(-20)`, `pools(-12)` light gradients, `deco(-8)` walls/dressing, `default(0)` props+actors with `ySort`, `fx(8)` motes/moths (dimmed by darkness on purpose), `darkness(16)` the cut-hole overlay at alpha 0.4, `cones(17)` guard vision, `overlay(20)` prompts/rings/pips/pickups, inventory layers (screen, 1050–1070), `ui(1100, screen)`. Camera: follow with pointer lookahead, `zoom = 1.22`, bounds clamped to the world.
## Entity types
- `PlayerEntity` (`src/entities/player.ts`) — dynamic body r11; `PlayerController` (movement, footsteps, interact scan, capture), `PlayerStatus` (cloak/dash/lit flags), `Facing`, `Abilities`, `AbilityDriverComponent`, eye-pip child.
- `GuardEntity` (`src/entities/guard.ts`) — kinematic body r12; `GuardBrain` FSM (PATROL → SUSPICIOUS → ALERT → RETURN), vision-cone child (raycast-clipped fan), detection-arc child, state pips, lantern glow, procedural whistle.
- Props (`src/entities/props.ts`): `LootPickup`, `DisplayCase` (hold-to-pry), `Door` (open-once, some locked), `VentGrate` (paired teleport, 1 pick), `Safe` (1 pick), `KeyHook`, `Breaker` (cuts hall+archive lights; archive guard walks over and restores it), `Van` (exfil channel), `Cat` (pettable secret), `SatchelDrop` (loot dropped on capture), `CoinEntity` (thrown distraction, spawned by the toss ability).
- `HudEntity` (`src/hud/hud.ts`) — objective chip, 3 ability icons with cooldown sweeps, satchel strip + weight bar, toast queue.
## Input actions (`InputPlugin`)
Gameplay group: `up/down/left/right` (WASD+arrows), `sneak` (Shift), `dash` (Space), `toss` (F), `veil` (C). Ungrouped: `interact` (E — shared with the inventory panel's confirm, so it must stay active while the panel is open; the player controller gates world interaction on `isGroupEnabled("gameplay")` instead), `restart` (R), plus the inventory addon defaults `move-up/-down/-left/-right`, `cancel` (Escape), `sort` (R), `inventory` (Tab/I). The gameplay group is disabled while the panel is open.
## Abilities (addon)
| Intent | Phases | Cooldown | Effect |
|---|---|---|---|
| `dash` | windup 0.09s → burst 0.16s (`velocity` step) → recover 0.12s | 3.5s | 950 px/s burst toward move dir; loud landing noise ring |
| `toss` | windup 0.22s → throw (spawn `CoinEntity` toward pointer) | 6s | coin clinks where it lands; nearby guards investigate |
| `veil` | windup 0.35s → cloak 2.5s (`cloak` step) → fade 0.3s | 14s | player counts as unlit; detection nearly off |
## Inventory (addon)
`Inventory` capacity 6, catalog in `src/items/catalog.ts`. Tools: lockpicks (stack 3), noisemakers (stack 2). Loot carries `data: { value, weight }`; total weight slows the player and widens noise rings. Panel opens on Tab (addon presenters, custom dark theme); a passive 6-diamond strip + weight bar always shows in the HUD.
## Detection model (numbers live in `src/entities/guard.ts` + `src/world/level.ts`)
Vision: fov 68°, range 250 (calm) / 270 (alert), line-of-sight raycast against walls. Player visibility factor: lit 1.0, shadow 0.18, veil 0.08; lantern proximity (r70) counts as lit. Meter fills by closeness × factor; ≥0.35 SUSPICIOUS, ≥1.0 ALERT; decays after 0.8s unseen. Hearing: noise events carry a radius (sneak 30, walk 120, dash-land 220, coin 160, noisemaker 320…), halved through walls; weight multiplies radius. Every state transition has a sting, a pip, and a cone recolor (teal → amber → red).
Caught: grab ring while alerted (or bumping a guard). Consequence: carried loot drops as a satchel at the capture spot; player respawns at the dock with tools intact; guards return to routes; `caught` counts in the summary.
## Run flow
Infiltrate → take the diadem (`getaway` phase: guards faster, routes extended, alarm bed) → reach the van → summary (haul, spotted/caught counts, secrets 0–5, time, rating). Secrets: the vent shortcut, the lit-nook floor safe (cross the bright nook with veil, or cut power first), the breaker, the locked storeroom, the cat inside it.
## Save payload
None. No persistence; each run is fresh (`SummaryScene` restart replaces `GameScene`).
## Verification
Engine `debug: true`; `window.__yage__.inspector` plus the `moth` extension (`state()`, `guards()`, `player()`, `satchel()`, `cooldowns()`, `teleport(x,y)`, `give(id)`, `lit(x,y)`, `sight(x1,y1,x2,y2)`) drive deterministic probes under `inspector.time.freeze()` + `step(n)`. Scene pushes/replaces need real awaits between step batches; a background tab starves the rAF loop, so probe under the frozen clock rather than wall time.