163 lines
4.6 KiB
TypeScript
163 lines
4.6 KiB
TypeScript
// 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<string, RegularMemory>;
|
|
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 };
|
|
}
|