Files
2026-05-06 08:17:32 +08:00

74 lines
2.1 KiB
TypeScript

/**
* 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<string> = 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 };
}
}