Validate campaign save steps
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
Reference in New Issue
Block a user