// NOT TONIGHT — shared contract types. Source of truth: docs/CONTRACTS.md. // Post-Phase-0 changes require a CONTRACT CHANGE REQUEST in LANEHANDOVER.md. export type Archetype = | 'fresh18' | 'almost18' | 'financeBro' | 'influencer' | 'regular' | 'ownersMate' | 'quietMessy' | 'inspector' | 'punter'; export type DrunkStage = 'sober' | 'tipsy' | 'loose' | 'messy' | 'maggot'; export type OutfitSlot = 'shoes' | 'legs' | 'top' | 'outer' | 'hair' | 'accessory'; export interface OutfitLayer { slot: OutfitSlot; type: string; colour: string; logo?: boolean; vintage?: boolean; } export interface IdFakeTells { peelingLaminate?: boolean; wrongHologram?: boolean; jokeName?: boolean; photoMismatch?: boolean; } export interface IdCard { name: string; dob: string; // ISO date expiry: string; // ISO date photoSeed: number; fake?: IdFakeTells; } export interface PatronFlags { contraband?: string[]; onGuestList?: boolean; claimsGuestList?: boolean; knowsOwner?: boolean; uvStamped?: boolean; timesDeniedBefore?: number; disguised?: boolean; /** Set on couples by door/QueueManager; predicate for noHandHolders (CCR-1). */ handHolding?: boolean; /** Stable cast identity for run-scoped regulars (cross-night memory, §4.1). */ regularId?: string; } export interface Patron { id: string; dollSeed: number; archetype: Archetype; age: number; idCard: IdCard; outfit: OutfitLayer[]; intoxication: number; // 0..1 tolerance: number; // 0..1 flags: PatronFlags; } // ---- rules ---- export interface DressCodeRule { id: string; text: string; /** Card label for the on-screen dress-code board (CCR-4). */ short?: string; activeFrom: number; // clock minutes since 21:00 violates: (p: Patron) => boolean; } export type Verdict = 'admit' | 'deny' | 'sobrietyTest' | 'patDown' | 'wait'; export interface HeatStrike { reason: string; deferred?: boolean; } export interface VerdictOutcome { vibeDelta: number; aggroDelta: number; /** Queue-theatre payoff channel — the 'wait' verdict's reward (CCR-2). */ hypeDelta?: number; heatStrike?: HeatStrike; dazzaText?: string; } // ---- night / run state ---- export interface IncidentRecord { clockMin: number; kind: string; patronId?: string; detail: string; } export interface NightState { venueId: string; nightIndex: number; vibe: number; // 0..100 aggro: number; // 0..100 heatStrikes: HeatStrike[]; hype: number; capacity: { inside: number; licensed: number; clickerShown: number }; clockMin: number; // minutes since 21:00; night ends at 360 location: 'door' | 'floor'; incidents: IncidentRecord[]; } export interface RegularMemory { patronId: string; timesDenied: number; lastSeenNight: number; grudge: number; // 0..1 } export interface GameState { v: 1; seed: number; venueIndex: number; nightIndex: number; heatStrikes: HeatStrike[]; regulars: Record; pastReports: IncidentRecord[][]; /** Scripted-encounter ids the run has already met (repeat gating). */ seenEncounters?: string[]; } // ---- event bus ---- export interface EventMap { 'clock:tick': { clockMin: number }; 'night:phaseChange': { location: 'door' | 'floor' }; 'radio:call': { urgency: 1 | 2 | 3; line: string }; 'door:patronUp': { patron: Patron }; 'door:verdict': { patron: Patron; verdict: Verdict; outcome: VerdictOutcome }; 'door:clicker': { direction: 'in' | 'out' }; 'dazza:text': { text: string; rule?: DressCodeRule }; 'floor:infractionSpotted': { patronId: string; kind: 'drunk' | 'stall' | 'contraband' | 'noStamp' | 'fight' }; 'floor:ejection': { patronId: string; style: 'clean' | 'messy' }; /** A patron went home on their own — capacity frees up (natural churn). */ 'floor:leave': { patronId: string }; 'beat:tick': { beatIndex: number; audioTimeMs: number }; 'audio:location': { location: 'door' | 'floor' }; 'meters:delta': { vibe?: number; aggro?: number; hype?: number }; 'meters:changed': { vibe: number; aggro: number; hype: number }; 'heat:strike': HeatStrike; // ---- folded in at Phase-1 review (see LANEHANDOVER.md REVIEW 2026-07-19) ---- /** Single-writer incident trail: core/meters.ts appends to NightState.incidents. */ 'incident:log': IncidentRecord; /** Player fiddled with their phone while a patron waits — pure theatre. */ 'door:phoneTheatre': { durationMs: number }; /** AudioContext resumed after the first real user gesture. */ 'audio:unlocked': { atMs: number }; /** Bar line boundary (every 4th beat) — for anything that wants downbeats. */ 'beat:bar': { barIndex: number; beatIndex: number; audioTimeMs: number }; }