import Phaser from 'phaser'; import { soundDirector } from '../audio/SoundDirector'; import { getBattleScenario } from '../data/battles'; import { storyBackgroundAssets } from '../data/storyAssets'; import { getCampaignState } from '../state/campaignState'; import { palette } from '../ui/palette'; export class EndingScene extends Phaser.Scene { private readonly backgroundKey = 'story-wuzhang-jiangwei-inheritance'; private ready = false; constructor() { super('EndingScene'); } create() { this.ready = false; this.createFallbackTexture(); this.ensureBackgroundLoaded(() => { this.ready = true; this.drawEnding(); }); } getDebugState() { const campaign = getCampaignState(); return { scene: this.scene.key, ready: this.ready, campaignStep: campaign.step, latestBattleId: campaign.latestBattleId ?? null, completedBattles: Object.keys(campaign.battleHistory).length, rosterCount: campaign.roster.length, endingTitle: '북벌의 끝과 남은 뜻', backgroundKey: this.backgroundKey, backgroundReady: this.textures.exists(this.backgroundKey) }; } private ensureBackgroundLoaded(onReady: () => void) { if (this.textures.exists(this.backgroundKey)) { onReady(); return; } const url = storyBackgroundAssets[this.backgroundKey]; if (!url) { onReady(); return; } this.load.once('complete', onReady); this.load.once('loaderror', () => { console.warn(`Failed to load ending background ${this.backgroundKey}`); }); this.load.image(this.backgroundKey, url); this.load.start(); } private createFallbackTexture() { if (this.textures.exists('ending-fallback')) { return; } const graphics = this.make.graphics({ x: 0, y: 0 }); graphics.fillStyle(0x080b10, 1); graphics.fillRect(0, 0, 1280, 720); graphics.fillStyle(0x152331, 1); graphics.fillRect(0, 0, 1280, 720); graphics.lineStyle(5, palette.gold, 0.42); graphics.beginPath(); graphics.moveTo(120, 570); graphics.lineTo(1160, 170); graphics.strokePath(); graphics.generateTexture('ending-fallback', 1280, 720); graphics.destroy(); } private drawEnding() { const { width, height } = this.scale; const campaign = getCampaignState(); const latestBattleTitle = campaign.latestBattleId ? getBattleScenario(campaign.latestBattleId).title : '기록 없음'; const textureKey = this.textures.exists(this.backgroundKey) ? this.backgroundKey : 'ending-fallback'; const background = this.add.image(width / 2, height / 2, textureKey); const source = this.textures.get(textureKey).getSourceImage(); const coverScale = Math.max(width / source.width, height / source.height); background.setScale(coverScale * 1.08); background.setPosition(width / 2 - 14, height / 2 + 2); this.tweens.add({ targets: background, x: width / 2 + 14, y: height / 2 - 6, scale: coverScale * 1.12, duration: 18000, ease: 'Sine.easeInOut', yoyo: true, repeat: -1 }); this.add.rectangle(0, 0, width, height, 0x04070b, 0.34).setOrigin(0); this.add.rectangle(0, 0, width, height, 0x05080c, 0.16).setOrigin(0); this.add.rectangle(0, height - 220, width, 220, 0x05070a, 0.64).setOrigin(0); const completedBattles = Object.keys(campaign.battleHistory).length; const titleY = Math.max(96, height * 0.18); this.add.text(width / 2, titleY, '북벌의 끝과 남은 뜻', { fontFamily: '"Malgun Gothic", "Noto Sans KR", serif', fontSize: '48px', color: '#f6e6bd', fontStyle: '700', align: 'center', shadow: { offsetX: 0, offsetY: 3, color: '#020408', blur: 8, fill: true } }).setOrigin(0.5); this.add.text(width / 2, titleY + 62, '촉한 군영 후일담 완료', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '22px', color: '#d8b15f', fontStyle: '700', align: 'center', shadow: { offsetX: 0, offsetY: 2, color: '#020408', blur: 6, fill: true } }).setOrigin(0.5); const summary = [ `완료 전투 ${completedBattles}개`, `최종 전장 ${latestBattleTitle}`, `합류 무장 ${campaign.roster.length}명`, `군영 공명 ${campaign.bonds.length}개` ]; const startX = width / 2 - 390; summary.forEach((line, index) => { this.add.text(startX + index * 260, height - 178, line, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '18px', color: '#f2e3bf', fontStyle: '700', shadow: { offsetX: 0, offsetY: 2, color: '#020408', blur: 5, fill: true } }).setOrigin(0.5); }); this.add.text(width / 2, height - 122, '도원에서 시작된 선택은 촉한의 깃발과 북벌의 장부를 지나, 다음 세대가 이어 받을 뜻으로 남았습니다.', { fontFamily: '"Malgun Gothic", "Noto Sans KR", serif', fontSize: '22px', color: '#e8dfca', align: 'center', wordWrap: { width: Math.min(920, width - 160), useAdvancedWrap: true }, lineSpacing: 8, shadow: { offsetX: 0, offsetY: 2, color: '#020408', blur: 6, fill: true } }).setOrigin(0.5); this.addButton(width / 2, height - 52, '타이틀로', () => { soundDirector.playSelect(); this.scene.start('TitleScene'); }); this.input.keyboard?.on('keydown-ENTER', () => this.scene.start('TitleScene')); this.input.keyboard?.on('keydown-SPACE', () => this.scene.start('TitleScene')); soundDirector.playMusic('oath-theme'); } private addButton(x: number, y: number, label: string, action: () => void) { const bg = this.add.rectangle(x, y, 160, 42, 0x1a2630, 0.96); bg.setStrokeStyle(1, palette.gold, 0.82); bg.setInteractive({ useHandCursor: true }); bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98)); bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96)); bg.on('pointerdown', action); const text = this.add.text(x, y, label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '17px', color: '#f2e3bf', fontStyle: '700' }); text.setOrigin(0.5); text.setInteractive({ useHandCursor: true }); text.on('pointerdown', action); } }