Files
KateLegend2_proj/tests/logic/BossSettlement.test.ts
T
2026-06-07 22:10:03 +08:00

127 lines
4.7 KiB
TypeScript

import { BossController } from '@logic/BossController';
import { BANNED_RESCUE_SEQUENCE, ChapterSettlement } from '@logic/ChapterSettlement';
import { IBossConfig } from '@data/Interfaces';
const bossCfg: IBossConfig = {
id: 'shuang_huan_fang',
displayName: '双幻坊',
hp: 3,
butterflyReveal: true,
princessCutsceneAtHpRatio: 0.5,
phases: [
{ hpThreshold: 1.0, mode: 'pair_pincer', actionIntervalSec: 2.2 },
{ hpThreshold: 2 / 3, mode: 'fireball_spread', actionIntervalSec: 1.8 },
{ hpThreshold: 1 / 3, mode: 'clone_confuse', actionIntervalSec: 1.4 },
],
};
describe('BossController — butterfly reveal (req 9.1-9.3)', () => {
it('body hits are ignored until butterfly is hit', () => {
const boss = new BossController(bossCfg);
expect(boss.onBodyHit()).toEqual([]);
expect(boss.currentHp).toBe(3);
});
it('butterfly hit emits reveal event once', () => {
const boss = new BossController(bossCfg);
expect(boss.onButterflyHit()).toEqual([{ kind: 'butterfly_revealed' }]);
expect(boss.onButterflyHit()).toEqual([]); // second hit is a no-op
expect(boss.isButterflyRevealed).toBe(true);
});
it('body hits after reveal decrement HP one at a time', () => {
const boss = new BossController(bossCfg);
boss.onButterflyHit();
boss.onBodyHit();
expect(boss.currentHp).toBe(2);
});
});
describe('BossController — phase transitions (req 9.4)', () => {
it('HP drop to 2/3 triggers phase_changed to fireball_spread', () => {
const boss = new BossController(bossCfg);
boss.onButterflyHit();
const events = boss.onBodyHit(); // 3 → 2 (ratio 0.66)
expect(events.some((e) => e.kind === 'phase_changed' && e.phase === 'fireball_spread')).toBe(true);
});
it('HP drop to 1/3 triggers clone_confuse', () => {
const boss = new BossController(bossCfg);
boss.onButterflyHit();
boss.onBodyHit();
const events = boss.onBodyHit(); // 2 → 1 (ratio 0.33)
expect(events.some((e) => e.kind === 'phase_changed' && e.phase === 'clone_confuse')).toBe(true);
});
});
describe('BossController — princess cutscene + death (req 8.6, 14.1)', () => {
it('emits princess_taken_cutscene when HP reaches 1/2', () => {
const boss = new BossController(bossCfg);
boss.onButterflyHit();
const events = boss.onBodyHit(); // 3 → 2 → ratio 0.66 (> 0.5, no cutscene yet)
expect(events.some((e) => e.kind === 'princess_taken_cutscene')).toBe(false);
const events2 = boss.onBodyHit(); // 2 → 1 → ratio 0.33 (< 0.5)
expect(events2.some((e) => e.kind === 'princess_taken_cutscene')).toBe(true);
});
it('emits boss_killed on final hit and marks isDead', () => {
const boss = new BossController(bossCfg);
boss.onButterflyHit();
boss.onBodyHit();
boss.onBodyHit();
const events = boss.onBodyHit();
expect(events.some((e) => e.kind === 'boss_killed')).toBe(true);
expect(boss.isDead).toBe(true);
});
it('further body hits after death are no-ops', () => {
const boss = new BossController(bossCfg);
boss.onButterflyHit();
boss.onBodyHit();
boss.onBodyHit();
boss.onBodyHit();
expect(boss.onBodyHit()).toEqual([]);
});
});
describe('ChapterSettlement — rope-cut ban (req 14.5)', () => {
it('assertCutsceneAllowed throws on every banned id', () => {
const cs = new ChapterSettlement({
totalScore: 0,
stageScore: 0,
comboCount: 0,
flawless: true,
remainingTimeSec: 0,
});
for (const bad of BANNED_RESCUE_SEQUENCE) {
expect(() => cs.assertCutsceneAllowed(bad)).toThrow(/banned/);
}
});
it('allows the legitimate cutscene ids', () => {
const cs = new ChapterSettlement({
totalScore: 0,
stageScore: 0,
comboCount: 0,
flawless: true,
remainingTimeSec: 0,
});
expect(() => cs.assertCutsceneAllowed('princess_taken')).not.toThrow();
expect(() => cs.assertCutsceneAllowed('boss_killed_freeze')).not.toThrow();
expect(() => cs.assertCutsceneAllowed('settlement_screen')).not.toThrow();
});
it('build() returns the "princess taken" closing line, not a rescue one', () => {
const cs = new ChapterSettlement({
totalScore: 1000,
stageScore: 500,
comboCount: 2,
flawless: true,
remainingTimeSec: 10,
});
const r = cs.build();
expect(r.closingLine).toMatch(/公主被带走/);
expect(r.closingLine).not.toMatch(/救/);
});
});