diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index 229e369..ee56184 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -103,8 +103,11 @@ import { getCampaignState, getFirstBattleReport, markCampaignStep, + listCampaignSaveSlots, + campaignSaveSlotCount, saveCampaignState, setCampaignReserveTrainingFocus, + type CampaignSaveSlotSummary, type CampaignStep, type CampaignState, type CampaignReserveTrainingFocusDefinition, @@ -10713,6 +10716,7 @@ export class CampScene extends Phaser.Scene { private contentObjects: Phaser.GameObjects.GameObject[] = []; private dialogueObjects: Phaser.GameObjects.GameObject[] = []; private sortieObjects: Phaser.GameObjects.GameObject[] = []; + private saveSlotObjects: Phaser.GameObjects.GameObject[] = []; private tabButtons: CampTabButtonView[] = []; private visitedTabs = new Set(); private selectedSortieUnitIds: string[] = []; @@ -10737,6 +10741,7 @@ export class CampScene extends Phaser.Scene { this.contentObjects = []; this.dialogueObjects = []; this.sortieObjects = []; + this.saveSlotObjects = []; this.tabButtons = []; this.visitedTabs = new Set(['status']); this.campaign = getCampaignState(); @@ -11825,9 +11830,7 @@ export class CampScene extends Phaser.Scene { this.addTabButton('연표', 'progress', 966, 38); this.addCommandButton('저장', 1044, 38, 76, () => { soundDirector.playSelect(); - this.campaign = saveCampaignState(getCampaignState()); - this.showCampNotice(`슬롯 ${this.campaign.activeSaveSlot}에 군영 진행을 저장했습니다.`); - this.render(); + this.showCampSaveSlotPanel(); }); this.addCommandButton('다음 이야기', 1160, 38, 142, () => { soundDirector.playSelect(); @@ -11840,6 +11843,141 @@ export class CampScene extends Phaser.Scene { }); } + private showCampSaveSlotPanel() { + if (this.saveSlotObjects.length > 0) { + this.hideCampSaveSlotPanel(); + return; + } + + const depth = 48; + const width = 430; + const height = 252; + const left = this.scale.width - width - 36; + const top = 70; + const activeSlot = this.campaign?.activeSaveSlot ?? getCampaignState().activeSaveSlot; + const slots = listCampaignSaveSlots(); + + const panel = this.add.rectangle(left, top, width, height, 0x0f1720, 0.98); + panel.setOrigin(0); + panel.setDepth(depth); + panel.setStrokeStyle(2, palette.gold, 0.86); + panel.setInteractive(); + this.saveSlotObjects.push(panel); + + const title = this.add.text(left + 22, top + 16, '군영 저장 슬롯', this.textStyle(22, '#f2e3bf', true)); + title.setDepth(depth + 1); + this.saveSlotObjects.push(title); + + const guide = this.add.text( + left + 22, + top + 48, + '현재 군영 진행, 보유 장비, 편성 정보를 선택한 슬롯에 저장합니다.', + this.textStyle(13, '#aeb7c2') + ); + guide.setDepth(depth + 1); + this.saveSlotObjects.push(guide); + + for (let slot = 1; slot <= campaignSaveSlotCount; slot += 1) { + const summary = slots.find((candidate) => candidate.slot === slot) ?? { slot, exists: false }; + this.renderCampSaveSlotRow(summary, activeSlot, left + 18, top + 82 + (slot - 1) * 52, width - 36, depth + 1); + } + + const close = this.add.text(left + width - 58, top + 18, '닫기', this.textStyle(14, '#d8b15f', true)); + close.setDepth(depth + 2); + close.setInteractive({ useHandCursor: true }); + close.on('pointerdown', () => { + soundDirector.playSelect(); + this.hideCampSaveSlotPanel(); + }); + this.saveSlotObjects.push(close); + } + + private renderCampSaveSlotRow( + summary: CampaignSaveSlotSummary, + activeSlot: number, + x: number, + y: number, + width: number, + depth: number + ) { + const active = summary.slot === activeSlot; + const row = this.add.rectangle(x, y, width, 44, active ? 0x263a2d : 0x172230, active ? 0.98 : 0.94); + row.setOrigin(0); + row.setDepth(depth); + row.setStrokeStyle(1, active ? palette.gold : palette.blue, active ? 0.86 : 0.62); + this.saveSlotObjects.push(row); + + const title = this.add.text(x + 14, y + 8, `슬롯 ${summary.slot}`, this.textStyle(15, '#f2e3bf', true)); + title.setDepth(depth + 1); + this.saveSlotObjects.push(title); + + const detail = this.add.text( + x + 92, + y + 7, + summary.exists ? this.campSaveSlotDetail(summary) : '비어 있음', + this.textStyle(12, summary.exists ? '#d4dce6' : '#87919c', summary.exists) + ); + detail.setDepth(depth + 1); + this.saveSlotObjects.push(detail); + + const meta = this.add.text( + x + 92, + y + 24, + summary.exists ? this.formatCampSaveUpdatedAt(summary.updatedAt) : '새 저장으로 사용', + this.textStyle(10, summary.exists ? '#9fb0bf' : '#d8b15f', true) + ); + meta.setDepth(depth + 1); + this.saveSlotObjects.push(meta); + + const saveLabel = this.add.text(x + width - 50, y + 13, active ? '현재' : '저장', this.textStyle(12, active ? '#a8ffd0' : '#ffdf7b', true)); + saveLabel.setOrigin(0.5, 0); + saveLabel.setDepth(depth + 1); + this.saveSlotObjects.push(saveLabel); + + const run = () => this.saveCampToSlot(summary.slot); + [row, title, detail, meta, saveLabel].forEach((object) => { + object.setInteractive({ useHandCursor: true }); + object.on('pointerdown', run); + }); + row.on('pointerover', () => row.setFillStyle(active ? 0x314a38 : 0x263647, 0.98)); + row.on('pointerout', () => row.setFillStyle(active ? 0x263a2d : 0x172230, active ? 0.98 : 0.94)); + } + + private campSaveSlotDetail(summary: CampaignSaveSlotSummary) { + const battleTitle = summary.battleTitle ?? '군영 진행'; + return this.compactText(`군자금 ${summary.gold ?? 0} · ${battleTitle}`, 28); + } + + private formatCampSaveUpdatedAt(updatedAt?: string) { + if (!updatedAt) { + return '저장 시각 없음'; + } + + 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 saveCampToSlot(slot: number) { + soundDirector.playSelect(); + this.hideCampSaveSlotPanel(); + this.campaign = saveCampaignState(getCampaignState(), slot); + this.showCampNotice(`슬롯 ${this.campaign.activeSaveSlot}에 군영 진행을 저장했습니다.`); + this.render(); + } + + private hideCampSaveSlotPanel() { + this.saveSlotObjects.forEach((object) => object.destroy()); + this.saveSlotObjects = []; + } + private addTabButton(label: string, tab: CampTab, x: number, y: number) { const bg = this.add.rectangle(x, y, 76, 34, 0x182431, 0.94); bg.setStrokeStyle(1, palette.blue, 0.62);