Validate campaign save steps

This commit is contained in:
2026-07-05 09:13:10 +09:00
parent 70933a7f51
commit d513aec22c
2 changed files with 33 additions and 1 deletions

View File

@@ -102,6 +102,21 @@ try {
`Expected malformed inventory and sortie assignments to normalize: ${JSON.stringify(malformed)}` `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.clear();
storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' })); storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' }));
const unsupported = loadCampaignState(); const unsupported = loadCampaignState();
@@ -122,7 +137,7 @@ try {
const fallback = loadCampaignState(); const fallback = loadCampaignState();
assert(fallback.step === 'second-camp' && fallback.activeSaveSlot === 2, `Expected valid slotted save fallback: ${JSON.stringify(fallback)}`); 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 { } finally {
await server.close(); await server.close();
} }

View File

@@ -388,6 +388,18 @@ const campaignBattleSteps: Record<string, { victory: CampaignStep; retry: Campai
'sixty-sixth-battle-wuzhang-final': { victory: 'sixty-sixth-camp', retry: 'sixty-sixth-battle' } 'sixty-sixth-battle-wuzhang-final': { victory: 'sixty-sixth-camp', retry: 'sixty-sixth-battle' }
}; };
const campaignStepIds = new Set<CampaignStep>([
'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 = { export type CampaignSaveSlotSummary = {
slot: number; slot: number;
exists: boolean; exists: boolean;
@@ -787,6 +799,7 @@ function normalizeCampaignState(state: Partial<CampaignState>): CampaignState {
}; };
normalized.version = 1; normalized.version = 1;
normalized.updatedAt = typeof normalized.updatedAt === 'string' && normalized.updatedAt ? normalized.updatedAt : new Date().toISOString(); normalized.updatedAt = typeof normalized.updatedAt === 'string' && normalized.updatedAt ? normalized.updatedAt : new Date().toISOString();
normalized.step = normalizeCampaignStep(normalized.step);
normalized.activeSaveSlot = normalizeSlot(normalized.activeSaveSlot); normalized.activeSaveSlot = normalizeSlot(normalized.activeSaveSlot);
normalized.gold = normalizeNonNegativeInteger(normalized.gold); normalized.gold = normalizeNonNegativeInteger(normalized.gold);
normalized.roster = arrayOrEmpty<UnitData>(normalized.roster).map(cloneUnit); normalized.roster = arrayOrEmpty<UnitData>(normalized.roster).map(cloneUnit);
@@ -861,6 +874,10 @@ function normalizeReserveTrainingFocusId(focusId?: string): CampaignReserveTrain
: defaultCampaignReserveTrainingFocusId; : defaultCampaignReserveTrainingFocusId;
} }
function normalizeCampaignStep(step: unknown): CampaignStep {
return typeof step === 'string' && campaignStepIds.has(step as CampaignStep) ? step as CampaignStep : 'new';
}
function reserveTrainingFocusDefinition(focusId?: string) { function reserveTrainingFocusDefinition(focusId?: string) {
const normalizedId = normalizeReserveTrainingFocusId(focusId); const normalizedId = normalizeReserveTrainingFocusId(focusId);
return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === normalizedId) ?? campaignReserveTrainingFocusDefinitions[0]; return campaignReserveTrainingFocusDefinitions.find((focus) => focus.id === normalizedId) ?? campaignReserveTrainingFocusDefinitions[0];