not-tonight/src/data/types.ts
type-two f38ea0743c Cross-night regulars with disguises, venue crowd identities, retractions as texts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:43:40 +10:00

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 };
}