157 lines
6.3 KiB
Markdown
157 lines
6.3 KiB
Markdown
# 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;
|
|
}
|
|
```
|
|
|
|
**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/`).
|