/** * Chapter settlement logic (task 8.2, req 14.1-14.5). * * After the boss dies the chapter-1 cutscene sequence is strictly: * * princess_taken_cutscene (≤ 3s) — optional, plays mid-fight when HP ≤ 1/2 * boss_killed (≤ 2s) — freeze-frame * settlement_screen — score + stats UI * * There is **no** rope-severing rescue event in the MVP (req 14.5), so we * expose a single `BANNED_RESCUE_SEQUENCE` constant the QA test asserts * against; any future code that tries to play it will fail the guardrail. */ export type CutsceneId = 'princess_taken' | 'boss_killed_freeze' | 'settlement_screen'; /** Any rescue-style cutscene ID in this set will fail CI (req 14.5). */ export const BANNED_RESCUE_SEQUENCE: ReadonlyArray = Object.freeze([ 'rope_cut_rescue', 'princess_rescued', 'chapter_end_rescue', ]); export interface ISettlementStats { totalScore: number; stageScore: number; comboCount: number; flawless: boolean; remainingTimeSec: number; } export interface ISettlementResult { stats: ISettlementStats; closingLine: string; } export class ChapterSettlement { private stats: ISettlementStats; constructor(initialStats: ISettlementStats) { this.stats = { ...initialStats }; } public addScore(pts: number): void { this.stats.stageScore += pts; this.stats.totalScore += pts; } public registerCombo(): void { this.stats.comboCount++; } public markTaken(): void { this.stats.flawless = false; } public setRemainingTime(sec: number): void { this.stats.remainingTimeSec = sec; } public assertCutsceneAllowed(id: CutsceneId | string): void { if (BANNED_RESCUE_SEQUENCE.includes(id)) { throw new Error( `ChapterSettlement: cutscene "${id}" is explicitly banned — chapter 1 must end with the princess taken (req 14.5)` ); } } public build(): ISettlementResult { const closing = '公主被带走,续章待续…'; return { stats: { ...this.stats }, closingLine: closing }; } }