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)); /** * Vibe per clock-minute the room loses on its own (emit from the night's * minute tick). Nobody stays impressed: above the baseline the room cools, so * a lit room is a job you keep doing, not a ratchet you finish. Never positive * — a dead floor does not heal itself (tuning pass 2026-07-19; before this, * any net-positive night pinned vibe at 100 by 10 PM and the meter went dead). */ export function vibeCoolingPerMin(vibe: number): number { if (vibe > 70) return -0.8; if (vibe > 50) return -0.4; return 0; } // 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(); } }