diff --git a/scripts/verify-campaign-save-normalization.mjs b/scripts/verify-campaign-save-normalization.mjs index 24e3aa1..7de5484 100644 --- a/scripts/verify-campaign-save-normalization.mjs +++ b/scripts/verify-campaign-save-normalization.mjs @@ -494,6 +494,37 @@ try { `Expected malformed report fields to be normalized without discarding the report: ${JSON.stringify(malformedReport)}` ); + storage.clear(); + storage.set( + campaignStorageKey, + JSON.stringify({ + version: 1, + updatedAt: '2026-07-04T00:30:00.000Z', + step: 'fourth-camp', + firstBattleReport: { + battleId: 'unknown-battle-id', + battleTitle: 'Unknown Battle', + outcome: 'victory', + turnNumber: 4, + rewardGold: 120, + defeatedEnemies: 3, + totalEnemies: 5, + objectives: [], + units: [], + bonds: [], + itemRewards: [], + completedCampDialogues: [], + completedCampVisits: [], + createdAt: '2026-07-04T00:30:00.000Z' + } + }) + ); + const unknownReport = loadCampaignState(); + assert( + unknownReport.step === 'fourth-camp' && unknownReport.firstBattleReport === undefined, + `Expected first battle report with unknown battle id to be discarded safely: ${JSON.stringify(unknownReport)}` + ); + storage.clear(); storage.set( campaignStorageKey, @@ -552,6 +583,19 @@ try { ], completedAt: '2026-07-04T01:00:00.000Z' }, + 'unknown-battle-key': { + battleId: 'unknown-battle-id', + battleTitle: 'Unknown Battle', + outcome: 'victory', + rewardGold: 999, + itemRewards: [], + campaignRewards: { supplies: [], equipment: [], reputation: [], recruits: [], unlocks: [] }, + objectives: [], + units: [], + bonds: [], + reserveTraining: [], + completedAt: '2026-07-04T02:00:00.000Z' + }, corrupted: 'not-a-settlement' } }) diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index b1b5c28..17e2ab3 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -950,9 +950,10 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle } const settlement = value as Partial; + const battleId = typeof settlement.battleId === 'string' ? settlement.battleId : ''; if ( - typeof settlement.battleId !== 'string' || - settlement.battleId.length === 0 || + battleId.length === 0 || + !(battleId in battleScenarios) || typeof settlement.battleTitle !== 'string' || settlement.battleTitle.length === 0 || (settlement.outcome !== 'victory' && settlement.outcome !== 'defeat') || @@ -963,7 +964,7 @@ function normalizeCampaignBattleSettlement(value: unknown): CampaignBattleSettle } return { - battleId: settlement.battleId, + battleId, battleTitle: settlement.battleTitle, outcome: settlement.outcome, rewardGold: normalizeNonNegativeInteger(settlement.rewardGold), @@ -1112,9 +1113,10 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi return undefined; } + const battleId = typeof report.battleId === 'string' ? report.battleId : ''; if ( - typeof report.battleId !== 'string' || - report.battleId.length === 0 || + battleId.length === 0 || + !(battleId in battleScenarios) || (report.outcome !== 'victory' && report.outcome !== 'defeat') ) { return undefined; @@ -1123,13 +1125,13 @@ function normalizeFirstBattleReport(report: unknown): FirstBattleReport | undefi const battleTitle = typeof report.battleTitle === 'string' && report.battleTitle.length > 0 ? report.battleTitle - : battleScenarios[report.battleId as BattleScenarioId]?.title; + : battleScenarios[battleId as BattleScenarioId]?.title; if (!battleTitle) { return undefined; } return { - battleId: report.battleId, + battleId, battleTitle, outcome: report.outcome, turnNumber: normalizeNonNegativeInteger(report.turnNumber),