- 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>
73 lines
2.6 KiB
TypeScript
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);
|
|
});
|
|
});
|