# 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`), 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; // folded in at the Phase-1 review (2026-07-19): 'incident:log': IncidentRecord; // single writer: core/meters.ts appends to NightState.incidents 'door:phoneTheatre': { durationMs: number }; 'audio:unlocked': { atMs: number }; 'beat:bar': { barIndex: number; beatIndex: number; audioTimeMs: number }; } ``` Phase-1 review also added: `PatronFlags.handHolding?` (CCR-1), `VerdictOutcome.hypeDelta?` (CCR-2), `DressCodeRule.short?` (CCR-4). `src/data/types.ts` remains the source of truth where this doc lags. **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/`).