not-tonight/src/core/meters.ts
2026-07-20 05:35:47 +10:00

84 lines
2.7 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));
/**
* 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();
}
}