701 lines
22 KiB
TypeScript
701 lines
22 KiB
TypeScript
import Phaser from 'phaser';
|
|
import { soundDirector } from '../audio/SoundDirector';
|
|
import { getBattleScenario } from '../data/battles';
|
|
import { battleIdForCampaignStep, isCampCampaignStep } from '../state/campaignRouting';
|
|
import {
|
|
hasCampaignSave,
|
|
listCampaignSaveSlots,
|
|
loadCampaignState,
|
|
startNewCampaign,
|
|
type CampaignSaveSlotSummary
|
|
} from '../state/campaignState';
|
|
import { palette } from '../ui/palette';
|
|
import { startLazyScene } from './lazyScenes';
|
|
|
|
export class TitleScene extends Phaser.Scene {
|
|
private focusedButton?: Phaser.GameObjects.Text;
|
|
private settingsPanel?: Phaser.GameObjects.Container;
|
|
private newGameConfirmPanel?: Phaser.GameObjects.Container;
|
|
private saveSlotPanel?: Phaser.GameObjects.Container;
|
|
private navigating = false;
|
|
|
|
constructor() {
|
|
super('TitleScene');
|
|
}
|
|
|
|
create() {
|
|
this.focusedButton = undefined;
|
|
this.settingsPanel = undefined;
|
|
this.newGameConfirmPanel = undefined;
|
|
this.saveSlotPanel = undefined;
|
|
this.navigating = false;
|
|
|
|
const debugBattleId = this.debugBattleId();
|
|
if (debugBattleId) {
|
|
this.time.delayedCall(0, () => {
|
|
void this.navigateTo('BattleScene', { battleId: debugBattleId });
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (this.shouldOpenDebugSortiePrep()) {
|
|
this.time.delayedCall(0, () => {
|
|
void this.navigateTo('CampScene', {
|
|
openSortiePrep: true,
|
|
skipIntroStory: true
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
const { width, height } = this.scale;
|
|
this.drawBackground(width, height);
|
|
this.drawAtmosphere(width, height);
|
|
this.drawTitleMark(width, height);
|
|
this.drawMenu(width, height);
|
|
|
|
soundDirector.playMusic('title-theme');
|
|
const unlockAudio = () => {
|
|
soundDirector.start();
|
|
soundDirector.resume();
|
|
soundDirector.playMusic('title-theme');
|
|
};
|
|
|
|
this.input.once('pointerdown', unlockAudio);
|
|
this.input.keyboard?.once('keydown', unlockAudio);
|
|
this.input.keyboard?.once('keydown-ENTER', () => this.activateDefaultMenuAction());
|
|
}
|
|
|
|
private shouldOpenDebugSortiePrep() {
|
|
if (typeof window === 'undefined') {
|
|
return false;
|
|
}
|
|
const params = new URLSearchParams(window.location.search);
|
|
return params.has('debugSortiePrep') && (params.has('debug') || import.meta.env.DEV);
|
|
}
|
|
|
|
private debugBattleId() {
|
|
if (typeof window === 'undefined') {
|
|
return undefined;
|
|
}
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (!params.has('debugBattle') || (!params.has('debug') && !import.meta.env.DEV)) {
|
|
return undefined;
|
|
}
|
|
return params.get('debugBattle')?.trim() || undefined;
|
|
}
|
|
|
|
private drawBackground(width: number, height: number) {
|
|
const background = this.add.image(width / 2, height / 2, 'title-taoyuan');
|
|
const texture = this.textures.get('title-taoyuan').getSourceImage();
|
|
const coverScale = Math.max(width / texture.width, height / texture.height);
|
|
|
|
background.setScale(coverScale * 1.08);
|
|
background.setPosition(width / 2 - 18, height / 2 + 4);
|
|
this.tweens.add({
|
|
targets: background,
|
|
x: width / 2 + 18,
|
|
y: height / 2 - 6,
|
|
scale: coverScale * 1.12,
|
|
duration: 18000,
|
|
ease: 'Sine.easeInOut',
|
|
yoyo: true,
|
|
repeat: -1
|
|
});
|
|
|
|
this.add.rectangle(0, 0, width, height, 0x080a0e, 0.2).setOrigin(0);
|
|
this.add.image(0, 0, this.createShadeTexture(width, height)).setOrigin(0);
|
|
}
|
|
|
|
private createShadeTexture(width: number, height: number) {
|
|
const key = 'title-shade';
|
|
if (this.textures.exists(key)) {
|
|
this.textures.remove(key);
|
|
}
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const context = canvas.getContext('2d');
|
|
|
|
if (!context) {
|
|
return key;
|
|
}
|
|
|
|
const rightShade = context.createLinearGradient(width * 0.42, 0, width, 0);
|
|
rightShade.addColorStop(0, 'rgba(8, 10, 14, 0)');
|
|
rightShade.addColorStop(0.42, 'rgba(8, 10, 14, 0.2)');
|
|
rightShade.addColorStop(1, 'rgba(8, 10, 14, 0.66)');
|
|
context.fillStyle = rightShade;
|
|
context.fillRect(0, 0, width, height);
|
|
|
|
const bottomShade = context.createLinearGradient(0, height * 0.55, 0, height);
|
|
bottomShade.addColorStop(0, 'rgba(8, 10, 14, 0)');
|
|
bottomShade.addColorStop(1, 'rgba(8, 10, 14, 0.56)');
|
|
context.fillStyle = bottomShade;
|
|
context.fillRect(0, 0, width, height);
|
|
|
|
this.textures.addCanvas(key, canvas);
|
|
return key;
|
|
}
|
|
|
|
private drawAtmosphere(width: number, height: number) {
|
|
const mist = this.add.ellipse(width / 2, height * 0.78, width * 1.25, 190, 0xd8b15f, 0.04);
|
|
this.tweens.add({
|
|
targets: mist,
|
|
alpha: 0.11,
|
|
x: width / 2 + 22,
|
|
duration: 8000,
|
|
ease: 'Sine.easeInOut',
|
|
yoyo: true,
|
|
repeat: -1
|
|
});
|
|
|
|
for (let index = 0; index < 34; index += 1) {
|
|
const petal = this.add.image(
|
|
Phaser.Math.Between(0, width),
|
|
Phaser.Math.Between(-80, height),
|
|
'petal'
|
|
);
|
|
const scale = Phaser.Math.FloatBetween(0.35, 0.9);
|
|
petal.setScale(scale);
|
|
petal.setAlpha(Phaser.Math.FloatBetween(0.22, 0.62));
|
|
petal.setRotation(Phaser.Math.FloatBetween(-1.2, 1.2));
|
|
|
|
this.tweens.add({
|
|
targets: petal,
|
|
x: petal.x + Phaser.Math.Between(80, 220),
|
|
y: height + Phaser.Math.Between(40, 180),
|
|
rotation: petal.rotation + Phaser.Math.FloatBetween(2.4, 5.6),
|
|
duration: Phaser.Math.Between(12000, 24000),
|
|
delay: Phaser.Math.Between(0, 6000),
|
|
ease: 'Sine.easeInOut',
|
|
repeat: -1,
|
|
onRepeat: () => {
|
|
petal.setPosition(Phaser.Math.Between(-120, width), Phaser.Math.Between(-180, -40));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private drawTitleMark(width: number, height: number) {
|
|
const compact = width < 720;
|
|
const titleX = compact ? width / 2 : 92;
|
|
const title = this.add.text(titleX, height - (compact ? 190 : 204), '삼국지', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", serif',
|
|
fontSize: compact ? '50px' : '64px',
|
|
color: '#f1e3c2',
|
|
fontStyle: '700',
|
|
padding: { top: 8, bottom: 8 },
|
|
shadow: { offsetX: 0, offsetY: 4, color: '#080a0e', blur: 10, fill: true }
|
|
});
|
|
title.setOrigin(compact ? 0.5 : 0, 0);
|
|
|
|
const subtitle = this.add.text(compact ? width / 2 : 96, height - (compact ? 112 : 118), '세 형제의 맹세', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", serif',
|
|
fontSize: compact ? '26px' : '34px',
|
|
color: '#d8b15f',
|
|
fontStyle: '700',
|
|
padding: { top: 10, bottom: 8 },
|
|
shadow: { offsetX: 0, offsetY: 2, color: '#080a0e', blur: 6, fill: true }
|
|
});
|
|
subtitle.setOrigin(compact ? 0.5 : 0, 0);
|
|
|
|
const lineX = compact ? width / 2 - 110 : 98;
|
|
const lineY = height - (compact ? 76 : 70);
|
|
this.add.rectangle(lineX, lineY, compact ? 220 : 310, 2, palette.gold, 0.68).setOrigin(0, 0.5);
|
|
}
|
|
|
|
private drawMenu(width: number, height: number) {
|
|
const compact = width < 720;
|
|
const menuX = compact ? width / 2 : width - 318;
|
|
const menuY = height * (compact ? 0.36 : 0.43);
|
|
const canContinue = hasCampaignSave();
|
|
const campaign = canContinue ? loadCampaignState() : undefined;
|
|
const isEndingComplete = campaign?.step === 'ending-complete';
|
|
const plateHeight = campaign ? 348 : 252;
|
|
|
|
const plate = this.add.rectangle(menuX, menuY, 250, plateHeight, 0x111821, 0.66);
|
|
plate.setStrokeStyle(1, palette.gold, 0.45);
|
|
this.add.rectangle(menuX, menuY - plateHeight / 2, 186, 2, palette.gold, 0.7);
|
|
this.add.rectangle(menuX, menuY + plateHeight / 2, 186, 2, palette.gold, 0.36);
|
|
|
|
this.createMenuButton(menuX, menuY - 70, '새 게임', true, () => this.requestStartGame());
|
|
this.createMenuButton(menuX, menuY, isEndingComplete ? '엔딩 보기' : '이어하기', canContinue, () => this.requestContinueGame());
|
|
this.createMenuButton(menuX, menuY + 70, '설정', true, () => this.openSettingsPanel(menuX, menuY));
|
|
|
|
if (campaign) {
|
|
this.drawCampaignSaveBadge(menuX, menuY + 132, campaign, isEndingComplete);
|
|
}
|
|
}
|
|
|
|
private drawCampaignSaveBadge(
|
|
x: number,
|
|
y: number,
|
|
campaign: ReturnType<typeof loadCampaignState>,
|
|
isEndingComplete: boolean
|
|
) {
|
|
const badge = this.add.container(x, y);
|
|
const background = this.add.rectangle(0, 0, 216, 78, 0x21180f, 0.92);
|
|
background.setStrokeStyle(1, palette.gold, 0.52);
|
|
|
|
const title = this.add.text(0, -24, isEndingComplete ? '완결 저장' : '진행 저장', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '18px',
|
|
color: '#f6e6bd',
|
|
fontStyle: '700',
|
|
fixedWidth: 198,
|
|
align: 'center',
|
|
padding: { top: 3, bottom: 3 }
|
|
});
|
|
title.setOrigin(0.5);
|
|
|
|
const detail = this.add.text(0, 0, this.campaignSaveDetail(campaign, isEndingComplete), {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '14px',
|
|
color: '#d8b15f',
|
|
fixedWidth: 198,
|
|
align: 'center'
|
|
});
|
|
detail.setOrigin(0.5);
|
|
|
|
const meta = this.add.text(0, 24, this.campaignSaveMeta(campaign), {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '12px',
|
|
color: '#9fb0bf',
|
|
fixedWidth: 198,
|
|
align: 'center'
|
|
});
|
|
meta.setOrigin(0.5);
|
|
|
|
badge.add([background, title, detail, meta]);
|
|
}
|
|
|
|
private campaignSaveDetail(campaign: ReturnType<typeof loadCampaignState>, isEndingComplete: boolean) {
|
|
if (isEndingComplete) {
|
|
return `${Object.keys(campaign.battleHistory).length}전 완수 · ${campaign.roster.length}명 합류`;
|
|
}
|
|
|
|
const nextBattleId = battleIdForCampaignStep(campaign.step);
|
|
if (nextBattleId) {
|
|
return this.compactSaveDetail(`다음 전장 · ${getBattleScenario(nextBattleId).title}`);
|
|
}
|
|
|
|
if (isCampCampaignStep(campaign.step)) {
|
|
const latestTitle = campaign.latestBattleId ? getBattleScenario(campaign.latestBattleId).title : '전투 이후';
|
|
return this.compactSaveDetail(`군영 정비 · ${latestTitle}`);
|
|
}
|
|
|
|
if (campaign.step === 'prologue') {
|
|
return '도원 결의 · 첫 출진 전';
|
|
}
|
|
|
|
if (campaign.step === 'first-victory-story') {
|
|
return '승리 후일담 · 군영 복귀';
|
|
}
|
|
|
|
return `군자금 ${campaign.gold} · ${campaign.roster.length}명`;
|
|
}
|
|
|
|
private compactSaveDetail(label: string) {
|
|
return label.length > 19 ? `${label.slice(0, 18)}...` : label;
|
|
}
|
|
|
|
private campaignSaveMeta(campaign: ReturnType<typeof loadCampaignState>) {
|
|
return `슬롯 ${campaign.activeSaveSlot} · ${this.formatSaveUpdatedAt(campaign.updatedAt)}`;
|
|
}
|
|
|
|
private formatSaveUpdatedAt(updatedAt: string) {
|
|
const date = new Date(updatedAt);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return '저장 시각 미상';
|
|
}
|
|
|
|
const month = date.getMonth() + 1;
|
|
const day = date.getDate();
|
|
const hour = String(date.getHours()).padStart(2, '0');
|
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
return `${month}/${day} ${hour}:${minute}`;
|
|
}
|
|
|
|
private createMenuButton(
|
|
x: number,
|
|
y: number,
|
|
label: string,
|
|
enabled: boolean,
|
|
onSelect: () => void
|
|
) {
|
|
const text = this.add.text(x, y, label, {
|
|
fontSize: '30px',
|
|
color: enabled ? '#f1e3c2' : '#7d8189',
|
|
fontStyle: '700',
|
|
fixedWidth: 190,
|
|
align: 'center',
|
|
padding: { top: 10, bottom: 10 }
|
|
});
|
|
text.setOrigin(0.5);
|
|
text.setShadow(0, 3, '#080a0e', 8, true, true);
|
|
|
|
if (!enabled) {
|
|
return text;
|
|
}
|
|
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerover', () => {
|
|
this.focusButton(text);
|
|
soundDirector.playHover();
|
|
});
|
|
text.on('pointerout', () => this.unfocusButton(text));
|
|
text.on('pointerdown', () => {
|
|
soundDirector.start();
|
|
soundDirector.resume();
|
|
onSelect();
|
|
});
|
|
|
|
return text;
|
|
}
|
|
|
|
private focusButton(text: Phaser.GameObjects.Text) {
|
|
this.focusedButton = text;
|
|
text.setColor('#111821');
|
|
text.setBackgroundColor('#d8b15f');
|
|
}
|
|
|
|
private unfocusButton(text: Phaser.GameObjects.Text) {
|
|
if (this.focusedButton === text) {
|
|
this.focusedButton = undefined;
|
|
}
|
|
text.setColor('#f1e3c2');
|
|
text.setBackgroundColor('rgba(0,0,0,0)');
|
|
}
|
|
|
|
private openSettingsPanel(x: number, y: number) {
|
|
if (this.settingsPanel) {
|
|
this.closeSettingsPanel();
|
|
return;
|
|
}
|
|
|
|
soundDirector.playSelect();
|
|
this.closeNewGameConfirmPanel();
|
|
this.closeSaveSlotPanel();
|
|
|
|
const panel = this.add.container(x, y + 238);
|
|
const backdrop = this.add.rectangle(0, 0, 286, 188, 0x0e151d, 0.9);
|
|
backdrop.setStrokeStyle(1, palette.gold, 0.52);
|
|
|
|
const title = this.add.text(0, -66, '설정', {
|
|
fontSize: '24px',
|
|
color: '#f1e3c2',
|
|
fontStyle: '700',
|
|
fixedWidth: 210,
|
|
align: 'center'
|
|
});
|
|
title.setOrigin(0.5);
|
|
|
|
const soundText = this.createSettingsButton(0, -10, this.soundLabel());
|
|
soundText.on('pointerdown', () => {
|
|
soundDirector.setMuted(!soundDirector.isMuted());
|
|
soundText.setText(this.soundLabel());
|
|
soundDirector.playSelect();
|
|
});
|
|
|
|
const close = this.createSettingsButton(0, 48, '닫기');
|
|
close.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.closeSettingsPanel();
|
|
});
|
|
|
|
panel.add([backdrop, title, soundText, close]);
|
|
panel.setAlpha(0);
|
|
this.settingsPanel = panel;
|
|
this.tweens.add({ targets: panel, alpha: 1, y: y + 228, duration: 160, ease: 'Sine.easeOut' });
|
|
}
|
|
|
|
private createSettingsButton(x: number, y: number, label: string) {
|
|
const text = this.add.text(x, y, label, {
|
|
fontSize: '20px',
|
|
color: '#d8b15f',
|
|
fixedWidth: 210,
|
|
align: 'center',
|
|
padding: { top: 8, bottom: 8 }
|
|
});
|
|
text.setOrigin(0.5);
|
|
text.setInteractive({ useHandCursor: true });
|
|
text.on('pointerover', () => text.setColor('#f1e3c2'));
|
|
text.on('pointerout', () => text.setColor('#d8b15f'));
|
|
|
|
return text;
|
|
}
|
|
|
|
private soundLabel() {
|
|
return `음향: ${soundDirector.isMuted() ? '꺼짐' : '켜짐'}`;
|
|
}
|
|
|
|
private closeSettingsPanel() {
|
|
this.settingsPanel?.destroy();
|
|
this.settingsPanel = undefined;
|
|
}
|
|
|
|
private requestContinueGame() {
|
|
if (!hasCampaignSave()) {
|
|
return;
|
|
}
|
|
|
|
const slots = listCampaignSaveSlots().filter((slot) => slot.exists);
|
|
if (slots.length <= 1) {
|
|
this.continueGame(slots[0]?.slot);
|
|
return;
|
|
}
|
|
|
|
const { width, height } = this.scale;
|
|
const compact = width < 720;
|
|
const menuX = compact ? width / 2 : width - 318;
|
|
const menuY = height * (compact ? 0.36 : 0.43);
|
|
this.openSaveSlotPanel(menuX, menuY);
|
|
}
|
|
|
|
private openSaveSlotPanel(x: number, y: number) {
|
|
if (this.saveSlotPanel) {
|
|
this.closeSaveSlotPanel();
|
|
return;
|
|
}
|
|
|
|
soundDirector.playSelect();
|
|
this.closeSettingsPanel();
|
|
this.closeNewGameConfirmPanel();
|
|
|
|
const panel = this.add.container(x, y + 252);
|
|
const backdrop = this.add.rectangle(0, 0, 318, 256, 0x0e151d, 0.95);
|
|
backdrop.setStrokeStyle(1, palette.gold, 0.58);
|
|
|
|
const title = this.add.text(0, -104, '이어갈 기록 선택', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '22px',
|
|
color: '#f1e3c2',
|
|
fontStyle: '700',
|
|
fixedWidth: 266,
|
|
align: 'center'
|
|
});
|
|
title.setOrigin(0.5);
|
|
|
|
const rows = listCampaignSaveSlots().map((slot, index) => this.createSaveSlotRow(slot, -74 + index * 58));
|
|
const close = this.createSettingsButton(0, 104, '닫기');
|
|
close.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.closeSaveSlotPanel();
|
|
});
|
|
|
|
panel.add([backdrop, title, ...rows, close]);
|
|
panel.setAlpha(0);
|
|
this.saveSlotPanel = panel;
|
|
this.tweens.add({ targets: panel, alpha: 1, y: y + 242, duration: 160, ease: 'Sine.easeOut' });
|
|
}
|
|
|
|
private createSaveSlotRow(slot: CampaignSaveSlotSummary, y: number) {
|
|
const row = this.add.container(0, y);
|
|
const enabled = slot.exists;
|
|
const background = this.add.rectangle(0, 0, 270, 48, enabled ? 0x1b2530 : 0x121820, enabled ? 0.9 : 0.56);
|
|
background.setStrokeStyle(1, enabled ? palette.gold : 0x56616d, enabled ? 0.42 : 0.2);
|
|
|
|
const campaign = enabled ? loadCampaignState(slot.slot) : undefined;
|
|
const isEndingComplete = campaign?.step === 'ending-complete';
|
|
const title = this.add.text(-122, -12, `슬롯 ${slot.slot}`, {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '16px',
|
|
color: enabled ? '#f6e6bd' : '#7d8189',
|
|
fontStyle: '700',
|
|
fixedWidth: 70,
|
|
align: 'left'
|
|
});
|
|
title.setOrigin(0, 0.5);
|
|
|
|
const detail = this.add.text(-42, -12, campaign ? this.campaignSaveDetail(campaign, isEndingComplete) : '저장 없음', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '13px',
|
|
color: enabled ? '#d8b15f' : '#747c86',
|
|
fixedWidth: 154,
|
|
align: 'left'
|
|
});
|
|
detail.setOrigin(0, 0.5);
|
|
|
|
const meta = this.add.text(-42, 12, campaign ? this.campaignSaveMeta(campaign) : '빈 슬롯', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '11px',
|
|
color: enabled ? '#9fb0bf' : '#626b75',
|
|
fixedWidth: 154,
|
|
align: 'left'
|
|
});
|
|
meta.setOrigin(0, 0.5);
|
|
|
|
row.add([background, title, detail, meta]);
|
|
|
|
if (enabled) {
|
|
background.setInteractive({ useHandCursor: true });
|
|
background.on('pointerover', () => {
|
|
background.setFillStyle(0xd8b15f, 0.22);
|
|
background.setStrokeStyle(1, palette.gold, 0.72);
|
|
soundDirector.playHover();
|
|
});
|
|
background.on('pointerout', () => {
|
|
background.setFillStyle(0x1b2530, 0.9);
|
|
background.setStrokeStyle(1, palette.gold, 0.42);
|
|
});
|
|
background.on('pointerdown', () => {
|
|
this.closeSaveSlotPanel();
|
|
this.continueGame(slot.slot);
|
|
});
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
private closeSaveSlotPanel() {
|
|
this.saveSlotPanel?.destroy();
|
|
this.saveSlotPanel = undefined;
|
|
}
|
|
|
|
private requestStartGame() {
|
|
if (!hasCampaignSave()) {
|
|
this.startGame();
|
|
return;
|
|
}
|
|
|
|
const { width, height } = this.scale;
|
|
const compact = width < 720;
|
|
const menuX = compact ? width / 2 : width - 318;
|
|
const menuY = height * (compact ? 0.36 : 0.43);
|
|
this.openNewGameConfirmPanel(menuX, menuY);
|
|
}
|
|
|
|
private openNewGameConfirmPanel(x: number, y: number) {
|
|
if (this.newGameConfirmPanel) {
|
|
this.closeNewGameConfirmPanel();
|
|
return;
|
|
}
|
|
|
|
soundDirector.playSelect();
|
|
this.closeSettingsPanel();
|
|
this.closeSaveSlotPanel();
|
|
|
|
const panel = this.add.container(x, y + 240);
|
|
const backdrop = this.add.rectangle(0, 0, 286, 174, 0x0e151d, 0.94);
|
|
backdrop.setStrokeStyle(1, palette.gold, 0.58);
|
|
|
|
const title = this.add.text(0, -58, '새로 시작', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '23px',
|
|
color: '#f1e3c2',
|
|
fontStyle: '700',
|
|
fixedWidth: 220,
|
|
align: 'center'
|
|
});
|
|
title.setOrigin(0.5);
|
|
|
|
const message = this.add.text(0, -20, '현재 저장을 덮고\n처음부터 시작합니다.', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
|
fontSize: '15px',
|
|
color: '#bfc8d3',
|
|
fixedWidth: 236,
|
|
align: 'center',
|
|
lineSpacing: 4
|
|
});
|
|
message.setOrigin(0.5);
|
|
|
|
const confirm = this.createSettingsButton(0, 30, '새 게임 시작');
|
|
confirm.setColor('#f1e3c2');
|
|
confirm.on('pointerdown', () => {
|
|
this.closeNewGameConfirmPanel();
|
|
this.startGame();
|
|
});
|
|
|
|
const cancel = this.createSettingsButton(0, 74, '취소');
|
|
cancel.on('pointerdown', () => {
|
|
soundDirector.playSelect();
|
|
this.closeNewGameConfirmPanel();
|
|
});
|
|
|
|
panel.add([backdrop, title, message, confirm, cancel]);
|
|
panel.setAlpha(0);
|
|
this.newGameConfirmPanel = panel;
|
|
this.tweens.add({ targets: panel, alpha: 1, y: y + 230, duration: 160, ease: 'Sine.easeOut' });
|
|
}
|
|
|
|
private closeNewGameConfirmPanel() {
|
|
this.newGameConfirmPanel?.destroy();
|
|
this.newGameConfirmPanel = undefined;
|
|
}
|
|
|
|
private activateDefaultMenuAction() {
|
|
if (hasCampaignSave()) {
|
|
this.continueGame();
|
|
return;
|
|
}
|
|
|
|
this.startGame();
|
|
}
|
|
|
|
private startGame() {
|
|
soundDirector.start();
|
|
soundDirector.resume();
|
|
soundDirector.playSelect();
|
|
startNewCampaign();
|
|
void this.navigateTo('StoryScene');
|
|
}
|
|
|
|
private continueGame(slot?: number) {
|
|
soundDirector.start();
|
|
soundDirector.resume();
|
|
soundDirector.playSelect();
|
|
void this.continueGameAsync(slot);
|
|
}
|
|
|
|
private async continueGameAsync(slot?: number) {
|
|
const campaign = loadCampaignState(slot);
|
|
if (campaign.step === 'ending-complete') {
|
|
await this.navigateTo('EndingScene');
|
|
return;
|
|
}
|
|
|
|
if (isCampCampaignStep(campaign.step)) {
|
|
await this.navigateTo('CampScene');
|
|
return;
|
|
}
|
|
|
|
const battleId = battleIdForCampaignStep(campaign.step);
|
|
if (battleId) {
|
|
if (campaign.step !== 'first-battle' && campaign.firstBattleReport?.outcome === 'victory') {
|
|
await this.navigateTo('CampScene', {
|
|
openSortiePrep: true,
|
|
skipIntroStory: true
|
|
});
|
|
return;
|
|
}
|
|
|
|
await this.navigateTo('BattleScene', { battleId });
|
|
return;
|
|
}
|
|
|
|
if (campaign.step === 'first-victory-story') {
|
|
const { firstBattleVictoryPages } = await import('../data/scenario');
|
|
await this.navigateTo('StoryScene', { pages: firstBattleVictoryPages, nextScene: 'TitleScene' });
|
|
return;
|
|
}
|
|
|
|
await this.navigateTo('StoryScene');
|
|
}
|
|
|
|
private async navigateTo(sceneKey: 'StoryScene' | 'BattleScene' | 'CampScene' | 'EndingScene', data?: Record<string, unknown>) {
|
|
if (this.navigating) {
|
|
return;
|
|
}
|
|
|
|
this.navigating = true;
|
|
try {
|
|
await startLazyScene(this, sceneKey, data);
|
|
} catch (error) {
|
|
console.error(`Failed to start ${sceneKey}`, error);
|
|
this.navigating = false;
|
|
}
|
|
}
|
|
}
|