Add campaign ending epilogue
This commit is contained in:
@@ -10954,6 +10954,11 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
this.addCommandButton('다음 이야기', 1160, 38, 142, () => {
|
||||
soundDirector.playSelect();
|
||||
const flow = this.currentSortieFlow();
|
||||
if (this.isFinalEpilogueFlow(flow)) {
|
||||
this.startVictoryStory();
|
||||
return;
|
||||
}
|
||||
this.showSortiePrep();
|
||||
});
|
||||
}
|
||||
@@ -11795,7 +11800,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private startVictoryStory() {
|
||||
const flow = this.currentSortieFlow();
|
||||
if (!this.ensureSortieSelectionSaved()) {
|
||||
if (!this.isFinalEpilogueFlow(flow) && !this.ensureSortieSelectionSaved()) {
|
||||
const availableIds = new Set(this.sortieAllies().map((unit) => unit.id));
|
||||
const requiredNames = [...this.requiredSortieUnitIdsFor()]
|
||||
.filter((unitId) => availableIds.has(unitId))
|
||||
@@ -11819,7 +11824,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.campaign = getCampaignState();
|
||||
this.scene.start('StoryScene', {
|
||||
pages: flow.pages,
|
||||
nextScene: 'CampScene'
|
||||
nextScene: flow.campaignStep === 'ending-complete' ? 'EndingScene' : 'CampScene'
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -11850,6 +11855,10 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
private isFinalEpilogueFlow(flow = this.currentSortieFlow()) {
|
||||
return flow.campaignStep === 'ending-complete' && !flow.nextBattleId;
|
||||
}
|
||||
|
||||
private hideSortiePrep() {
|
||||
this.sortieObjects.forEach((object) => object.destroy());
|
||||
this.sortieObjects = [];
|
||||
|
||||
178
src/game/scenes/EndingScene.ts
Normal file
178
src/game/scenes/EndingScene.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import Phaser from 'phaser';
|
||||
import { soundDirector } from '../audio/SoundDirector';
|
||||
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-weishui-northbank';
|
||||
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: '북벌의 끝과 남은 뜻'
|
||||
};
|
||||
}
|
||||
|
||||
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 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}개`,
|
||||
`최종 전장 ${campaign.latestBattleId ? '위수 북안 압박전' : '기록 없음'}`,
|
||||
`합류 무장 ${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);
|
||||
}
|
||||
}
|
||||
@@ -364,6 +364,11 @@ export class TitleScene extends Phaser.Scene {
|
||||
soundDirector.playSelect();
|
||||
|
||||
const campaign = loadCampaignState();
|
||||
if (campaign.step === 'ending-complete') {
|
||||
this.scene.start('EndingScene');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
campaign.step === 'first-camp' ||
|
||||
campaign.step === 'second-camp' ||
|
||||
|
||||
Reference in New Issue
Block a user