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:
commit
5086a325d8
35
CLAUDE.md
Normal file
35
CLAUDE.md
Normal 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
48
LANEHANDOVER.md
Normal 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
23
README.md
Normal 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
104
docs/BUILD_PLAN.md
Normal 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 2–4, 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
156
docs/CONTRACTS.md
Normal 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
186
docs/GAME_DESIGN.md
Normal 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** | 0–100 | 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) | 0–100 | Making people wait, arbitrary denials, rain | Letting people in, smooth queue flow | Aggro 100 = queue riot / someone glasses the door (fail night) |
|
||||
| **HEAT** (legal) | 0–3 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 ~12–15 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
98
lanes/LANE0_SCAFFOLD.md
Normal 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. ~8–12 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 1–2 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
102
lanes/LANE_DOOR.md
Normal 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:30–12: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" ~2–5 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
85
lanes/LANE_FLOOR.md
Normal 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 5–6 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
89
lanes/LANE_JUICE.md
Normal 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 126–132 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, 2–4 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.
|
||||
Loading…
Reference in New Issue
Block a user