Add title save slot selection

This commit is contained in:
2026-07-05 02:48:04 +09:00
parent 9d2e3a0c6f
commit 2de9f15e7d

View File

@@ -2,7 +2,13 @@ import Phaser from 'phaser';
import { soundDirector } from '../audio/SoundDirector';
import { getBattleScenario } from '../data/battles';
import { battleIdForCampaignStep, isCampCampaignStep } from '../state/campaignRouting';
import { hasCampaignSave, loadCampaignState, startNewCampaign } from '../state/campaignState';
import {
hasCampaignSave,
listCampaignSaveSlots,
loadCampaignState,
startNewCampaign,
type CampaignSaveSlotSummary
} from '../state/campaignState';
import { palette } from '../ui/palette';
import { startLazyScene } from './lazyScenes';
@@ -10,6 +16,7 @@ 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() {
@@ -20,6 +27,7 @@ export class TitleScene extends Phaser.Scene {
this.focusedButton = undefined;
this.settingsPanel = undefined;
this.newGameConfirmPanel = undefined;
this.saveSlotPanel = undefined;
this.navigating = false;
const debugBattleId = this.debugBattleId();
@@ -213,7 +221,7 @@ export class TitleScene extends Phaser.Scene {
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.continueGame());
this.createMenuButton(menuX, menuY, isEndingComplete ? '엔딩 보기' : '이어하기', canContinue, () => this.requestContinueGame());
this.createMenuButton(menuX, menuY + 70, '설정', true, () => this.openSettingsPanel(menuX, menuY));
if (campaign) {
@@ -369,6 +377,7 @@ export class TitleScene extends Phaser.Scene {
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);
@@ -427,6 +436,124 @@ export class TitleScene extends Phaser.Scene {
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();
@@ -448,6 +575,7 @@ export class TitleScene extends Phaser.Scene {
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);
@@ -514,15 +642,15 @@ export class TitleScene extends Phaser.Scene {
void this.navigateTo('StoryScene');
}
private continueGame() {
private continueGame(slot?: number) {
soundDirector.start();
soundDirector.resume();
soundDirector.playSelect();
void this.continueGameAsync();
void this.continueGameAsync(slot);
}
private async continueGameAsync() {
const campaign = loadCampaignState();
private async continueGameAsync(slot?: number) {
const campaign = loadCampaignState(slot);
if (campaign.step === 'ending-complete') {
await this.navigateTo('EndingScene');
return;