diff --git a/scripts/verify-campaign-save-normalization.mjs b/scripts/verify-campaign-save-normalization.mjs index 144b21f..fd693ff 100644 --- a/scripts/verify-campaign-save-normalization.mjs +++ b/scripts/verify-campaign-save-normalization.mjs @@ -102,6 +102,21 @@ try { `Expected malformed inventory and sortie assignments to normalize: ${JSON.stringify(malformed)}` ); + storage.clear(); + storage.set( + campaignStorageKey, + JSON.stringify({ + version: 1, + updatedAt: '2026-07-03T00:00:00.000Z', + step: 'lost-between-battles', + activeSaveSlot: 1, + gold: 90 + }) + ); + const invalidStep = loadCampaignState(); + assert(invalidStep.step === 'new', `Expected unknown campaign step to reset safely: ${JSON.stringify(invalidStep)}`); + assert(invalidStep.gold === 90, `Expected unknown-step save to keep harmless economy data: ${JSON.stringify(invalidStep)}`); + storage.clear(); storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' })); const unsupported = loadCampaignState(); @@ -122,7 +137,7 @@ try { const fallback = loadCampaignState(); assert(fallback.step === 'second-camp' && fallback.activeSaveSlot === 2, `Expected valid slotted save fallback: ${JSON.stringify(fallback)}`); - console.log('Verified campaign save normalization, malformed-field recovery, unsupported-version rejection, and slotted fallback.'); + console.log('Verified campaign save normalization, malformed-field recovery, unknown-step recovery, unsupported-version rejection, and slotted fallback.'); } finally { await server.close(); } diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index a07d7c2..295be36 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -388,6 +388,18 @@ const campaignBattleSteps: Record([ + 'new', + 'prologue', + 'first-victory-story', + 'ending-complete', + 'hanzhong-king-camp', + 'shu-han-foundation-camp', + 'baidi-entrustment-camp', + 'northern-campaign-prep-camp', + ...Object.values(campaignBattleSteps).flatMap(({ victory, retry }) => [victory, retry]) +]); + export type CampaignSaveSlotSummary = { slot: number; exists: boolean; @@ -787,6 +799,7 @@ function normalizeCampaignState(state: Partial): CampaignState { }; normalized.version = 1; normalized.updatedAt = typeof normalized.updatedAt === 'string' && normalized.updatedAt ? normalized.updatedAt : new Date().toISOString(); + normalized.step = normalizeCampaignStep(normalized.step); normalized.activeSaveSlot = normalizeSlot(normalized.activeSaveSlot); normalized.gold = normalizeNonNegativeInteger(normalized.gold); normalized.roster = arrayOrEmpty(normalized.roster).map(cloneUnit); @@ -861,6 +874,10 @@ function normalizeReserveTrainingFocusId(focusId?: string): CampaignReserveTrain : defaultCampaignReserveTrainingFocusId; } +function normalizeCampaignStep(step: unknown): CampaignStep { + return typeof step === 'string' && campaignStepIds.has(step as CampaignStep) ? step as CampaignStep : 'new'; +} + function reserveTrainingFocusDefinition(focusId?: string) { const normalizedId = normalizeReserveTrainingFocusId(focusId); return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === normalizedId) ?? campaignReserveTrainingFocusDefinitions[0];