mafia / frontend /src /scenes /BaseEndingScene.ts
Alfaxad's picture
Migrate Mafia game to ZeroGPU Gradio Space
c670567 verified
Raw
History Blame Contribute Delete
6.24 kB
/**
* ============================================================================
* 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<string, string | number>;
}
// ============================================================================
// 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);
}
}
}