not-tonight/tests/roleBeats.test.ts
type-two 9bc7dd1bfd Three jobs: sweep out of the god object, story beats per role, and dolls that breathe
- scenes/floor/sweep.ts: the closing sweep extracted behind an accessor host.
  Scene 2100 -> 1981 lines, behaviour verified identical.
- data/roleBeats.ts: 18 beats across 6 roles x 3 nights, landing in the venue
  group chat. The glassie is spoken about; the HoS is spoken to.
- dollPlan(p, pose, frame): 4-frame idle on the queue pose. NOT pre-rendered
  sheets — those would hide the outfit tells the dress code convicts on, which
  design 4.1 forbids; the pre-render path must go UNDER the outfit rects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 20:49:42 +10:00

73 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { ROLE_BEATS, beatsDue } from '../src/data/roleBeats';
import { ROLES } from '../src/data/roster';
// Thu/Fri/Sat. Not imported from NightScene: that module pulls in Phaser,
// which needs a `window` this suite does not have.
const TOTAL_NIGHTS = 3;
describe('role story beats (design §6)', () => {
it('every role on the board has a week of them', () => {
for (const r of ROLES) {
const beats = ROLE_BEATS[r.id];
expect(beats, `${r.id} has no beats`).toBeTruthy();
expect(beats.length).toBeGreaterThan(0);
}
});
it('one beat per night, and every night of the week is covered', () => {
for (const r of ROLES) {
const nights = ROLE_BEATS[r.id].map((b) => b.night).sort();
expect(nights, `${r.id} week`).toEqual([...Array(TOTAL_NIGHTS).keys()]);
}
});
it('they land mid-shift, not at the door-open or after last drinks', () => {
// A beat at minute 0 is noise; one at 3 AM never arrives.
for (const r of ROLES) {
for (const b of ROLE_BEATS[r.id]) {
expect(b.atMin, `${r.id} n${b.night}`).toBeGreaterThan(60);
expect(b.atMin, `${r.id} n${b.night}`).toBeLessThan(300);
}
}
});
it('fires each beat exactly once as the clock passes it', () => {
const seen: string[] = [];
let last = -1;
for (let m = 0; m <= 360; m++) {
for (const b of beatsDue('glassie', 0, m, last)) seen.push(b.text);
last = m;
}
expect(seen.length).toBe(1);
});
it('never fires another role\'s week, or another night of your own', () => {
const all = (role: 'glassie' | 'hos', night: number): number => {
let last = -1;
let n = 0;
for (let m = 0; m <= 360; m++) {
n += beatsDue(role, night, m, last).length;
last = m;
}
return n;
};
expect(all('glassie', 0)).toBe(1);
expect(all('hos', 0)).toBe(1);
expect(ROLE_BEATS.glassie[0]!.text).not.toBe(ROLE_BEATS.hos[0]!.text);
// A night index past the week is silent rather than throwing.
expect(all('glassie', 9)).toBe(0);
});
it('a clock that jumps several minutes does not swallow a beat', () => {
// GameClock can advance more than a minute in one tick under load.
const b = ROLE_BEATS.bartender.find((x) => x.night === 1)!;
expect(beatsDue('bartender', 1, b.atMin + 5, b.atMin - 5)).toHaveLength(1);
});
it('each role gets its OWN voice — no shared copy between shifts', () => {
const texts = ROLES.flatMap((r) => ROLE_BEATS[r.id].map((b) => b.text));
expect(new Set(texts).size).toBe(texts.length);
});
});