not-tonight/src/core/meters.ts
type-two 595371f684 Phase 1 review: merge door/floor/juice, fold CCRs into contracts, integration fixes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:26:35 +10:00

71 lines
2.2 KiB
TypeScript

import type { EventBus } from './EventBus';
import type { HeatStrike, NightState } from '../data/types';
export function freshNightState(venueId: string, nightIndex: number, licensed: number): NightState {
return {
venueId,
nightIndex,
vibe: 50,
aggro: 0,
heatStrikes: [],
hype: 1,
capacity: { inside: 0, licensed, clickerShown: 0 },
clockMin: 0,
location: 'door',
incidents: [],
};
}
const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
// Single writer for the night's numbers. Scenes emit meters:delta / heat:strike;
// nothing else may mutate NightState meters directly.
export class Meters {
private readonly offs: Array<() => void> = [];
constructor(
private readonly bus: EventBus,
public readonly state: NightState,
) {
this.offs.push(
bus.on('meters:delta', (d) => this.applyDelta(d)),
bus.on('heat:strike', (s) => this.applyStrike(s)),
bus.on('clock:tick', ({ clockMin }) => {
this.state.clockMin = clockMin;
}),
bus.on('night:phaseChange', ({ location }) => {
this.state.location = location;
}),
bus.on('incident:log', (incident) => {
this.state.incidents.push(incident);
}),
bus.on('door:clicker', ({ direction }) => {
this.state.capacity.clickerShown = Math.max(
0,
this.state.capacity.clickerShown + (direction === 'in' ? 1 : -1),
);
}),
);
}
private applyDelta(d: { vibe?: number; aggro?: number; hype?: number }): void {
const s = this.state;
if (d.vibe !== undefined) {
// Hype multiplies the good news only — queue theatre never softens a hit.
const scaled = d.vibe > 0 ? d.vibe * s.hype : d.vibe;
s.vibe = clamp(s.vibe + scaled, 0, 100);
}
if (d.aggro !== undefined) s.aggro = clamp(s.aggro + d.aggro, 0, 100);
if (d.hype !== undefined) s.hype = clamp(s.hype + d.hype, 1, 3);
this.bus.emit('meters:changed', { vibe: s.vibe, aggro: s.aggro, hype: s.hype });
}
private applyStrike(strike: HeatStrike): void {
this.state.heatStrikes.push(strike);
}
destroy(): void {
for (const off of this.offs) off();
}
}