/** * ============================================================================ * BASE ENDING SCENE - Foundation for result/ending screens * ============================================================================ * * Provides lifecycle for displaying game endings, victory/defeat screens, * score summaries, and narrative conclusions. * * LIFECYCLE: * create() calls in order: * 1. createBackground() -- HOOK: set background * 2. createEndingContent() -- HOOK: title, text, images * 3. showResults() -- HOOK: score, stats display * 4. playEndingMusic() -- uses getEndingMusicKey() hook * 5. setupContinue() -- sets up continue input * * HOOK METHODS: * - getEndingData(): Return ending configuration (title, text, bg) * - createBackground(): Custom background * - createEndingContent(): Main ending display (text, images, animations) * - showResults(): Display scores, stats, achievements * - getEndingMusicKey(): Return audio key for ending music * - onContinue(): Called when player presses continue * - onUpdate(): Per-frame logic * * Usage: * export class VictoryScene extends BaseEndingScene { * constructor() { super({ key: 'VictoryScene' }); } * * protected getEndingData(): EndingData { * return { title: 'Victory!', text: 'You won the duel!', type: 'victory' }; * } * } * * SAFETY NOTES: * - onContinue() default goes to 'TitleScreen' -- ensure this matches main.ts * - All type imports MUST use the "type" keyword: * import { type EndingData } from './BaseEndingScene'; * - Use this.events.once('shutdown', cb) for cleanup, NOT override shutdown() */ import Phaser from 'phaser'; // ============================================================================ // TYPES & INTERFACES // ============================================================================ export type EndingType = 'victory' | 'defeat' | 'neutral' | 'secret' | 'custom'; export interface EndingData { /** Ending title text */ title: string; /** Ending body text or description */ text?: string; /** Ending type for styling */ type: EndingType; /** Background image key */ backgroundKey?: string; /** Background music key */ musicKey?: string; /** Stats to display */ stats?: Record; } // ============================================================================ // BASE CLASS // ============================================================================ export abstract class BaseEndingScene extends Phaser.Scene { protected endingData?: EndingData; protected backgroundMusic?: Phaser.Sound.BaseSound; // ============================================================================ // LIFECYCLE // ============================================================================ create(): void { // Reset mutable state (Phaser reuses scene instances on scene.start, // so constructor field initializers do NOT run again) if (this.backgroundMusic) { this.backgroundMusic.stop(); } this.endingData = undefined; this.backgroundMusic = undefined; this.endingData = this.getEndingData(); this.createBackground(); this.createEndingContent(); this.showResults(); this.playEndingMusic(); this.setupContinue(); } update(time: number, delta: number): void { this.onUpdate(time, delta); } // ============================================================================ // HOOKS // ============================================================================ /** * HOOK (REQUIRED): Return the ending configuration. * This determines the title, text, type, and styling of the ending. */ protected abstract getEndingData(): EndingData; /** HOOK: Create the background. */ protected createBackground(): void {} /** * HOOK: Create the main ending content (title, text, images, animations). * Called after getEndingData() so this.endingData is available. */ protected createEndingContent(): void {} /** * HOOK: Display score summary, statistics, achievements. * Use GameDataManager to retrieve persistent data. */ protected showResults(): void {} /** HOOK: Return audio key for ending music. */ protected getEndingMusicKey(): string | undefined { return this.endingData?.musicKey; } /** * HOOK: Called when the player presses continue/restart. * Default: return to TitleScreen. */ protected onContinue(): void { this.scene.start('TitleScreen'); } /** HOOK: Per-frame logic. */ protected onUpdate(time: number, delta: number): void {} // ============================================================================ // UTILITIES // ============================================================================ /** Set up continue/restart input (Enter, Space, Click). */ protected setupContinue(): void { const enterKey = this.input.keyboard?.addKey( Phaser.Input.Keyboard.KeyCodes.ENTER, ); const spaceKey = this.input.keyboard?.addKey( Phaser.Input.Keyboard.KeyCodes.SPACE, ); const handleContinue = () => { // Remove listeners to prevent double-fire enterKey?.off('down', handleContinue); spaceKey?.off('down', handleContinue); this.input.off('pointerdown', handleContinue); if (this.backgroundMusic) { this.backgroundMusic.stop(); } this.onContinue(); }; enterKey?.on('down', handleContinue); spaceKey?.on('down', handleContinue); this.input.on('pointerdown', handleContinue); } /** Play ending music with fade-in. */ private playEndingMusic(): void { const key = this.getEndingMusicKey(); if (!key) return; try { if (this.sound.get(key)) { this.backgroundMusic = this.sound.get(key); } else { this.backgroundMusic = this.sound.add(key, { loop: true, volume: 0 }); } this.backgroundMusic?.play(); this.tweens.add({ targets: this.backgroundMusic, volume: 0.5, duration: 1000, }); } catch (e) { console.warn(`[BaseEndingScene] Could not play music: ${key}`, e); } } }