import { describe, expect, it, vi } from 'vitest'; // The functions under test are pure, but they live in modules that subclass // Phaser.Scene, and Phaser touches `window` at import time under the node // environment. Stubbing the module is the only way to reach them from a test. vi.mock('phaser', () => ({ default: { Scene: class {} } })); import { freshNightState } from '../src/core/meters'; import { DAZZA_SIGNOFF } from '../src/data/strings/door'; import { MAX_HEAT_STRIKES, shouldRecordStrike, type NightLog } from '../src/scenes/door/NightScene'; import { nightCash, signoffFor } from '../src/scenes/door/NightSummaryScene'; import type { NightState } from '../src/data/types'; const log = (over: Partial = {}): NightLog => ({ vibeHistory: [50], verdicts: [], admitted: 0, denied: 0, tested: 0, pattedDown: 0, hypePeak: 1, heatStrikes: [], endReason: 'clock', seed: 1, role: 'DOOR', wage: 180, floorCash: 0, ...over, }); const stateWith = (...reasons: string[]): NightState => { const s = freshNightState('voltage', 0, 80); for (const reason of reasons) s.heatStrikes.push({ reason }); return s; }; describe('nightCash', () => { it('pays a flat wage for an uneventful night', () => { expect(nightCash(log(), stateWith())).toBe(180); }); it('pays $2 per admit on top of the wage', () => { const clean = stateWith(); expect(nightCash(log({ admitted: 10 }), clean)).toBe(nightCash(log(), clean) + 20); expect(nightCash(log({ admitted: 40 }), clean)).toBeGreaterThan( nightCash(log({ admitted: 10 }), clean), ); }); it('pays a hype bonus that scales with peak hype, not with final hype', () => { const clean = stateWith(); const base = nightCash(log(), clean); expect(nightCash(log({ hypePeak: 2 }), clean)).toBe(base + 90); expect(nightCash(log({ hypePeak: 3 }), clean)).toBe(base + 180); }); it('deducts $60 for every heat strike', () => { const l = log({ admitted: 20, hypePeak: 2 }); const none = nightCash(l, stateWith()); expect(nightCash(l, stateWith('over capacity'))).toBe(none - 60); expect(nightCash(l, stateWith('over capacity', 'minor admitted'))).toBe(none - 120); }); it('floors at zero — a catastrophic night pays nothing, it does not invoice you', () => { const strikes = Array.from({ length: 20 }, (_, i) => `offence ${i}`); expect(nightCash(log(), stateWith(...strikes))).toBe(0); }); it('returns a whole number even when hype is fractional', () => { for (const hypePeak of [1, 1.3, 1.75, 2.4, 3]) { const cash = nightCash(log({ hypePeak, admitted: 7 }), stateWith()); expect(Number.isInteger(cash)).toBe(true); } }); }); describe('signoffFor', () => { it('gives the harshest line at two strikes no matter how good the room felt', () => { expect(signoffFor(100, 2)).toBe(DAZZA_SIGNOFF[0]); expect(signoffFor(100, 3)).toBe(DAZZA_SIGNOFF[0]); // one strike is survivable — a great room still earns praise expect(signoffFor(100, 1)).not.toBe(DAZZA_SIGNOFF[0]); }); it('never moves to an earlier line as vibe rises (no strikes)', () => { for (const strikes of [0, 1]) { let lowest = 0; for (let vibe = 0; vibe <= 100; vibe++) { const idx = DAZZA_SIGNOFF.indexOf(signoffFor(vibe, strikes)); expect(idx).toBeGreaterThanOrEqual(lowest); lowest = idx; } } }); it('actually varies with vibe rather than always saying the same thing', () => { expect(signoffFor(0, 0)).not.toBe(signoffFor(100, 0)); }); it('returns a real signoff line across the whole 0..100 band, endpoints included', () => { for (let vibe = 0; vibe <= 100; vibe++) { const line = signoffFor(vibe, 0); expect(line.length).toBeGreaterThan(0); expect(DAZZA_SIGNOFF).toContain(line); } }); }); describe('shouldRecordStrike', () => { it('matches CONTRACTS.md §3: max 3 strikes', () => { expect(MAX_HEAT_STRIKES).toBe(3); }); it('records a strike the night has not seen before', () => { expect(shouldRecordStrike(stateWith(), { reason: 'minor admitted' })).toBe(true); expect(shouldRecordStrike(stateWith('over capacity'), { reason: 'minor admitted' })).toBe(true); }); it('stops recording once the licence is already gone', () => { const full = stateWith('a', 'b', 'c'); expect(full.heatStrikes).toHaveLength(MAX_HEAT_STRIKES); expect(shouldRecordStrike(full, { reason: 'd' })).toBe(false); }); it('treats a persistent breach as one offence, not one per patron', () => { const s = stateWith('over capacity'); expect(shouldRecordStrike(s, { reason: 'over capacity' })).toBe(false); expect(shouldRecordStrike(s, { reason: 'over capacity', deferred: true })).toBe(false); }); });