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