Design docs, build plan, contracts, and lane instructions for NOT TONIGHT

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-19 15:48:44 +10:00
commit 5086a325d8
10 changed files with 926 additions and 0 deletions

35
CLAUDE.md Normal file
View File

@ -0,0 +1,35 @@
# NOT TONIGHT — instructions for Claude sessions in this repo
Papers, Please-style bouncer sim. Browser game: Phaser 3 + TypeScript (strict) +
Vite + Vitest. You are probably an **Opus 4.8 lane executor** — this repo is built
by parallel "lanes" coordinated through files, reviewed by Fable.
## Before writing any code
1. Read `LANEHANDOVER.md` — find the latest REVIEW block and any block for your
lane. Review instructions override lane files.
2. Read your lane file in `lanes/` (John/Fable will tell you which lane you are;
if not stated, ask — do not guess).
3. Skim `docs/GAME_DESIGN.md`, `docs/BUILD_PLAN.md`, and read `docs/CONTRACTS.md`
fully. Contracts are law.
## Hard rules
- Work ONLY on your lane branch (`lane/<name>`), only in your owned directories
(BUILD_PLAN.md §2) + your tests + LANEHANDOVER.md. **Never merge or push main.**
- Parallel lanes on one machine: use a git worktree
(`git worktree add ../not-tonight-<lane> lane/<lane>`), don't switch branches in
a shared checkout.
- Don't modify `docs/CONTRACTS.md`, `src/core/`, or `src/data/types.ts` after
Phase 0 — file a CONTRACT CHANGE REQUEST in the handover instead, stub locally
with `// TODO(contract):`, keep working.
- No `Math.random()` — SeededRNG streams only. No cross-lane scene imports.
Game logic in pure tested functions (`src/rules/`, per-lane logic modules);
scenes stay thin.
- End EVERY session: `npm run lint && npm run build && npm test` clean, append a
SESSION block to `LANEHANDOVER.md` (template in that file), commit, push your
branch. An unpushed session didn't happen.
- Remote is Gitea: `ssh://git@100.71.119.27:222/monster/not-tonight.git`.
## Tone guard
The game is Aussie hospitality satire — affectionate, funny, with a moral undertow
(design doc §4.3). If content you're writing reads as mean-spirited rather than
absurd, soften toward absurd.

48
LANEHANDOVER.md Normal file
View File

@ -0,0 +1,48 @@
# LANE HANDOVER LOG
Coordination file between lanes (Opus 4.8 execution sessions) and the reviewer
(Fable). **Append-only** — never edit or delete previous blocks. Newest block at the
BOTTOM. Push after every session so the reviewer can see state remotely.
## Protocol
1. Before starting work: read this whole file + your lane file in `lanes/` +
`docs/CONTRACTS.md`. Check for a REVIEW block addressed to your lane — it
overrides your lane file where they conflict.
2. Work only inside your lane's owned directories (see BUILD_PLAN.md §2).
3. End of session: append a SESSION block (template below), commit, push your
`lane/<name>` branch.
4. Never merge to main — the reviewer does that.
5. Blocked on a contract ambiguity? Don't stall and don't silently change the
contract: stub locally with `// TODO(contract):`, log a CONTRACT CHANGE REQUEST
below, keep moving.
## SESSION block template
```markdown
---
### SESSION — <LANE-NAME><date> <time>
**Branch/commits:** lane/<name> @ <short-sha>..<short-sha>
**Done:** bullet list of what actually works now (be honest — "renders but untested" ≠ "done")
**How to see it:** exact command + what to click (e.g. `npm run dev` → press D → deny 3 patrons)
**Tests:** X passing / Y failing (name the failures)
**Broke / known-wonky:** anything you left imperfect
**Contract change requests:** none | numbered list with justification
**Questions for reviewer:** none | numbered list
**Next session should:** bullet list
```
## REVIEW block template (reviewer only)
```markdown
---
### REVIEW — Fable — <date>
**Reviewed:** <lanes/commits>
**Merged to main:** <yes/no + sha>
**Contract decisions:** rulings on any open change requests
**Instructions for <LANE>:** what to do next / what to fix first
```
---
*(log starts below)*

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# NOT TONIGHT
*A hospitality power trip.* Papers, Please meets a Saturday night in Fortitude
Valley: you're the head of security at a volatile, pretentious nightclub. Protect
the Vibe. Watch the queue Aggro. Don't let the Liquor Licensing Inspector catch
you slipping.
Browser game — Phaser 3 · TypeScript · Vite. Pixel art, synthesized techno,
one very loud DENIED stamp.
## Docs
- [Game design](docs/GAME_DESIGN.md) — what the game is
- [Build plan](docs/BUILD_PLAN.md) — architecture, phases, milestones
- [Contracts](docs/CONTRACTS.md) — shared types & events (law)
- [Lane handover log](LANEHANDOVER.md) — build coordination
- Lane instructions: [scaffold](lanes/LANE0_SCAFFOLD.md) · [door](lanes/LANE_DOOR.md) · [floor](lanes/LANE_FLOOR.md) · [juice](lanes/LANE_JUICE.md)
## Dev (once Phase 0 lands)
```sh
npm install
npm run dev # parade / demo scenes
npm test
```

104
docs/BUILD_PLAN.md Normal file
View File

@ -0,0 +1,104 @@
# NOT TONIGHT — Build Plan & Technical Architecture
*Read [GAME_DESIGN.md](GAME_DESIGN.md) first. This doc is the engineering plan the
lanes execute. Contract details live in [CONTRACTS.md](CONTRACTS.md).*
## 1. Stack (decided — don't relitigate)
| Choice | What | Why |
|---|---|---|
| Engine | **Phaser 3 (latest 3.x)** | Scenes map 1:1 to game phases; tweens, input, Light2D for the flashlight cone; huge ecosystem |
| Language | **TypeScript, strict** | Multiple parallel lanes NEED typed contracts; the rules engine is pure typed functions |
| Build | **Vite** | Instant dev server, static build output |
| Tests | **Vitest** | Rules engine, patron generator, ID validity, clock — all pure logic, all unit-testable |
| Audio | **Raw WebAudio** (no Howler) | The low-pass-filter-as-location trick and the beat-clock need direct node graph access |
| Saves | localStorage | No backend. Ever. |
| Art (v0.x) | **Runtime paper-doll renderer** | Patrons are layered data (see design §4.1); placeholder layers are coloured-pixel shapes drawn to a texture. Real pixel-art layers swap in later behind the SAME layer contract |
| Deploy | Static `dist/` | Eventually → partly.party games VPS. Not a v0.1 concern |
## 2. Repo layout (ownership boundaries = merge-conflict avoidance)
```
src/
core/ # LANE-0 then frozen: EventBus, GameClock, SeededRNG, GameState, save
data/ # LANE-0 then frozen-ish: types + static config (venues, nights, rules, archetypes)
patrons/ # LANE-0: generator, paper-doll renderer, memory/regulars
rules/ # dress-code engine, ID validator, sobriety logic (pure functions ONLY)
scenes/
door/ # LANE-DOOR owns
floor/ # LANE-FLOOR owns
shared/ # Boot, Menu, NightSummary, HUD — LANE-0 stubs, integration fills
audio/ # LANE-JUICE owns: techno engine, sfx synth
ui/ # LANE-JUICE owns: stamp, phone, dialogue box, clicker widgets
tests/ # mirrors src/; every lane adds tests for its pure logic
docs/ # design + this plan + CONTRACTS.md
lanes/ # lane instruction files
LANEHANDOVER.md
```
**Rule: a lane commits only inside its owned directories + its tests + LANEHANDOVER.md.**
Changes to `core/`, `data/` types, or `CONTRACTS.md` after Phase 0 require a
CONTRACT CHANGE REQUEST in the handover file and reviewer sign-off — never just do it.
## 3. Phases
### Phase 0 — Scaffold & Contracts (ONE lane, sequential — everything depends on it)
`lanes/LANE0_SCAFFOLD.md`. Deliverable: repo compiles, tests green, and a **Patron
Parade** demo scene proves the generator + paper-doll renderer (generated patrons
walk across the screen; ID card renders from the same data). All shared types in
`src/data/types.ts` matching CONTRACTS.md. Event bus + seeded RNG + game clock done
and tested.
### Phase 1 — Parallel lanes (after Phase 0 review passes)
Three lanes run concurrently on separate branches/worktrees:
- **LANE-DOOR** (`lanes/LANE_DOOR.md`) — the Door scene, full v0.1 loop: queue, ID
minigame, dress-code stamps, clicker, Dazza texts, verdicts, Vibe/Aggro wiring.
*This is the v0.1 critical path — the game is shippable with only this lane done.*
- **LANE-FLOOR** (`lanes/LANE_FLOOR.md`) — top-down Floor scene: patrol movement,
flashlight/UV cone, drunk detection, Cut-Off dialogue, pat-down minigame, toilet
rhythm game (against a stub beat clock until integration).
- **LANE-JUICE** (`lanes/LANE_JUICE.md`) — WebAudio techno engine + low-pass
location filter + beat clock; SFX synth (stamp, thud, static, clicker); reusable
UI widgets (stamp, phone, dialogue). Develops against its own demo scenes.
Lanes talk ONLY through the event bus + contracts. No lane imports another lane's
scene code. Beat sync example: audio engine emits `beat:tick` on the bus; the toilet
rhythm game consumes `beat:tick`; until integration, LANE-FLOOR uses the
`StubBeatClock` from core (same event, fixed 128 BPM).
### Phase 2 — Integration (ONE lane, sequential)
Night flow state machine (Door ⇄ Floor interleave, radio pulls, Last Drinks,
Summary), real audio engine replaces stubs, save/progression, Kayden-at-door
simulation while player is on Floor. Playtest pass: one full night start→summary.
### Phase 3 — Content & polish (parallelizable again)
Archetypes, scripted moral encounters, inspector, incident report memory, regulars,
venues 24, real pixel art swapped into the paper-doll layer slots, MODELBEAST-
assisted art generation if useful.
## 4. Coordination protocol (all lanes)
1. **Branches:** each lane works on `lane/<name>` (e.g. `lane/door`), pushed to
origin (`ssh://git@100.71.119.27:222/monster/not-tonight.git`). Never push main.
On one machine, use worktrees: `git worktree add ../not-tonight-door lane/door`.
2. **Handover:** every work session APPENDS a session block to `LANEHANDOVER.md`
(template inside it) and pushes. This is how the reviewer (Fable) sees state.
3. **Review gate:** reviewer reads handovers + diffs, replies with a REVIEW block in
the same file (or a fresh lane instruction file), merges approved lanes to main.
4. **Contract changes:** request-only via handover. If blocked >30 min on a contract
ambiguity, write the question in the handover, make the smallest local stub that
unblocks you, mark it `// TODO(contract):`, and continue.
5. **Definition of done for any session:** compiles (`npm run build`), tests green
(`npm test`), demo scene for your lane runs (`npm run dev`), handover written.
## 5. Milestone acceptance
- **M0 (Phase 0):** `npm run dev` shows Patron Parade; ≥25 unit tests green covering
RNG determinism (same seed → same patrons), ID validity math, layer rendering data.
- **M1 (v0.1):** One full Door-only night playable start→summary; a stranger can
understand it with no explanation; fail states (Vibe 0, Aggro 100) both reachable.
- **M2 (v0.2):** Full night with ≥2 forced Floor interludes; toilet game verifiably
on-beat (log beat timestamps vs hit timestamps in tests).
- **M3 (v0.3):** 3-night run with persistence; an incident-report lie can bite you a
night later.

156
docs/CONTRACTS.md Normal file
View File

@ -0,0 +1,156 @@
# NOT TONIGHT — Shared Contracts
*The typed spine of the project. Phase 0 implements these verbatim in
`src/data/types.ts` and `src/core/`. After Phase 0 review, changes require a
CONTRACT CHANGE REQUEST in LANEHANDOVER.md. Lanes may EXTEND (new event names in
their namespace, new fields marked optional) but never repurpose existing fields.*
## 1. Patron
```ts
export type Archetype =
| 'fresh18' | 'almost18' | 'financeBro' | 'influencer'
| 'regular' | 'ownersMate' | 'quietMessy' | 'inspector' | 'punter';
export type DrunkStage = 'sober' | 'tipsy' | 'loose' | 'messy' | 'maggot';
export interface OutfitLayer {
slot: 'shoes' | 'legs' | 'top' | 'outer' | 'hair' | 'accessory';
type: string; // e.g. 'sneaker' | 'thong' | 'singlet' | 'blazer' — open vocab, see data/outfits.ts
colour: string; // canonical colour name, not hex: 'white' | 'black' | 'red' ...
logo?: boolean;
vintage?: boolean;
}
export interface IdCard {
name: string;
dob: string; // ISO date. Age math vs GameClock date is the minigame.
expiry: string; // ISO date
photoSeed: number; // paper-doll seed the photo was rendered from
fake?: { // present only on fakes; each flag = one visible tell
peelingLaminate?: boolean;
wrongHologram?: boolean;
jokeName?: boolean; // e.g. 'M. Lovin'
photoMismatch?: boolean; // photoSeed deliberately ≠ patron.dollSeed
};
}
export interface Patron {
id: string; // stable across the run — regulars keep theirs
dollSeed: number; // drives paper-doll face/body render
archetype: Archetype;
age: number; // true age; ID may disagree
idCard: IdCard;
outfit: OutfitLayer[];
intoxication: number; // 0..1, drifts up per tolerance
tolerance: number; // 0..1
flags: {
contraband?: string[]; // item ids from data/contraband.ts
onGuestList?: boolean;
claimsGuestList?: boolean;
knowsOwner?: boolean; // the trap: claims can be TRUE
uvStamped?: boolean;
timesDeniedBefore?: number; // regulars' grudge counter
disguised?: boolean;
};
}
export const drunkStage = (x: number): DrunkStage => /* thresholds in rules/ */
```
## 2. Rules engine (pure functions only — no Phaser imports in `src/rules/`)
```ts
export interface DressCodeRule {
id: string;
text: string; // exactly what Dazza's text says
activeFrom: number; // clock minutes since 21:00
violates: (p: Patron) => boolean; // evaluates DATA, never pixels
}
export type Verdict = 'admit' | 'deny' | 'sobrietyTest' | 'patDown' | 'wait';
export interface VerdictOutcome { // returned by rules/judge.ts, consumed by scenes
vibeDelta: number;
aggroDelta: number;
heatStrike?: { reason: string; deferred?: boolean }; // deferred = surfaces on inspection/audit
dazzaText?: string;
}
```
## 3. Game state & meters
```ts
export interface NightState {
venueId: string; nightIndex: number;
vibe: number; // 0..100
aggro: number; // 0..100
heatStrikes: HeatStrike[]; // run-scoped, max 3
hype: number; // vibe multiplier from queue theatre
capacity: { inside: number; licensed: number; clickerShown: number }; // clickerShown can drift if you misclick — that's the game
clockMin: number; // minutes since 21:00; night ends 360
location: 'door' | 'floor';
incidents: IncidentRecord[]; // truth log; report form may diverge — divergence is stored
}
```
`GameState` (run-scoped) holds: seed, venue progression, regulars' memory
(`Map<patronId, RegularMemory>`), past incident reports (for cross-night audit).
## 4. Event bus (`src/core/EventBus.ts` — typed emitter over this map)
```ts
export interface EventMap {
// clock & flow
'clock:tick': { clockMin: number };
'night:phaseChange': { location: 'door' | 'floor' };
'radio:call': { urgency: 1|2|3; line: string }; // floor pulling you in
// door
'door:patronUp': { patron: Patron };
'door:verdict': { patron: Patron; verdict: Verdict; outcome: VerdictOutcome };
'door:clicker': { direction: 'in'|'out' };
'dazza:text': { text: string; rule?: DressCodeRule };
// floor
'floor:infractionSpotted': { patronId: string; kind: 'drunk'|'stall'|'contraband'|'noStamp'|'fight' };
'floor:ejection': { patronId: string; style: 'clean'|'messy' };
// audio (LANE-JUICE emits; StubBeatClock emits same shape at fixed 128bpm)
'beat:tick': { beatIndex: number; audioTimeMs: number };
'audio:location': { location: 'door'|'floor' }; // drives low-pass cutoff
// meters (single writer: core/meters.ts — scenes emit deltas, never set directly)
'meters:delta': { vibe?: number; aggro?: number; hype?: number };
'meters:changed': { vibe: number; aggro: number; hype: number };
'heat:strike': HeatStrike;
}
```
**Bus discipline:** scenes emit `meters:delta`; only `core/meters.ts` mutates state
and emits `meters:changed`. HUD listens to `meters:changed` only. This keeps three
lanes from fighting over the same numbers.
## 5. Paper-doll renderer (`src/patrons/doll.ts`)
```ts
// Renders a Patron (dollSeed + outfit layers) to a Phaser texture at 3 sizes:
export type DollPose = 'queue' | 'idPhoto' | 'floorTopDown';
export function renderDoll(scene: Phaser.Scene, p: Patron, pose: DollPose): string; // returns texture key
```
v0.x layers are procedural coloured-pixel shapes; the function signature is the
permanent contract so real art swaps in without touching callers. Drunk sway/red-eye
tells are render-time modifiers driven by `intoxication` — renderer owns tells,
rules own thresholds.
## 6. RNG & determinism
`SeededRNG` (mulberry32 or similar) — ALL generation flows from the run seed via
named child streams: `rng.stream('patrons')`, `rng.stream('idFakes')`, etc.
No `Math.random()` anywhere (add an eslint rule). Same seed ⇒ identical night.
Tests assert this.
## 7. Naming & conventions
- Event names: `domain:eventName` as above; new events go in your lane's domain.
- No cross-lane scene imports. Shared visual bits live in `ui/` (LANE-JUICE owns).
- Aussie dialogue strings live in `data/strings/` as typed const maps, not inline.
- Every pure module gets a test file. Scenes don't need tests; their logic should
be thin enough not to want them (if it isn't, move logic into `rules/`).

186
docs/GAME_DESIGN.md Normal file
View File

@ -0,0 +1,186 @@
# NOT TONIGHT — Game Design Document
*Working title: **NOT TONIGHT** (aka "Hard No: A Hospitality Power Trip")*
*Genre: bureaucracy-sim / power-trip catharsis. Papers, Please meets a Saturday night in Fortitude Valley.*
*Platform: browser (desktop-first, mouse-driven). Pixel art.*
---
## 1. Pitch
You are the Head of Security at a volatile, pretentious nightclub. Your job is not to
make money. Your job is to protect the **Vibe** — by keeping the wrong people out,
throwing the messy people onto the street, and doing it all with absolute,
unquestionable authority.
But you're squeezed from both sides: management's dress-code demands get more
psychotic by the hour, and the Liquor Licensing Inspector is circling. One minor
through the door on a fake ID and you're not power-tripping anymore — you're
unemployed.
## 2. The Three Meters (core economy)
Everything in the game moves one of three numbers:
| Meter | Range | Goes UP when | Goes DOWN when | At zero / max |
|---|---|---|---|---|
| **VIBE** | 0100 | Right people in, messy people ejected stylishly, queue hype | Wrong people in, dead floor, fights you missed | Vibe 0 = Dazza fires you (fail night) |
| **AGGRO** (queue) | 0100 | Making people wait, arbitrary denials, rain | Letting people in, smooth queue flow | Aggro 100 = queue riot / someone glasses the door (fail night) |
| **HEAT** (legal) | 03 strikes | Minor admitted, maggot-drunk served/ignored, capacity breach, incident-report lie discovered | Never decreases within a night | 3 strikes = licence pulled, run over (fail RUN, not just night) |
The design intent: **Vibe rewards cruelty, Heat punishes carelessness, Aggro punishes
slowness.** Every decision at the rope pulls at least two of them in opposite
directions. That tension is the whole game.
## 3. The Night Loop
A night = **Door phase → Floor phase (interleaved) → Last Drinks → Night Summary.**
The clock runs 9:00 PM → 3:00 AM (compressed to ~1215 real minutes). You are
physically in ONE place at a time. While you're on the Floor, the queue Aggro climbs
and the door is worked (badly) by Trainee Kayden. While you're at the Door,
infractions accumulate on the Floor. **Deciding where to be is a resource decision.**
A radio crackle ("mate you'd better get in here") tells you when the Floor needs you.
### 3.1 Phase 1 — THE DOOR (static desk scene, Papers-Please grammar)
Forward-facing tableau: the rope, the queue snaking off-screen, rain, the kebab shop
glow across the road. Patrons step up one at a time. Your desk/tools:
- **The Rope button.** Unhook to call the next patron up. You can just… not.
Making people wait builds **Hype** (a Vibe multiplier) but ticks Aggro.
You can look at your phone while they stand there. This is a feature.
- **ID tray.** Patron hands over ID. Checks: **birthday math** (are they 18 *as of
tonight's date*?), expiry, photo vs face (paper-doll hair/skin/face features must
match), obvious fakes (laminate peel, "McLovin"-tier joke names, wrong state
hologram). Admitting a minor = instant Heat strike *when the inspector audits*.
- **The Clicker.** Physical capacity counter. Click IN, click OUT. Venue has a
licensed capacity; going over is a Heat strike if inspected. (The clicker is also
just… satisfying. Make it clunky and loud.)
- **Dress-code card.** Management (Dazza) texts new rules through the night,
escalating from sane to unhinged:
- 9:30 PM — "no thongs, no singlets"
- 10:30 PM — "no visible logos"
- 11:30 PM — "no white sneakers UNLESS vintage"
- 1:00 AM — "no one who looks like they'd request a song"
Rules are live the moment the text arrives. You scan the sprite, then **DENIED**
(giant red stamp) or **wave-through**. Wrong admits drain Vibe when Dazza does a
lap and spots them.
- **Sobriety check.** Red eyes / sway in the idle animation / hiccup SFX are tells.
Trigger a test: recite the alphabet backwards, answer an unhinged riddle, walk the
line (cursor-follow minigame). You may fail them *even if they pass*. (Aggro cost.)
- **Guest list clipboard.** Misspelled names, "+1"s that arrive as +6, and the trap:
"I know the owner." Usually a lie (deny → Vibe up). Sometimes TRUE (deny →
massive Vibe hit + a furious Dazza text). Tells exist if you look hard enough.
- **UV pass-out stamp.** Anyone stepping out for a smoke gets a UV hand stamp.
Re-entry = check the stamp. This pays off in the Floor phase (see flashlight).
**Verdict verbs:** ADMIT · DENY (stamp) · SOBRIETY TEST · PAT-DOWN (send to
contraband check) · "one sec" (leave them standing there).
### 3.2 Phase 2 — THE FLOOR (top-down patrol)
Top-down 2D crawler through the dark venue — bar, dance floor, booths, toilets,
smoking area. Nearly black except neon accents; your **flashlight cone** reveals
detail. The flashlight is also the **UV lamp** (toggle): checking pass-out stamps,
spotting spiked drinks glowing wrong.
| Infraction | Detection | Mini-game | Notes |
|---|---|---|---|
| **The Cut-Off** | Sway animation at the bar, bumping into people | Aggressive dialogue tree: "Have some water, mate" → "You're done for the night." Escalating drunk states: Tipsy → Loose → Messy → **Maggot** | Leaving a Maggot on the floor when the inspector walks in = Heat strike. Escort-out animation: their avatar slouches in defeat. |
| **Toilet intruders** | Two pairs of feet under one stall door | **Rhythm game:** time your door-bangs to the muffled techno kick. On-beat bangs = shock factor multiplier, they scatter. Off-beat = they ignore you, Vibe leaks | The beat is the actual audio engine beat — see §5 |
| **Contraband** | Suspicious idle (patting pockets, glancing) | **Pat-down:** rapid drag-and-drop from pockets into the AMNESTY BIN. Chaotic item pool: baggies, flasks, a whole kebab, a live budgie, someone else's ID | Timed; miss items and they walk with them |
| **No stamp** | UV lamp on a re-entered patron shows nothing | March them back to the door | They snuck past Kayden. Kayden apologises on radio. |
| **Fight brewing** | Two patrons in escalating shove animation | Get physically between them before the swing timer ends | Missed fight = big Vibe + Aggro hit, possible Heat |
### 3.3 Last Drinks & Night Summary
3:00 AM: lights up (the floor scene literally brightens — brutal), sweep the
stragglers, then the **Night Summary**:
- Vibe graph across the night, cash (wage + Dazza's mood bonus), Heat status.
- **The Incident Report.** A form. You fill in what happened. You can lie
("the patron fell over by himself"). Lies reduce Heat *now* but the inspector
cross-references reports across nights — contradictions surface later as strikes.
This is the game's long-memory conscience mechanic.
## 4. People
### 4.1 Patrons are data, not sprites (THE core technical/design decision)
Every patron is a generated record:
- demographics: age (some 17½ — that's the ID game), archetype
- **outfit as typed layers**: shoes {type, colour, vintage?}, top {type, logo?},
accessories… — the paper-doll renderer draws the layers; the dress-code engine
evaluates the same data. *The player judges pixels; the rules judge data.* The gap
between those two is where the gameplay lives.
- intoxication (drifts up over the night, per-patron tolerance)
- hidden flags: fake ID?, contraband?, on guest list?, actually-knows-the-owner?
- **memory**: regulars persist across nights (seeded RNG). Deny the same bloke
twice and he comes back in a bad disguise (fake moustache on the same sprite —
same walk animation gives him away).
### 4.2 Archetypes (initial set)
The Fresh-18 (nervous, ID is REAL, don't be a monster) · The Almost-18 (confident,
ID is fake) · The Finance Bros (travel in packs, one is always Maggot) · The
Influencer ("I'm on the list", films you when denied — denying costs Aggro, gains
Vibe) · The Regular (knows your name, tips) · The Owner's Mate (the trap) ·
The Quiet Messy One (seems fine at the rope, Maggot by midnight — tells were there) ·
**The Inspector** (plainclothes! reads as a normal boring patron; if you're breaching
when they reveal, strikes rain down).
### 4.3 The moral undertow (why it's not just cruelty)
Papers, Please works because the stamps land on people. Scatter scripted encounters
through the generated crowd: the girl who *needs* back in to grab her mate from a bad
date (no stamp, over capacity — your call); the freshly-dumped guy who just wants one
quiet beer (he's already Loose); the regular's last night before moving away. Doing
the human thing usually costs Vibe or risks Heat. The game never tells you which
choice was right. This is the difference between satire and a bullying simulator —
**protect this in every content pass.**
### 4.4 Dazza (management, via text)
Never seen. Escalating texts: dress codes, vibe complaints ("why is the floor MID"),
2 AM philosophy, occasionally something that reveals he's terrified of the owner too.
The unreliable authority you serve.
## 5. Aesthetic
- **Visuals:** gritty high-contrast pixel art. Rain-slick street, neon signage, dark
interior with light pools. Exaggerated animations: dramatic drunk falls, the
defeated slouch of the ejected, the stamp SLAM.
- **Audio (the signature trick):** ONE looping synthesized techno track (WebAudio),
played through a **low-pass filter whose cutoff = your location**. At the door:
muffled kick through the wall. Step inside: filter opens, full track. The toilet
rhythm game syncs to this same clock — the beat is authoritative from the audio
engine, not an animation timer. UI sounds heavy and aggressive: stamp slams, door
thuds, radio static, the clicker's clunk.
- **Tone of writing:** Australian. "Mate", "youse", "carn". Affectionate, not
mocking. The kebab shop across the road is visible all night and is the true
moral centre of the game.
## 6. Progression
Venues as levels, nights as rounds (Thu → Fri → Sat per venue):
1. **The Royal (dive bar).** Tutorial. Loose rules, low stakes, capacity 80.
2. **Voltage (Valley club).** Full ruleset, guest list, inspector active.
3. **Elevate (rooftop).** Dress code hell, influencer density, VIP politics.
4. **ROOM (underground techno, boss venue).** Dress code re-randomises every 15
in-game minutes, everyone claims VIP, the inspector is *definitely* in the
building, and the techno is so loud all dialogue is written as lip-read guesses.
Roguelite-lite structure: fail a night → retry the night; 3 Heat strikes → run over.
Seeded runs for shareability ("seed VALLEY-4207 is brutal").
## 7. Scope guardrails
- v0.1 = ONE venue, Door phase only, 3 nights, placeholder paper-doll art. Ship it.
- v0.2 = Floor phase + door/floor interleave + audio engine.
- v0.3 = progression, regulars' memory, incident report, inspector.
- v0.4 = content pass (archetypes, scripted encounters), real pixel art, boss venue.
- Cut before slipping: fight mini-game, budgie, venue 3. Never cut: the clicker,
the stamp feel, the low-pass filter trick.

98
lanes/LANE0_SCAFFOLD.md Normal file
View File

@ -0,0 +1,98 @@
# LANE-0 — Scaffold & Contracts (Phase 0, single lane)
**Executor:** Opus 4.8. **Branch:** `lane/scaffold`. **Prereq reading (in order):**
`docs/GAME_DESIGN.md``docs/BUILD_PLAN.md``docs/CONTRACTS.md``LANEHANDOVER.md`.
Everything downstream depends on this lane being solid. Favour boring correctness
over cleverness; three parallel lanes will build on your exact interfaces.
## Deliverable
Repo compiles strict-TS clean, ≥25 unit tests green, and `npm run dev` shows the
**Patron Parade**: an endless stream of generated patrons walking across a dark
street scene, each with their ID card popping up beside them, rendered from the SAME
data record. This demo is the proof that the generator, paper-doll renderer, RNG
determinism, and clock all work.
## Tasks (in order)
### 1. Project scaffold
- `npm create vite@latest` (vanilla-ts) + `phaser` + `vitest`. Strict tsconfig
(`strict`, `noUncheckedIndexedAccess`). ESLint flat config with a rule banning
`Math.random` (`no-restricted-properties`).
- Scripts: `dev`, `build`, `test`, `lint`. All four must work before anything else.
- Pixel-art rendering config: `pixelArt: true`, integer zoom, 640×360 base
resolution scaled up. Phaser game boots to a `BootScene``ParadeScene`.
### 2. `src/core/`
- **`SeededRNG.ts`** — mulberry32. `rng.stream(name)` returns an independent child
generator derived from (seed, name). Helpers: `int(min,max)`, `pick(arr)`,
`chance(p)`, `weighted(entries)`. TEST: same seed+stream ⇒ identical sequence;
different streams don't correlate; 10k-draw distribution sanity checks.
- **`EventBus.ts`** — typed emitter over `EventMap` from CONTRACTS.md §4 (implement
the full map now, even events nothing emits yet). `on`, `off`, `emit`, `once`.
TEST: typed payloads, off during emit, once semantics.
- **`GameClock.ts`** — converts real elapsed ms → clock minutes (config: night is
360 clock-min over 13 real minutes; curve can be linear v0). Emits `clock:tick`
once per clock-minute. Pausable. Exposes `nightDate` (an actual date, needed for
ID birthday math — pick Sat 2026-07-18 as night 1). TEST: tick counts, pause,
date math across a leap-year birthday.
- **`meters.ts`** — owns NightState numbers. Listens `meters:delta`, clamps 0..100,
emits `meters:changed`; `heat:strike` appends and is run-scoped. TEST: clamping,
single-writer behaviour, hype multiplier applied to positive vibe deltas.
- **`StubBeatClock.ts`** — emits `beat:tick` at fixed 128 BPM from
`setInterval`-free Phaser time events. LANE-JUICE will replace it behind the same
event; keep it dumb.
- **`save.ts`** — (de)serialise run-scoped GameState to localStorage, versioned
(`{v: 1, ...}`), corrupt-save-safe (falls back to fresh). TEST: roundtrip,
corruption fallback.
### 3. `src/data/`
- **`types.ts`** — the CONTRACTS.md types, verbatim.
- **`outfits.ts`** — the outfit vocabulary: per-slot type lists with weights and
colour palettes. Include everything the design's dress codes will need: thongs,
singlets, white sneakers (with `vintage` variants), logo tops, blazers, bucket
hats. ~812 types per major slot.
- **`archetypes.ts`** — per-archetype generation params (age ranges, outfit biases,
tolerance ranges, flag probabilities). All 9 archetypes from CONTRACTS.md, even
though v0.1 only surfaces some — cheap now, annoying later.
- **`contraband.ts`** — item table (id, name, sprite hint, severity): baggie, flask,
goon sack, whole kebab, someone-else's-ID, live budgie.
- **`strings/dazza.ts`** — 15+ Dazza texts incl. the 4 dress-code escalations from
the design doc. Typed const maps.
### 4. `src/patrons/`
- **`generator.ts`** — `generatePatron(rng, clockMin, archetypeOverride?)`:
full Patron per CONTRACTS.md. Critical details:
- `almost18`: age 17, ID fake with 12 tells; `fresh18`: age 18 + days(0..60),
ID REAL. DOBs must make the birthday math genuinely non-trivial (cluster DOBs
within ±90 days of the 18-year boundary relative to `nightDate`).
- `photoSeed === dollSeed` unless `photoMismatch`.
- intoxication start scales with clockMin (crowd arrives drunker later).
TEST heavily: determinism, age/ID consistency matrix (real IDs never have `fake`;
every fake has ≥1 tell), archetype constraint spot-checks.
- **`doll.ts`** — `renderDoll(scene, patron, pose)` per CONTRACTS.md §5.
Procedural placeholder v0: layered coloured-rect/pixel shapes onto a
`Phaser.Textures.CanvasTexture` — body silhouette from dollSeed (skin tone, build,
hair shape/colour), then outfit layers in slot order, distinct enough that a
player can SEE "white sneakers" vs "black boots" at queue size (~32×48px).
Poses: `queue` (front-facing), `idPhoto` (head crop, flat background),
`floorTopDown` (~16×16 blob with hair colour + top colour). Drunk tells: sway
offset + red eye pixels above `intoxication` thresholds. Deterministic per
(patron, pose): TEST that texture pixels for same input are identical.
- **`memory.ts`** — RegularMemory store keyed by patron id (timesDenied, lastSeen
night, grudge). Just the store + tests; behaviour wiring is Phase 2/3.
### 5. `src/scenes/shared/` stubs
- `BootScene` (asset-less boot, config), `ParadeScene` (the deliverable demo:
patrons spawn on the left, walk across a dark street with rain particles and a
kebab-shop glow rectangle, ID card texture shown above each; press SPACE to
respawn with a new seed, seed shown on screen), `HudStub` (renders
`meters:changed` values as text).
### 6. Handover
Append your SESSION block to `LANEHANDOVER.md` (template inside it), push
`lane/scaffold`. Reviewer merges to main and opens Phase 1.
## Definition of done
`npm run lint && npm run build && npm test` all clean; parade runs at 60fps with
20+ patrons on screen; same seed reproduces the same parade; handover written.

102
lanes/LANE_DOOR.md Normal file
View File

@ -0,0 +1,102 @@
# LANE-DOOR — The Door (Phase 1, parallel)
**Executor:** Opus 4.8. **Branch:** `lane/door` (from main after Phase 0 merge).
**Owns:** `src/scenes/door/`, `src/rules/` (shared with reviewer approval — you are
its primary author in Phase 1), `tests/` mirrors. **Prereq reading:**
`docs/GAME_DESIGN.md` §3.1 → `docs/CONTRACTS.md``LANEHANDOVER.md` (check for
REVIEW blocks addressed to you).
**You are the v0.1 critical path.** When this lane is done, the game is shippable
as a Door-only night. Bias every decision toward "playable and funny" over
"architecturally complete".
## Deliverable
`npm run dev` → full Door-only night: 9:00 PM to 3:00 AM, patrons queue and step
up, you inspect and rule on them, Dazza texts land, meters move, night ends in a
summary screen (win) or a fail state (Vibe 0 / Aggro 100). A stranger can play it
with zero explanation.
## Scene layout (640×360, static tableau)
Street view: queue snaking from the left, rope + patron-up position centre, venue
door right, rain + kebab-shop glow (reuse ParadeScene ambience). Desk UI along the
bottom: ID tray · dress-code card (latest rules) · clicker · phone · verdict
buttons. Patron steps up BIG (queue-pose doll scaled ×3) so outfit details read.
## Systems to build (in rough order)
### 1. Queue manager (`door/QueueManager.ts`)
Spawns patrons from the generator on a clock-driven arrival curve (quiet 9 PM,
slams 10:3012:30). Maintains visible queue of ≤8 dolls + an offscreen count.
**The Rope:** next patron only steps up when the player clicks the rope hook.
Idle time while queue is non-empty and no patron is up: +hype (capped), +aggro
(uncapped, accelerating). Wire via `meters:delta`. Phone prop: clicking your phone
while someone waits gives bonus hype and a little animation — reward the theatre.
### 2. Inspection surface
Patron-up view with hover/click zones: face (compare to ID photo), each outfit
slot (tooltip: "white sneakers — look new"), eyes (bloodshot?), stance (sway tween
driven by intoxication tell from doll renderer). The ID card: click tray → card
zooms to half-screen; DOB, expiry, name, photo (rendered `idPhoto` pose from
`photoSeed` — mismatch tell = it's visibly a different doll). Fake tells per
CONTRACTS.md: peeling laminate (corner pixels), wrong hologram (wrong colour
shimmer), joke name.
### 3. Rules engine (`src/rules/` — PURE, tested, no Phaser imports)
- `dressCode.ts`: implement `DressCodeRule[]` for night 1 (the 4 escalations from
the design doc) + 4 more of your own invention in the same escalating-absurdity
register. `activeRules(clockMin)`, `violations(patron, clockMin)`.
- `idCheck.ts`: `idVerdict(idCard, nightDate)``{underage, expired, tells[]}`.
All date math here, tested against boundary cases (18th birthday IS tonight;
expiry tomorrow; leap-day DOB).
- `judge.ts`: `judge(patron, verdict, ctx) → VerdictOutcome` — the single scoring
function. Encode the design's tensions:
- deny a violator: +vibe; deny a clean punter: +vibe small, +aggro (arbitrary
power is rewarded but heats the queue)
- admit a violator: deferred vibe ("Dazza does a lap" ~25 min later → dazza
text + vibe hit)
- admit underage: deferred `heat:strike` (fires on audit/inspection, Phase 2 —
for v0.1 fire it at night end so it still hurts)
- deny an actual owner's mate: big vibe + furious Dazza text
TEST every branch of this table.
### 4. Verdict flow & stamps
Verdict buttons: ADMIT / DENY / TEST / PAT-DOWN / (rope = wait). DENY = the big
red stamp: button press → stamp slams down over the patron with screen shake and a
dust ring, patron does the defeated-slouch walk-off. **Spend real time on this
feel** — it's the game's signature interaction. (LANE-JUICE will supply the stamp
widget + SFX later; build a local version now, swap on integration — mark it
`// TODO(juice): swap for ui/Stamp`.) ADMIT: rope unhooks, patron walks in the
door, **clicker increments only if the player clicks it** — the on-screen clicker
count vs true inside-count divergence is tracked in NightState per CONTRACTS.md.
### 5. Sobriety test (dialogue minigame)
Modal dialogue: pick one of 3 challenges (alphabet backwards / riddle / cursor
line-walk — implement the first two v0.1, line-walk stretch). Their answer quality
is derived from intoxication + rng, rendered as typed-out text with drunk typos.
Player then rules PASS/FAIL regardless of performance — failing someone who
passed: +vibe, +aggro. It's a power trip, remember.
### 6. Dazza & the phone
Phone widget (bottom corner): buzz animation + preview on `dazza:text`; click to
open thread history. Rule-bearing texts add the rule to the dress-code card with a
NEW badge. Strings from `data/strings/dazza.ts`; add more as needed (keep his
voice: aggressive lowercase, no punctuation, occasional fear of the owner).
### 7. Night shell (temporary, yours for v0.1)
Minimal `NightScene` wrapper: starts clock, runs Door, listens for fail states,
3 AM → `NightSummaryScene` (temporary version: vibe graph from sampled history,
cash = base + hypePeak bonus, verdict stats table). Phase 2 integration will
replace this shell — keep it thin and clearly marked `// TODO(integration)`.
## Constraints
- Consume `beat:tick` from StubBeatClock for any rhythm flourish; never your own timer.
- All scoring via `meters:delta` — never touch NightState directly.
- No imports from `scenes/floor/` or `audio/`.
- Dialogue/strings in `data/strings/`, Aussie register per design doc §5.
## Definition of done
Full night playable; both fail states reachable by playing badly on purpose;
`lint`/`build`/`test` clean; rules-engine coverage includes every judge branch;
SESSION block appended to LANEHANDOVER.md; branch pushed.

85
lanes/LANE_FLOOR.md Normal file
View File

@ -0,0 +1,85 @@
# LANE-FLOOR — The Floor (Phase 1, parallel)
**Executor:** Opus 4.8. **Branch:** `lane/floor` (from main after Phase 0 merge).
**Owns:** `src/scenes/floor/` + its tests. **Prereq reading:** `docs/GAME_DESIGN.md`
§3.2 → `docs/CONTRACTS.md``LANEHANDOVER.md` (check for REVIEW blocks for you).
## Deliverable
`FloorDemoScene` runnable via `npm run dev` (menu key F): a populated top-down
venue you patrol with a flashlight, containing live instances of all three v0.2
infractions (cut-off, toilet stall, pat-down) + UV stamp checks. Runs standalone —
it self-populates from the patron generator; integration (Phase 2) will hand it the
real admitted-patron list instead. Code against `Patron[]` input from day one.
## Scene: the venue (top-down, ~2× screen scrollable)
Tilemap (hand-authored array is fine, no Tiled dependency): entry corridor, bar
(along a wall, stools), dance floor (centre), booths, toilets (2 stalls each,
door tiles), smoking-area door. **Darkness is the aesthetic:** black base with neon
accent strips; use Phaser Light2D (`setPipeline('Light2D')`) — ambient very low,
neon point lights, and the player's cone.
### 1. Player & flashlight
WASD/arrows patrol, slightly heavy acceleration (you're a big unit). Flashlight
cone in facing direction — a masked light + a cone polygon that REVEALS detail:
outside the cone patrons render as dim silhouettes (tint), inside it their full
doll colours + tell animations show. TAB toggles **UV mode**: cone goes purple,
normal detail hidden, `uvStamped` hands glow green. No stamp glow on a patron who
re-entered = infraction (march them doorward: walk-behind escort, below).
### 2. Crowd sim (`floor/CrowdSim.ts`)
Patrons wander between anchor points (bar → dance floor → booth → toilet loop)
with per-archetype weights. Dance-floor patrons bob **on `beat:tick`** (StubBeatClock
until integration — never your own timer). Intoxication drifts up over clock time
(bar visits accelerate it); drunk-stage thresholds from `rules/` if LANE-DOOR has
landed them, else local `// TODO(contract)` copy. Visible drunk tells escalate:
sway amplitude, stumble pathing, bumping others (small vibe leaks via
`meters:delta` if un-handled).
### 3. Infraction: The Cut-Off
Approach a `messy`+ patron, interact (E) → dialogue tree modal: three escalation
lines ("Have some water, mate" → "You're done for the night" → firm escort).
Right escalation level for their stage = clean ejection (+vibe, defeated-slouch
walk you escort to the exit); too aggressive on a merely `loose` patron = scene
(vibe, +aggro); too soft on a `maggot` = they stagger free, try again. Emit
`floor:infractionSpotted` / `floor:ejection` per contract. Leaving a `maggot`
unhandled for >N clock-min queues a deferred heat risk (v0.1: log to
`NightState.incidents`; inspector wiring is Phase 3).
### 4. Infraction: Toilet stalls (the rhythm game)
Stall doors show feet beneath: TWO pairs under one door = infraction. Interact →
close-up view of the door + a beat bar fed by **`beat:tick` events only** (do not
infer beat from elapsed time; measure hit accuracy vs the last tick's
`audioTimeMs`). Bang (SPACE) on-beat 3× consecutively: door flies open, two
patrons scatter in opposite directions with comic speed (+vibe, big). Off-beat
bangs: muffled "piss off mate" from inside, reset combo, small aggro. TEST: hit
windows off recorded tick timestamps (pure function, unit test —
`judgeBang(bangTimeMs, lastTick): 'perfect'|'ok'|'miss'`).
### 5. Infraction: Pat-down (contraband)
Suspicious idle (pocket-pat animation, driven by `flags.contraband`). Interact →
pat-down overlay: patron outline with 56 pocket zones; timed 10s; click zones to
reveal items, drag each to the **AMNESTY BIN** on the right. Items from
`data/contraband.ts` with silly sprites (placeholder: labelled coloured rects —
the budgie flaps). Missed items walk with them (deferred vibe leak). Contraband
found = +vibe, patron stays (confiscation, not ejection) unless it's severity 3.
### 6. Escort-out (shared mechanic)
Ejections attach the patron to walk-in-front-of-you pathing to the exit; they play
the defeated slouch. While escorting you can't interact — travel time is the cost
of every ejection (this is the floor's core resource: your position).
## Constraints
- Input: `Patron[]` + EventBus + clock. No imports from `scenes/door/` or `audio/`.
- All scoring via `meters:delta`; incidents appended via NightState API from core.
- Keep every judgement rule (bang windows, escalation matrix, pat-down scoring) in
pure functions with tests — scene code stays thin.
- Performance: 40+ patrons with Light2D at 60fps; profile before optimizing, but
silhouette-tint rendering outside the cone should be cheap (no per-frame texture work).
## Definition of done
Demo scene: all three minigames + UV check playable and readable in the dark
aesthetic; `lint`/`build`/`test` clean; pure-logic tests for bang timing,
escalation matrix, pat-down scoring; SESSION block appended to LANEHANDOVER.md;
branch pushed.

89
lanes/LANE_JUICE.md Normal file
View File

@ -0,0 +1,89 @@
# LANE-JUICE — Audio Engine & UI Widgets (Phase 1, parallel)
**Executor:** Opus 4.8. **Branch:** `lane/juice` (from main after Phase 0 merge).
**Owns:** `src/audio/`, `src/ui/` + tests. **Prereq reading:** `docs/GAME_DESIGN.md`
§5 → `docs/CONTRACTS.md` §4 (beat + audio events) → `LANEHANDOVER.md`.
You build the two things every other lane will lean on: the sound of the game and
the reusable UI widgets. Everything must work in your own demo scenes with zero
imports from door/floor code.
## Deliverable
`JuiceDemoScene` (menu key J): buttons that exercise every widget and every SFX,
plus the techno engine playing with a big on-screen low-pass cutoff slider and a
DOOR/FLOOR toggle. Beat indicator flashes on `beat:tick` and visibly matches the
audio.
## Part A — Audio (`src/audio/`) — raw WebAudio, no libraries
### 1. `TechnoEngine.ts` — the looping track
Fully synthesized, no samples: kick (sine drop 150→50Hz, ~8ms attack) four-on-the-
floor at 126132 BPM (config), off-beat hi-hat (filtered noise burst), a 2-bar
16th-note bassline (detuned saw through its own low-pass), an 8-bar pad that fades
in/out on a slow cycle. Schedule with the **look-ahead pattern** (timer checks
every 25ms, schedules nodes ~100ms ahead on `AudioContext.currentTime`) — never
schedule audio from rAF or Phaser update.
- **Beat authority:** emit `beat:tick {beatIndex, audioTimeMs}` on the EventBus AT
SCHEDULING TIME with the scheduled audio timestamp (consumers compare against
it — this is what makes the toilet rhythm game honest). On integration your
engine replaces `core/StubBeatClock` — same event, so consumers don't change.
- **The location filter (the signature):** master bus → `BiquadFilterNode`
(lowpass). `audio:location: 'door'` → cutoff ~250Hz + slight volume dip +
a touch of muffled reverb-ish feel (a short convolver is optional; the filter
alone is 90% of it). `'floor'` → sweep open to ~18kHz over ~600ms. That sweep IS
the "walking through the door" moment — make the ramp feel like a door opening,
exponential not linear.
- AudioContext unlock on first user gesture (browser autoplay policy); engine
boots silent until then, beat events start with audio, not before.
### 2. `Sfx.ts` — synthesized one-shots
All procedural (noise bursts, filtered clicks, pitch envelopes). Required set,
each with a named function:
`stampSlam` (heavy thunk + paper snap) · `stampInk` (squelch) · `clickerClunk`
(mechanical double-click) · `doorBang` (for the toilet game — must cut through the
muffled mix) · `radioStatic` + `radioChirp` · `phoneBuzz` · `ropeUnhook` (chain
jingle) · `denyCrowdOoh` (small crowd murmur from filtered noise) · `rainLoop`
(ambient, door scene). Route SFX AROUND the location filter (UI sounds are always
crisp; only the music is muffled).
## Part B — UI widgets (`src/ui/`) — reusable Phaser containers
Each widget: self-contained container class, configurable position, emits/consumes
bus events, demo'd in JuiceDemoScene. Style: chunky pixel UI, high contrast,
slightly grubby (this club is not clean).
1. **`Stamp.ts`** — THE interaction. `stamp(scene, {text: 'DENIED', onSlam})`:
raised stamp follows a short arc, SLAMS with 4px screen shake, ink ring +
splatter decal that stays on the patron, dust particles, `stampSlam` +
`stampInk` sfx, tiny hang before release. Tune until it feels like closing a
argument. Also a green `ADMITTED`-capable variant (config text/colour).
2. **`Phone.ts`** — bottom-corner phone: buzz animation + `phoneBuzz` on
`dazza:text`, message preview toast, click → slide-up thread history with
typing-dots for incoming. Also the "look at phone" idle interaction (emits
`door:phoneTheatre` — add to EventMap as a juice-domain event).
3. **`DialogueBox.ts`** — bottom-third dialogue with typed-out text (per-char
tick sfx), speaker tag, 24 choice buttons, drunk-typo render mode (letters
wobble/swap for drunk speakers). Consumed by both door tests and floor cut-offs.
4. **`Clicker.ts`** — the capacity tally: chunky physical counter, big IN button,
small OUT button, mechanical digit-roll animation, `clickerClunk`. Emits
`door:clicker`.
5. **`MeterHud.ts`** — Vibe/Aggro/Hype bars listening to `meters:changed`: vibe as
a neon sign that flickers when low, aggro as a crowd-temperature strip that
pulses when high, heat as 3 licence-stamp slots. Replaces core's HudStub at
integration.
## Constraints
- No imports from `scenes/door/` or `scenes/floor/`. Widgets learn everything from
constructor config + bus events.
- New event names go in the EventMap via CONTRACT CHANGE REQUEST if outside the
audio domain (`beat:*`, `audio:*` are yours to extend freely).
- Pure-logic tests where they exist (beat scheduling math: given BPM + lookahead,
assert scheduled beat times; typo-renderer determinism). Audio nodes themselves
need only the demo scene — note in handover what was verified by ear.
## Definition of done
JuiceDemoScene exercises everything; beat flash visibly locks to audio; the
door→floor filter sweep feels right (describe your tuning in the handover);
`lint`/`build`/`test` clean; SESSION block appended to LANEHANDOVER.md; branch pushed.