From 390cfcf37a4ba47226f79a4e03daf6bd9621a1e1 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 03:01:40 +0900 Subject: [PATCH] Clarify campaign save slot summaries --- src/game/scenes/BattleScene.ts | 2 +- src/game/scenes/CampScene.ts | 12 +++-- src/game/scenes/TitleScene.ts | 25 ++------- src/game/state/campaignState.ts | 90 ++++++++++++++++++++++++++++++++- 4 files changed, 103 insertions(+), 26 deletions(-) diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index ccd3a45..e556129 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -11142,7 +11142,7 @@ export class BattleScene extends Phaser.Scene { ? `${this.formatSavedAt(battleSave.savedAt)} · ${battleSave.turnNumber}턴 · ${factionLabels[battleSave.activeFaction]}` : '전투 저장 없음'; const campaignSummary = campaignSlot?.exists - ? `군자금 ${campaignSlot.gold ?? 0} · ${campaignSlot.battleTitle ?? '진행 중'}` + ? `${campaignSlot.progressMeta ?? '캠페인'} · ${campaignSlot.progressTitle ?? campaignSlot.battleTitle ?? '진행 중'} · 군자금 ${campaignSlot.gold ?? 0}` : '캠페인 저장 없음'; const detail = this.add.text(left + 104, rowTop + 9, `${battleSummary}\n${campaignSummary}`, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index ee56184..63328a1 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -11923,7 +11923,7 @@ export class CampScene extends Phaser.Scene { const meta = this.add.text( x + 92, y + 24, - summary.exists ? this.formatCampSaveUpdatedAt(summary.updatedAt) : '새 저장으로 사용', + summary.exists ? this.campSaveSlotMeta(summary) : '새 저장으로 사용', this.textStyle(10, summary.exists ? '#9fb0bf' : '#d8b15f', true) ); meta.setDepth(depth + 1); @@ -11944,8 +11944,14 @@ export class CampScene extends Phaser.Scene { } private campSaveSlotDetail(summary: CampaignSaveSlotSummary) { - const battleTitle = summary.battleTitle ?? '군영 진행'; - return this.compactText(`군자금 ${summary.gold ?? 0} · ${battleTitle}`, 28); + const progressTitle = summary.progressTitle ?? summary.battleTitle ?? '군영 진행'; + return this.compactText(`군자금 ${summary.gold ?? 0} · ${progressTitle}`, 28); + } + + private campSaveSlotMeta(summary: CampaignSaveSlotSummary) { + const roster = summary.rosterCount ? ` · ${summary.rosterCount}명` : ''; + const supplies = summary.inventoryCount ? ` · 보급 ${summary.inventoryCount}` : ''; + return this.compactText(`${summary.progressMeta ?? '진행 저장'} · ${this.formatCampSaveUpdatedAt(summary.updatedAt)}${roster}${supplies}`, 44); } private formatCampSaveUpdatedAt(updatedAt?: string) { diff --git a/src/game/scenes/TitleScene.ts b/src/game/scenes/TitleScene.ts index ad7f36d..f2ab6bf 100644 --- a/src/game/scenes/TitleScene.ts +++ b/src/game/scenes/TitleScene.ts @@ -1,11 +1,11 @@ import Phaser from 'phaser'; import { soundDirector } from '../audio/SoundDirector'; -import { getBattleScenario } from '../data/battles'; import { battleIdForCampaignStep, isCampCampaignStep } from '../state/campaignRouting'; import { hasCampaignSave, listCampaignSaveSlots, loadCampaignState, + summarizeCampaignProgress, startNewCampaign, type CampaignSaveSlotSummary } from '../state/campaignState'; @@ -273,28 +273,11 @@ export class TitleScene extends Phaser.Scene { private campaignSaveDetail(campaign: ReturnType, isEndingComplete: boolean) { if (isEndingComplete) { - return `${Object.keys(campaign.battleHistory).length}전 완수 · ${campaign.roster.length}명 합류`; + return this.compactSaveDetail(`${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}명`; + const progress = summarizeCampaignProgress(campaign); + return this.compactSaveDetail(`${progress.meta} · ${progress.title}`); } private compactSaveDetail(label: string) { diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index 1044b83..5e5d3bf 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -1,6 +1,8 @@ import { equipmentExpToNext, equipmentSlots } from '../data/battleItems'; +import { battleScenarios, type BattleScenarioId } from '../data/battles'; import { campaignRecruitUnits, type BattleBond, type UnitData } from '../data/scenario'; import { normalizeSortieFormationAssignments, type SortieFormationAssignments } from '../data/sortieDeployment'; +import { battleIdForCampaignStep, isCampCampaignStep } from './campaignRouting'; export type BattleObjectiveSnapshot = { id: string; @@ -393,12 +395,93 @@ export type CampaignSaveSlotSummary = { step?: CampaignStep; gold?: number; battleTitle?: string; + progressTitle?: string; + progressMeta?: string; + rosterCount?: number; + inventoryCount?: number; }; let campaignState: CampaignState | undefined; const campaignRecruitUnitById = new Map(campaignRecruitUnits.map((unit) => [unit.id, unit])); +const campaignStepProgressLabels: Partial> = { + new: { title: '새 캠페인', meta: '시작 전' }, + prologue: { title: '도원 결의', meta: '프롤로그' }, + 'first-victory-story': { title: '탁현 방어 성공', meta: '승리 후 이야기' }, + 'hanzhong-king-camp': { title: '한중왕 즉위 준비', meta: '군영 정비' }, + 'shu-han-foundation-camp': { title: '촉한 건국 선포', meta: '군영 정비' }, + 'baidi-entrustment-camp': { title: '백제성 유탁', meta: '군영 정비' }, + 'northern-campaign-prep-camp': { title: '북벌 준비 회의', meta: '군영 정비' }, + 'ending-complete': { title: '북벌의 끝과 남은 뜻', meta: '완료' } +}; + +export function summarizeCampaignProgress(state: CampaignState) { + const battleId = battleIdForCampaignStep(state.step); + const battleTitle = battleId ? scenarioTitle(battleId) : undefined; + if (battleTitle) { + return { + title: battleTitle, + meta: state.step.endsWith('-battle') ? '출전 준비' : '전투 진행' + }; + } + + const special = campaignStepProgressLabels[state.step]; + if (special) { + return special; + } + + const latestSettlement = latestCampaignSettlement(state); + if (isCampCampaignStep(state.step) && latestSettlement) { + return { + title: latestSettlement.battleTitle, + meta: latestSettlement.outcome === 'victory' ? '승리 후 군영' : '재정비' + }; + } + + if (latestSettlement) { + return { + title: latestSettlement.battleTitle, + meta: latestSettlement.outcome === 'victory' ? '최근 승리' : '최근 전투' + }; + } + + if (state.firstBattleReport) { + return { + title: state.firstBattleReport.battleTitle, + meta: state.firstBattleReport.outcome === 'victory' ? '승리 기록' : '전투 기록' + }; + } + + return { + title: '캠페인 진행', + meta: '진행 중' + }; +} + +function scenarioTitle(id?: BattleScenarioId | string) { + if (id && id in battleScenarios) { + return battleScenarios[id as BattleScenarioId].title; + } + return undefined; +} + +function latestCampaignSettlement(state: CampaignState) { + if (state.latestBattleId) { + const latest = state.battleHistory[state.latestBattleId]; + if (latest) { + return latest; + } + } + + return Object.values(state.battleHistory) + .sort((a, b) => String(b.completedAt).localeCompare(String(a.completedAt)))[0]; +} + +function countCampaignInventory(inventory: Record) { + return Object.values(inventory).reduce((total, amount) => total + Math.max(0, amount), 0); +} + export function createInitialCampaignState(): CampaignState { return { version: 1, @@ -469,13 +552,18 @@ export function listCampaignSaveSlots(): CampaignSaveSlotSummary[] { if (!state) { return { slot, exists: false }; } + const progress = summarizeCampaignProgress(state); return { slot, exists: true, updatedAt: state.updatedAt, step: state.step, gold: state.gold, - battleTitle: state.firstBattleReport?.battleTitle + battleTitle: progress.title, + progressTitle: progress.title, + progressMeta: progress.meta, + rosterCount: state.roster.length, + inventoryCount: countCampaignInventory(state.inventory) }; }); }