# 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;
  // 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/`).
