import { createServer } from 'vite'; const storage = new Map(); globalThis.window = { localStorage: { getItem(key) { return storage.has(key) ? storage.get(key) : null; }, setItem(key, value) { storage.set(key, String(value)); }, removeItem(key) { storage.delete(key); }, clear() { storage.clear(); } } }; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true }, appType: 'custom' }); try { const { campaignStorageKey, applyCampBondExp, applyCampVisitReward, getCampaignState, hasCampaignSave, loadCampaignState, normalizeCampaignReserveTrainingAssignments, normalizeCampaignSortieFormationPresets, normalizeCampaignSortieItemAssignments, normalizeCampaignSortieOrderResultSnapshot, normalizeCampaignSortiePerformanceSnapshot, normalizeCampaignSortieReviewSnapshot, resetCampaignState, saveCampaignState, setCampaignState, setCampaignReserveTrainingFocus, setCampaignReserveTrainingAssignment, setCampaignSortieOrderSelection, setFirstBattleReport } = await server.ssrLoadModule('/src/game/state/campaignState.ts'); const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts'); const { campaignRecruitUnits } = await server.ssrLoadModule('/src/game/data/scenario.ts'); const { normalizeSortieFormationAssignments } = await server.ssrLoadModule('/src/game/data/sortieDeployment.ts'); const sortieStatsFixture = (unitId, overrides = {}) => ({ unitId, hpBefore: 30, maxHpBefore: 30, damageDealt: 10, damageTaken: 5, defeats: 1, actions: 2, support: 0, sortieBonusDamage: 1, sortiePreventedDamage: 2, sortieBonusHealing: 0, sortieExtendedBondTriggers: 0, intentDefeats: 0, intentLineBreaks: 0, intentGuardSuccesses: 0, ...overrides }); storage.clear(); const empty = loadCampaignState(); assert(empty.version === 1 && empty.step === 'new', `Expected empty storage to create initial campaign state: ${JSON.stringify(empty)}`); const normalizedLaunchSupplies = normalizeCampaignSortieItemAssignments( { 'liu-bei': { ' bean ': '9', salve: '2', stone: '4', ['x'.repeat(97)]: '1' }, ' liu-bei ': { wine: '1' }, 'guan-yu': { wine: '3' }, 'ghost-unit': { bean: '1' }, ['x'.repeat(97)]: { bean: '1' }, '': { bean: '1' } }, new Set(['liu-bei', 'guan-yu']) ); assert( normalizedLaunchSupplies['liu-bei']?.bean === 2 && normalizedLaunchSupplies['liu-bei']?.salve === 1 && normalizedLaunchSupplies['liu-bei']?.stone === undefined && normalizedLaunchSupplies['liu-bei']?.wine === 1 && normalizedLaunchSupplies['liu-bei']?.['x'.repeat(97)] === undefined && normalizedLaunchSupplies['guan-yu']?.wine === 1 && normalizedLaunchSupplies['ghost-unit'] === undefined, `Expected launch sortie supplies to share save normalization rules: ${JSON.stringify(normalizedLaunchSupplies)}` ); const cappedLaunchSupplies = normalizeCampaignSortieItemAssignments( Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, { bean: 1 }])) ); assert( Object.keys(cappedLaunchSupplies).length === 96 && cappedLaunchSupplies['unit-95']?.bean === 1 && cappedLaunchSupplies['unit-96'] === undefined, `Expected launch sortie supplies without a roster filter to stay capped: ${JSON.stringify(cappedLaunchSupplies)}` ); const normalizedLaunchFormation = normalizeSortieFormationAssignments( { 'liu-bei': 'front', ' liu-bei ': 'support', 'guan-yu': 'flank', 'ghost-unit': 'reserve', 'zhang-fei': 'broken', ['x'.repeat(97)]: 'front', '': 'front' }, new Set(['liu-bei', 'guan-yu']) ); assert( normalizedLaunchFormation['liu-bei'] === 'front' && normalizedLaunchFormation['guan-yu'] === 'flank' && normalizedLaunchFormation['ghost-unit'] === undefined && normalizedLaunchFormation['zhang-fei'] === undefined && normalizedLaunchFormation['x'.repeat(97)] === undefined, `Expected launch sortie formation assignments to trim, filter, and preserve first valid roles: ${JSON.stringify(normalizedLaunchFormation)}` ); const cappedLaunchFormation = normalizeSortieFormationAssignments( Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, 'front'])) ); assert( Object.keys(cappedLaunchFormation).length === 96 && cappedLaunchFormation['unit-95'] === 'front' && cappedLaunchFormation['unit-96'] === undefined, `Expected launch sortie formation assignments without a roster filter to stay capped: ${JSON.stringify(cappedLaunchFormation)}` ); const normalizedSortiePresets = normalizeCampaignSortieFormationPresets( { elite: { selectedUnitIds: [' liu-bei ', 'guan-yu', 'liu-bei', 'ghost-unit', '', 7, 'x'.repeat(97)], formationAssignments: { ' liu-bei ': 'front', 'guan-yu': 'flank', 'zhang-fei': 'reserve', 'ghost-unit': 'support', broken: 'center' }, sortieItemAssignments: { 'liu-bei': { bean: 2 } }, itemAssignments: { 'guan-yu': { wine: 1 } } }, mobile: 'corrupted', siege: { selectedUnitIds: [], formationAssignments: { 'liu-bei': 'front' } }, ghost: { selectedUnitIds: ['liu-bei'], formationAssignments: { 'liu-bei': 'front' } } }, new Set(['liu-bei', 'guan-yu', 'zhang-fei']) ); assert( JSON.stringify(Object.keys(normalizedSortiePresets)) === JSON.stringify(['elite']) && JSON.stringify(normalizedSortiePresets.elite?.selectedUnitIds) === JSON.stringify(['liu-bei', 'guan-yu']) && JSON.stringify(normalizedSortiePresets.elite?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front', 'guan-yu': 'flank' }) && !Object.prototype.hasOwnProperty.call(normalizedSortiePresets.elite ?? {}, 'sortieItemAssignments') && !Object.prototype.hasOwnProperty.call(normalizedSortiePresets.elite ?? {}, 'itemAssignments'), `Expected sortie presets to trim and dedupe ids, whitelist slots, scope roles to selected roster members, and exclude supplies: ${JSON.stringify(normalizedSortiePresets)}` ); const cappedSortiePresets = normalizeCampaignSortieFormationPresets({ elite: { selectedUnitIds: Array.from({ length: 120 }, (_, index) => `unit-${index}`), formationAssignments: Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, 'front'])), sortieItemAssignments: { 'unit-0': { bean: 2 } } } }); assert( cappedSortiePresets.elite?.selectedUnitIds.length === 96 && cappedSortiePresets.elite.selectedUnitIds[0] === 'unit-0' && cappedSortiePresets.elite.selectedUnitIds[95] === 'unit-95' && !cappedSortiePresets.elite.selectedUnitIds.includes('unit-96') && Object.keys(cappedSortiePresets.elite.formationAssignments).length === 96 && cappedSortiePresets.elite.formationAssignments['unit-95'] === 'front' && cappedSortiePresets.elite.formationAssignments['unit-96'] === undefined && !Object.prototype.hasOwnProperty.call(cappedSortiePresets.elite, 'sortieItemAssignments'), `Expected each sortie preset and its role map to stay capped at 96 entries without persisting supplies: ${JSON.stringify(cappedSortiePresets)}` ); const normalizedSortiePerformance = normalizeCampaignSortiePerformanceSnapshot( { selectedUnitIds: [' liu-bei ', 'guan-yu', 'liu-bei', 'ghost-unit', '', 7, 'x'.repeat(97)], formationAssignments: { ' liu-bei ': 'front', 'liu-bei': 'support', 'guan-yu': 'flank', 'ghost-unit': 'reserve', broken: 'center' }, unitStats: [ sortieStatsFixture(' liu-bei ', { hpBefore: '28', maxHpBefore: '20', damageDealt: '1000000', damageTaken: -4, defeats: '2.9', actions: 'not-a-number', support: '7', sortieBonusDamage: '3', sortiePreventedDamage: '4', sortieBonusHealing: '5', sortieExtendedBondTriggers: '6', intentDefeats: '7', intentLineBreaks: '8', intentGuardSuccesses: '9', finalHp: 1, sortieItemAssignments: { bean: 2 } }), sortieStatsFixture('liu-bei', { damageDealt: 1 }), sortieStatsFixture('guan-yu', { hpBefore: '12', maxHpBefore: '30', support: '4' }), sortieStatsFixture('ghost-unit'), { broken: true } ], sortieItemAssignments: { 'liu-bei': { bean: 2 } }, itemAssignments: { 'guan-yu': { wine: 1 } } }, new Set(['liu-bei', 'guan-yu']) ); const normalizedLiuBeiPerformance = normalizedSortiePerformance?.unitStats.find((stats) => stats.unitId === 'liu-bei'); const normalizedGuanYuPerformance = normalizedSortiePerformance?.unitStats.find((stats) => stats.unitId === 'guan-yu'); assert( JSON.stringify(normalizedSortiePerformance?.selectedUnitIds) === JSON.stringify(['liu-bei', 'guan-yu']) && JSON.stringify(normalizedSortiePerformance?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front', 'guan-yu': 'flank' }) && normalizedSortiePerformance?.unitStats.length === 2 && normalizedLiuBeiPerformance?.hpBefore === 28 && normalizedLiuBeiPerformance.maxHpBefore === 28 && normalizedLiuBeiPerformance.damageDealt === 999999 && normalizedLiuBeiPerformance.damageTaken === 0 && normalizedLiuBeiPerformance.defeats === 2 && normalizedLiuBeiPerformance.actions === 0 && normalizedLiuBeiPerformance.support === 7 && normalizedLiuBeiPerformance.sortieBonusDamage === 3 && normalizedLiuBeiPerformance.sortiePreventedDamage === 4 && normalizedLiuBeiPerformance.sortieBonusHealing === 5 && normalizedLiuBeiPerformance.sortieExtendedBondTriggers === 6 && normalizedLiuBeiPerformance.intentDefeats === 7 && normalizedLiuBeiPerformance.intentLineBreaks === 8 && normalizedLiuBeiPerformance.intentGuardSuccesses === 9 && normalizedGuanYuPerformance?.maxHpBefore === 30 && normalizedGuanYuPerformance.support === 4 && !Object.prototype.hasOwnProperty.call(normalizedSortiePerformance ?? {}, 'sortieItemAssignments') && !Object.prototype.hasOwnProperty.call(normalizedSortiePerformance ?? {}, 'itemAssignments') && !Object.prototype.hasOwnProperty.call(normalizedLiuBeiPerformance ?? {}, 'finalHp') && !Object.prototype.hasOwnProperty.call(normalizedLiuBeiPerformance ?? {}, 'sortieItemAssignments'), `Expected sortie performance snapshots to normalize ids, roles, HP baselines, numeric contribution fields, duplicates, and exclude supplies: ${JSON.stringify(normalizedSortiePerformance)}` ); const cappedSortiePerformance = normalizeCampaignSortiePerformanceSnapshot({ selectedUnitIds: Array.from({ length: 120 }, (_, index) => `unit-${index}`), formationAssignments: Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, 'front'])), unitStats: Array.from({ length: 120 }, (_, index) => sortieStatsFixture(`unit-${index}`)) }); assert( cappedSortiePerformance?.selectedUnitIds.length === 96 && cappedSortiePerformance.selectedUnitIds[95] === 'unit-95' && cappedSortiePerformance.formationAssignments['unit-95'] === 'front' && cappedSortiePerformance.formationAssignments['unit-96'] === undefined && cappedSortiePerformance.unitStats.length === 96 && cappedSortiePerformance.unitStats[95].unitId === 'unit-95' && normalizeCampaignSortiePerformanceSnapshot({ selectedUnitIds: [], unitStats: [] }) === undefined, `Expected sortie performance snapshots to stay capped and empty snapshots to be discarded: ${JSON.stringify(cappedSortiePerformance)}` ); const normalizedSortieReview = normalizeCampaignSortieReviewSnapshot( { version: 1, score: '88.9', grade: 'D', activeBondCount: '999', enemyCount: '1000', sourcePresetIds: ['siege', 'elite', 'siege', 'ghost', 'mobile', 7], sortieItemAssignments: { 'liu-bei': { bean: 2 } } }, normalizedSortiePerformance ); assert( normalizedSortieReview?.version === 1 && normalizedSortieReview.score === 88 && normalizedSortieReview.grade === 'A' && normalizedSortieReview.activeBondCount === 96 && normalizedSortieReview.enemyCount === 96 && JSON.stringify(normalizedSortieReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile', 'siege']) && !Object.prototype.hasOwnProperty.call(normalizedSortieReview, 'sortieItemAssignments'), `Expected sortie reviews to clamp evaluator inputs, derive grade from score, canonicalize known preset ids, and exclude unrelated fields: ${JSON.stringify(normalizedSortieReview)}` ); assert( normalizeCampaignSortieReviewSnapshot({ ...normalizedSortieReview, version: 2 }, normalizedSortiePerformance) === undefined && normalizeCampaignSortieReviewSnapshot({ score: 88, grade: 'A', sourcePresetIds: ['elite'] }, normalizedSortiePerformance) === undefined && normalizeCampaignSortieReviewSnapshot(normalizedSortieReview) === undefined && normalizeCampaignSortieReviewSnapshot(normalizedSortieReview, { selectedUnitIds: ['liu-bei'], formationAssignments: {}, unitStats: [] }) === undefined, 'Expected sortie reviews with unsupported/missing nested versions or missing/incomplete performance snapshots to be discarded' ); const sortieReviewGradeBoundaries = [[90, 'S'], [75, 'A'], [60, 'B'], [45, 'C'], [44, 'D']]; assert( sortieReviewGradeBoundaries.every(([score, grade]) => ( normalizeCampaignSortieReviewSnapshot( { ...normalizedSortieReview, score, grade: 'D' }, normalizedSortiePerformance )?.grade === grade )), `Expected persisted sortie review grades to match evaluator boundaries: ${JSON.stringify(sortieReviewGradeBoundaries)}` ); const normalizedEliteOrder = normalizeCampaignSortieOrderResultSnapshot({ version: 1, orderId: 'elite', score: '999', achieved: false, rewardGranted: true, progress: [ { id: 'elite-survival', achieved: true, value: '3.9', target: '3.2' }, { id: 'victory', achieved: true, value: '1', target: '0' }, { id: 'elite-grade', achieved: false, value: '-8', target: '75' }, { id: 'ghost-check', achieved: true, value: 999, target: 1 } ] }); assert( normalizedEliteOrder?.version === 1 && normalizedEliteOrder.orderId === 'elite' && normalizedEliteOrder.score === 100 && normalizedEliteOrder.achieved === false && normalizedEliteOrder.rewardGranted === false && JSON.stringify(normalizedEliteOrder.progress.map((entry) => entry.id)) === JSON.stringify(['victory', 'elite-grade', 'elite-survival']) && normalizedEliteOrder.progress[0].target === 1 && normalizedEliteOrder.progress[1].value === 0 && normalizedEliteOrder.progress[2].value === 3 && normalizedEliteOrder.progress[2].target === 3, `Expected sortie orders to canonicalize checks, clamp score/numbers, derive completion, and reject unearned reward flags: ${JSON.stringify(normalizedEliteOrder)}` ); const normalizedMissingMobileCheck = normalizeCampaignSortieOrderResultSnapshot({ version: 1, orderId: 'mobile', score: 90, achieved: true, rewardGranted: true, progress: [ { id: 'victory', achieved: true, value: 1, target: 1 }, { id: 'mobile-quick', achieved: true, value: 8, target: 8 } ] }); assert( normalizedMissingMobileCheck?.achieved === false && normalizedMissingMobileCheck.rewardGranted === false && normalizedMissingMobileCheck.progress.find((entry) => entry.id === 'mobile-breakthrough')?.achieved === false && normalizeCampaignSortieOrderResultSnapshot({ ...normalizedEliteOrder, version: 2 }) === undefined && normalizeCampaignSortieOrderResultSnapshot({ ...normalizedEliteOrder, orderId: 'ghost' }) === undefined, `Expected missing checks and unsupported sortie-order versions/ids to fail safely: ${JSON.stringify(normalizedMissingMobileCheck)}` ); const normalizedStringBooleans = normalizeCampaignSortieOrderResultSnapshot({ ...normalizedEliteOrder, achieved: 'false', rewardGranted: 'false', progress: normalizedEliteOrder.progress.map((entry) => ({ ...entry, achieved: 'false' })) }); assert( normalizedStringBooleans?.achieved === false && normalizedStringBooleans.rewardGranted === false && normalizedStringBooleans.progress.every((entry) => entry.achieved === false), `Expected malformed string booleans to stay false instead of restoring an unearned reward claim: ${JSON.stringify(normalizedStringBooleans)}` ); const normalizedOrderCommand = normalizeCampaignSortieOrderResultSnapshot({ ...normalizedEliteOrder, command: { source: 'sortie-order', role: 'support', actorId: ' liu-bei ', usedTurn: '3.9', effectPending: 'true', effectValue: '1000001', statusesCleared: '2.9', effectLost: true, extra: 'discard' } }); assert( normalizedOrderCommand?.command?.source === 'sortie-order' && normalizedOrderCommand.command.role === 'support' && normalizedOrderCommand.command.actorId === 'liu-bei' && normalizedOrderCommand.command.usedTurn === 3 && normalizedOrderCommand.command.effectPending === false && normalizedOrderCommand.command.effectValue === 999999 && normalizedOrderCommand.command.statusesCleared === 2 && normalizedOrderCommand.command.effectLost === true && !Object.prototype.hasOwnProperty.call(normalizedOrderCommand.command, 'extra') && !Object.prototype.hasOwnProperty.call(normalizedEliteOrder, 'command'), `Expected sortie-order commands to sanitize ids, numbers, booleans, and preserve command-less legacy snapshots: ${JSON.stringify(normalizedOrderCommand)}` ); const invalidOrderCommands = [ { ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, source: 'morale' } }, { ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, role: 'reserve' } }, { ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, actorId: 'x'.repeat(97) } }, { ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, usedTurn: 0 } } ]; assert( invalidOrderCommands.every((snapshot) => ( !Object.prototype.hasOwnProperty.call(normalizeCampaignSortieOrderResultSnapshot(snapshot) ?? {}, 'command') )), 'Expected malformed optional command records to be discarded without losing the sortie-order result.' ); const normalizedInitiativeCommand = normalizeCampaignSortieOrderResultSnapshot({ ...normalizedEliteOrder, command: { ...normalizedOrderCommand.command, source: 'initiative', role: 'flank', effectPending: true, effectLost: 'false' } })?.command; assert( normalizedInitiativeCommand?.source === 'initiative' && normalizedInitiativeCommand.role === 'flank' && normalizedInitiativeCommand.effectPending === true && normalizedInitiativeCommand.effectLost === false, `Expected explicit initiative command sources and strict command booleans to normalize: ${JSON.stringify(normalizedInitiativeCommand)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T11:10:00.000Z', step: 'first-camp', sortieOrderSelection: { battleId: 'second-battle-yellow-turban-pursuit', orderId: 'mobile', extra: true }, sortieOrderHistory: { 'first-battle-zhuo-commandery': { elite: { version: 1, orderId: 'elite', score: 101, achieved: false, rewardGranted: true, progress: [ { id: 'victory', achieved: true, value: 1, target: 1 }, { id: 'elite-grade', achieved: true, value: 90, target: 75 }, { id: 'elite-survival', achieved: true, value: 3, target: 3 }, { id: 'ghost-check', achieved: true, value: 1, target: 1 } ] }, mobile: { ...normalizedEliteOrder, orderId: 'elite' }, ghost: normalizedEliteOrder }, 'unknown-battle': { elite: normalizedEliteOrder }, corrupted: 'broken' }, claimedSortieOrderRewardIds: [ 'first-battle-zhuo-commandery:elite', 'first-battle-zhuo-commandery:elite', 'second-battle-yellow-turban-pursuit:mobile', 'unknown-battle:elite', 'first-battle-zhuo-commandery:ghost', 7 ] }) ); const normalizedOrderState = loadCampaignState(); const normalizedHistoricalElite = normalizedOrderState.sortieOrderHistory['first-battle-zhuo-commandery']?.elite; assert( JSON.stringify(normalizedOrderState.sortieOrderSelection) === JSON.stringify({ battleId: 'second-battle-yellow-turban-pursuit', orderId: 'mobile' }) && Object.keys(normalizedOrderState.sortieOrderHistory).length === 1 && Object.keys(normalizedOrderState.sortieOrderHistory['first-battle-zhuo-commandery']).length === 1 && normalizedHistoricalElite?.score === 100 && normalizedHistoricalElite.achieved === true && normalizedHistoricalElite.rewardGranted === true && JSON.stringify(normalizedOrderState.claimedSortieOrderRewardIds.sort()) === JSON.stringify([ 'first-battle-zhuo-commandery:elite', 'second-battle-yellow-turban-pursuit:mobile' ]), `Expected sortie-order selection/history/claims to whitelist battles and ids, dedupe claims, and recover reward claims from history: ${JSON.stringify(normalizedOrderState)}` ); const selectedOrderState = setCampaignSortieOrderSelection('first-battle-zhuo-commandery', 'siege'); assert( JSON.stringify(selectedOrderState.sortieOrderSelection) === JSON.stringify({ battleId: 'first-battle-zhuo-commandery', orderId: 'siege' }), `Expected explicit sortie-order selection to bind to one battle: ${JSON.stringify(selectedOrderState.sortieOrderSelection)}` ); const invalidOrderSelection = setCampaignSortieOrderSelection('unknown-battle', 'elite'); assert( JSON.stringify(invalidOrderSelection.sortieOrderSelection) === JSON.stringify({ battleId: 'first-battle-zhuo-commandery', orderId: 'siege' }), `Expected an unknown battle not to disturb the current sortie-order selection: ${JSON.stringify(invalidOrderSelection.sortieOrderSelection)}` ); const clearedOrderSelection = setCampaignSortieOrderSelection('first-battle-zhuo-commandery'); assert( clearedOrderSelection.sortieOrderSelection === undefined, `Expected clearing the matching battle order to remove the scoped selection: ${JSON.stringify(clearedOrderSelection.sortieOrderSelection)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T11:20:00.000Z', step: 'third-camp', activeSaveSlot: 2, roster: [ { id: 'liu-bei', name: 'Liu Bei', faction: 'ally' }, { id: 'guan-yu', name: 'Guan Yu', faction: 'ally' } ], sortieFormationPresets: { elite: { selectedUnitIds: ['liu-bei', 'guan-yu'], formationAssignments: { 'liu-bei': 'front', 'guan-yu': 'flank' } } } }) ); const loadedPresetState = loadCampaignState(); const detachedPresetState = getCampaignState(); detachedPresetState.sortieFormationPresets.elite.selectedUnitIds.push('ghost-unit'); detachedPresetState.sortieFormationPresets.elite.formationAssignments['liu-bei'] = 'support'; const unmutatedPresetState = getCampaignState(); assert( JSON.stringify(unmutatedPresetState.sortieFormationPresets.elite?.selectedUnitIds) === JSON.stringify(['liu-bei', 'guan-yu']) && unmutatedPresetState.sortieFormationPresets.elite?.formationAssignments['liu-bei'] === 'front', `Expected getCampaignState to deep-clone nested sortie preset members and roles: ${JSON.stringify(unmutatedPresetState.sortieFormationPresets)}` ); const savedPresetState = saveCampaignState(loadedPresetState, 2); savedPresetState.sortieFormationPresets.elite.selectedUnitIds.length = 0; savedPresetState.sortieFormationPresets.elite.formationAssignments['guan-yu'] = 'support'; const persistedCurrentPresetState = JSON.parse(storage.get(campaignStorageKey)); const persistedSlotPresetState = JSON.parse(storage.get(`${campaignStorageKey}:slot-2`)); assert( JSON.stringify(persistedCurrentPresetState.sortieFormationPresets) === JSON.stringify(persistedSlotPresetState.sortieFormationPresets) && JSON.stringify(persistedCurrentPresetState.sortieFormationPresets.elite?.selectedUnitIds) === JSON.stringify(['liu-bei', 'guan-yu']) && persistedCurrentPresetState.sortieFormationPresets.elite?.formationAssignments['liu-bei'] === 'front' && persistedCurrentPresetState.sortieFormationPresets.elite?.formationAssignments['guan-yu'] === 'flank', `Expected saving nested sortie presets to synchronize current and slot storage without sharing returned references: ${JSON.stringify({ current: persistedCurrentPresetState.sortieFormationPresets, slot: persistedSlotPresetState.sortieFormationPresets })}` ); const normalizedReserveDrills = normalizeCampaignReserveTrainingAssignments( { 'liu-bei': 'bond-practice', ' liu-bei ': 'class-practice', 'guan-yu': 'class-practice', 'ghost-unit': 'balanced', 'zhang-fei': 'broken-focus', ['x'.repeat(97)]: 'balanced', '': 'balanced' }, new Set(['liu-bei', 'guan-yu']) ); assert( normalizedReserveDrills['liu-bei'] === 'bond-practice' && normalizedReserveDrills['guan-yu'] === 'class-practice' && normalizedReserveDrills['ghost-unit'] === undefined && normalizedReserveDrills['zhang-fei'] === undefined && normalizedReserveDrills['x'.repeat(97)] === undefined, `Expected reserve drill assignments to trim, filter, and preserve first valid focus ids: ${JSON.stringify(normalizedReserveDrills)}` ); const cappedReserveDrills = normalizeCampaignReserveTrainingAssignments( Object.fromEntries(Array.from({ length: 120 }, (_, index) => [`unit-${index}`, 'balanced'])) ); assert( Object.keys(cappedReserveDrills).length === 96 && cappedReserveDrills['unit-95'] === 'balanced' && cappedReserveDrills['unit-96'] === undefined, `Expected reserve drill assignments without a roster filter to stay capped: ${JSON.stringify(cappedReserveDrills)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T11:30:00.000Z', step: 'third-camp', roster: [{ id: 'liu-bei', name: 'Liu Bei', faction: 'ally' }] }) ); loadCampaignState(); const assignedReserveDrill = setCampaignReserveTrainingAssignment(' liu-bei ', 'class-practice'); const ignoredReserveDrill = setCampaignReserveTrainingAssignment('ghost-unit', 'bond-practice'); const clearedReserveDrill = setCampaignReserveTrainingAssignment('liu-bei'); assert( assignedReserveDrill.reserveTrainingAssignments['liu-bei'] === 'class-practice' && ignoredReserveDrill.reserveTrainingAssignments['ghost-unit'] === undefined && clearedReserveDrill.reserveTrainingAssignments['liu-bei'] === undefined, `Expected reserve drill assignment API to trim, filter, and clear per-officer focus ids: ${JSON.stringify({ assignedReserveDrill, ignoredReserveDrill, clearedReserveDrill })}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T11:35:00.000Z', step: 'third-camp', roster: [ { id: 'liu-bei', name: 'Liu Bei', faction: 'ally' }, { id: 'guan-yu', name: 'Guan Yu', faction: 'ally' } ], reserveTrainingFocus: 'balanced', reserveTrainingAssignments: { 'liu-bei': 'bond-practice', 'guan-yu': 'class-practice' } }) ); loadCampaignState(); const dedupedReserveDrill = setCampaignReserveTrainingFocus('bond-practice'); assert( dedupedReserveDrill.reserveTrainingFocus === 'bond-practice' && dedupedReserveDrill.reserveTrainingAssignments['liu-bei'] === undefined && dedupedReserveDrill.reserveTrainingAssignments['guan-yu'] === 'class-practice', `Expected global reserve drill focus changes to remove redundant individual assignments: ${JSON.stringify(dedupedReserveDrill.reserveTrainingAssignments)}` ); resetCampaignState(); const firstScenario = battleScenarios['first-battle-zhuo-commandery']; const alliedFirstBattleUnits = firstScenario.units.filter((unit) => unit.faction === 'ally'); const firstScenarioUnlocks = firstScenario.campaignReward?.unlockBattleId ? [ { battleId: firstScenario.campaignReward.unlockBattleId, title: firstScenario.campaignReward.unlockLabel ?? battleScenarios[firstScenario.campaignReward.unlockBattleId].title } ] : []; const firstBattleReport = { battleId: firstScenario.id, battleTitle: firstScenario.title, outcome: 'victory', turnNumber: 5, rewardGold: 100, defeatedEnemies: 2, totalEnemies: 4, objectives: [], units: alliedFirstBattleUnits, bonds: firstScenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), itemRewards: ['Bean x2', 'Iron Sword 1'], campaignRewards: { supplies: ['Bean x2'], equipment: ['Iron Sword 1'], reputation: [], recruits: [], unlocks: firstScenarioUnlocks }, completedCampDialogues: [], completedCampVisits: [], createdAt: '2026-07-03T09:00:00.000Z' }; const performanceUnit = alliedFirstBattleUnits.find((unit) => unit.id === 'liu-bei') ?? alliedFirstBattleUnits[0]; assert(performanceUnit, 'Expected an allied first-battle unit for sortie performance clone checks'); const performanceReport = { ...firstBattleReport, sortiePerformance: { selectedUnitIds: [performanceUnit.id], formationAssignments: { [performanceUnit.id]: 'front' }, unitStats: [sortieStatsFixture(performanceUnit.id, { hpBefore: performanceUnit.maxHp, maxHpBefore: performanceUnit.maxHp, damageDealt: 24 })] }, sortieReview: { version: 1, score: 88, grade: 'A', activeBondCount: 2, enemyCount: 4, sourcePresetIds: ['elite', 'mobile'] }, createdAt: '2026-07-03T09:05:00.000Z' }; resetCampaignState(); setFirstBattleReport(performanceReport); performanceReport.sortiePerformance.selectedUnitIds.push('ghost-unit'); performanceReport.sortiePerformance.formationAssignments[performanceUnit.id] = 'support'; performanceReport.sortiePerformance.unitStats[0].damageDealt = 999; performanceReport.sortieReview.score = 1; performanceReport.sortieReview.grade = 'D'; performanceReport.sortieReview.sourcePresetIds.push('siege'); const clonedPerformanceSettlement = getCampaignState(); const clonedReportPerformance = clonedPerformanceSettlement.firstBattleReport?.sortiePerformance; const clonedHistoryPerformance = clonedPerformanceSettlement.battleHistory[firstScenario.id]?.sortiePerformance; const clonedReportReview = clonedPerformanceSettlement.firstBattleReport?.sortieReview; const clonedHistoryReview = clonedPerformanceSettlement.battleHistory[firstScenario.id]?.sortieReview; assert( JSON.stringify(clonedReportPerformance?.selectedUnitIds) === JSON.stringify([performanceUnit.id]) && clonedReportPerformance.formationAssignments[performanceUnit.id] === 'front' && clonedReportPerformance.unitStats[0].damageDealt === 24 && JSON.stringify(clonedHistoryPerformance?.selectedUnitIds) === JSON.stringify([performanceUnit.id]) && clonedHistoryPerformance.formationAssignments[performanceUnit.id] === 'front' && clonedHistoryPerformance.unitStats[0].damageDealt === 24 && clonedReportReview?.score === 88 && clonedReportReview.grade === 'A' && JSON.stringify(clonedReportReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile']) && clonedHistoryReview?.score === 88 && clonedHistoryReview.grade === 'A' && JSON.stringify(clonedHistoryReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile']), `Expected battle report settlement to deep-clone nested sortie performance and review input: ${JSON.stringify(clonedPerformanceSettlement)}` ); clonedReportPerformance.selectedUnitIds.push('detached-unit'); clonedReportPerformance.formationAssignments[performanceUnit.id] = 'reserve'; clonedReportPerformance.unitStats[0].damageDealt = 777; clonedReportReview.score = 2; clonedReportReview.grade = 'D'; clonedReportReview.sourcePresetIds.push('siege'); const unmutatedPerformanceSettlement = getCampaignState(); assert( clonedHistoryPerformance.unitStats[0].damageDealt === 24 && clonedHistoryReview.score === 88 && clonedHistoryReview.grade === 'A' && JSON.stringify(clonedHistoryReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile']) && JSON.stringify(unmutatedPerformanceSettlement.firstBattleReport?.sortiePerformance?.selectedUnitIds) === JSON.stringify([performanceUnit.id]) && unmutatedPerformanceSettlement.firstBattleReport?.sortiePerformance?.formationAssignments[performanceUnit.id] === 'front' && unmutatedPerformanceSettlement.firstBattleReport?.sortiePerformance?.unitStats[0].damageDealt === 24 && unmutatedPerformanceSettlement.battleHistory[firstScenario.id]?.sortiePerformance?.unitStats[0].damageDealt === 24 && unmutatedPerformanceSettlement.firstBattleReport?.sortieReview?.score === 88 && unmutatedPerformanceSettlement.firstBattleReport?.sortieReview?.grade === 'A' && JSON.stringify(unmutatedPerformanceSettlement.firstBattleReport?.sortieReview?.sourcePresetIds) === JSON.stringify(['elite', 'mobile']) && unmutatedPerformanceSettlement.battleHistory[firstScenario.id]?.sortieReview?.score === 88 && JSON.stringify(unmutatedPerformanceSettlement.battleHistory[firstScenario.id]?.sortieReview?.sourcePresetIds) === JSON.stringify(['elite', 'mobile']), `Expected returned report, history, and persisted sortie performance/review snapshots to remain deeply detached: ${JSON.stringify(unmutatedPerformanceSettlement)}` ); resetCampaignState(); const reserveDrillTemplate = campaignRecruitUnits.find((unit) => unit.id === 'jian-yong'); assert(reserveDrillTemplate, 'Expected Jian Yong recruit template to exist for reserve drill assignment checks'); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T12:10:00.000Z', step: 'first-camp', roster: [reserveDrillTemplate], reserveTrainingFocus: 'balanced', reserveTrainingAssignments: { [reserveDrillTemplate.id]: 'bond-practice' } }) ); loadCampaignState(); setFirstBattleReport({ ...firstBattleReport, bonds: [ ...firstBattleReport.bonds, { id: 'reserve-drill-bond', unitIds: [reserveDrillTemplate.id, 'liu-bei'], title: 'Reserve Drill Bond', level: 1, exp: 0, battleExp: 0, description: 'Reserve drill verification bond' } ] }); const individualizedReserveTraining = getCampaignState(); const individualizedReserveAward = individualizedReserveTraining.battleHistory[firstScenario.id]?.reserveTraining?.find( (entry) => entry.unitId === reserveDrillTemplate.id ); const individualizedReserveBond = individualizedReserveTraining.bonds.find((bond) => bond.id === 'reserve-drill-bond'); assert( individualizedReserveAward?.focusId === 'bond-practice' && individualizedReserveAward.expGained === 5 && individualizedReserveAward.equipmentExpGained === 1 && individualizedReserveAward.bondExpGained === 4 && individualizedReserveBond?.exp === 4, `Expected per-officer reserve drill assignments to override the global focus and apply bond growth during battle settlement: ${JSON.stringify(individualizedReserveTraining)}` ); resetCampaignState(); setFirstBattleReport(firstBattleReport); setFirstBattleReport(firstBattleReport); const repeatedSettlement = getCampaignState(); assert( repeatedSettlement.gold === 100 && repeatedSettlement.inventory.Bean === 2 && repeatedSettlement.inventory['Iron Sword'] === 1 && repeatedSettlement.battleHistory[firstScenario.id]?.rewardGold === 100, `Expected repeated battle report settlement to avoid duplicate gold/items: ${JSON.stringify(repeatedSettlement)}` ); assert( repeatedSettlement.firstBattleReport?.sortiePerformance === undefined && repeatedSettlement.battleHistory[firstScenario.id]?.sortiePerformance === undefined && repeatedSettlement.firstBattleReport?.sortieReview === undefined && repeatedSettlement.battleHistory[firstScenario.id]?.sortieReview === undefined, `Expected legacy reports without sortie performance/review to remain compatible in both latest report and history: ${JSON.stringify(repeatedSettlement)}` ); assert( repeatedSettlement.firstBattleReport?.campaignRewards?.unlocks[0]?.battleId === 'second-battle-yellow-turban-pursuit' && repeatedSettlement.firstBattleReport.campaignRewards.unlocks[0].title === firstScenarioUnlocks[0]?.title && repeatedSettlement.battleHistory[firstScenario.id]?.campaignRewards?.unlocks[0]?.battleId === 'second-battle-yellow-turban-pursuit', `Expected campaign unlock rewards to survive repeated battle report settlement: ${JSON.stringify(repeatedSettlement)}` ); setFirstBattleReport({ ...firstBattleReport, rewardGold: 140, itemRewards: ['Bean x3', 'Wine', 'Iron Armor 1'], campaignRewards: { supplies: ['Bean x3', 'Wine'], equipment: ['Iron Armor 1'], reputation: [], recruits: [], unlocks: firstScenarioUnlocks } }); const revisedSettlement = getCampaignState(); assert( revisedSettlement.gold === 140 && revisedSettlement.inventory.Bean === 3 && revisedSettlement.inventory.Wine === 1 && revisedSettlement.inventory['Iron Sword'] === undefined && revisedSettlement.inventory['Iron Armor'] === 1, `Expected revised battle report settlement to replace previous rewards instead of stacking: ${JSON.stringify(revisedSettlement)}` ); applyCampVisitReward('repeat-visit', { gold: 10, itemRewards: ['Bean x2'] }); applyCampVisitReward('repeat-visit', { gold: 10, itemRewards: ['Bean x2'] }); const repeatedVisit = getCampaignState(); assert( repeatedVisit.gold === 150 && repeatedVisit.inventory.Bean === 5 && repeatedVisit.completedCampVisits.includes('repeat-visit') && repeatedVisit.firstBattleReport?.completedCampVisits.includes('repeat-visit'), `Expected repeated camp visit rewards to be applied once: ${JSON.stringify(repeatedVisit)}` ); const visitDesynced = getCampaignState(); visitDesynced.completedCampVisits = []; visitDesynced.firstBattleReport.completedCampVisits = ['repeat-visit']; setCampaignState(visitDesynced); applyCampVisitReward('repeat-visit', { gold: 10, itemRewards: ['Bean x2'] }); const visitResynced = getCampaignState(); assert( visitResynced.gold === 150 && visitResynced.inventory.Bean === 5 && visitResynced.completedCampVisits.includes('repeat-visit') && visitResynced.firstBattleReport?.completedCampVisits.includes('repeat-visit'), `Expected report-only camp visit completion to resync without duplicate rewards: ${JSON.stringify(visitResynced)}` ); applyCampVisitReward('dirty-reward-visit', { itemRewards: ['', ' ', 'Bean -2', 'Bean +0', 'Bean x2'] }); const dirtyRewardVisit = getCampaignState(); assert( dirtyRewardVisit.inventory.Bean === 7 && dirtyRewardVisit.inventory[''] === undefined && dirtyRewardVisit.inventory['Bean -'] === undefined && dirtyRewardVisit.completedCampVisits.includes('dirty-reward-visit'), `Expected malformed reward labels to be ignored while valid rewards apply: ${JSON.stringify(dirtyRewardVisit)}` ); const repeatBondId = firstScenario.bonds[0].id; applyCampBondExp('repeat-dialogue', repeatBondId, 10); const repeatedDialogueOnce = getCampaignState(); applyCampBondExp('repeat-dialogue', repeatBondId, 10); const repeatedDialogueTwice = getCampaignState(); const onceBond = repeatedDialogueOnce.bonds.find((bond) => bond.id === repeatBondId); const twiceBond = repeatedDialogueTwice.bonds.find((bond) => bond.id === repeatBondId); assert( onceBond?.exp === twiceBond?.exp && onceBond?.battleExp === twiceBond?.battleExp && repeatedDialogueTwice.completedCampDialogues.includes('repeat-dialogue') && repeatedDialogueTwice.firstBattleReport?.completedCampDialogues.includes('repeat-dialogue'), `Expected repeated camp dialogue rewards to be applied once: ${JSON.stringify(repeatedDialogueTwice)}` ); const dialogueDesynced = getCampaignState(); dialogueDesynced.completedCampDialogues = []; dialogueDesynced.firstBattleReport.completedCampDialogues = ['repeat-dialogue']; setCampaignState(dialogueDesynced); applyCampBondExp('repeat-dialogue', repeatBondId, 10); const dialogueResynced = getCampaignState(); const resyncedBond = dialogueResynced.bonds.find((bond) => bond.id === repeatBondId); assert( resyncedBond?.exp === twiceBond?.exp && resyncedBond?.battleExp === twiceBond?.battleExp && dialogueResynced.completedCampDialogues.includes('repeat-dialogue') && dialogueResynced.firstBattleReport?.completedCampDialogues.includes('repeat-dialogue'), `Expected report-only camp dialogue completion to resync without duplicate bond exp: ${JSON.stringify(dialogueResynced)}` ); const maxedBondState = getCampaignState(); const maxedCampaignBond = maxedBondState.bonds.find((bond) => bond.id === repeatBondId); const maxedReportBond = maxedBondState.firstBattleReport?.bonds.find((bond) => bond.id === repeatBondId); assert(maxedCampaignBond && maxedReportBond, `Expected repeat bond to exist before max-level checks: ${JSON.stringify(maxedBondState)}`); maxedCampaignBond.level = 100; maxedCampaignBond.exp = 95; maxedCampaignBond.battleExp = 3; maxedReportBond.level = 100; maxedReportBond.exp = 95; maxedReportBond.battleExp = 3; maxedBondState.completedCampDialogues = maxedBondState.completedCampDialogues.filter((id) => id !== 'maxed-dialogue'); maxedBondState.firstBattleReport.completedCampDialogues = maxedBondState.firstBattleReport.completedCampDialogues.filter((id) => id !== 'maxed-dialogue'); maxedBondState.completedCampVisits = maxedBondState.completedCampVisits.filter((id) => id !== 'maxed-visit'); maxedBondState.firstBattleReport.completedCampVisits = maxedBondState.firstBattleReport.completedCampVisits.filter((id) => id !== 'maxed-visit'); setCampaignState(maxedBondState); applyCampBondExp('maxed-dialogue', repeatBondId, 10); const maxedDialogueState = getCampaignState(); const maxedDialogueBond = maxedDialogueState.bonds.find((bond) => bond.id === repeatBondId); const maxedDialogueReportBond = maxedDialogueState.firstBattleReport?.bonds.find((bond) => bond.id === repeatBondId); assert( maxedDialogueBond?.level === 100 && maxedDialogueBond.exp === 100 && maxedDialogueBond.battleExp === 13 && maxedDialogueReportBond?.exp === 100 && maxedDialogueReportBond.battleExp === 13, `Expected max-level dialogue bond exp to cap instead of wrapping: ${JSON.stringify(maxedDialogueState)}` ); applyCampVisitReward('maxed-visit', { bondId: repeatBondId, bondExp: 10 }); const maxedVisitState = getCampaignState(); const maxedVisitBond = maxedVisitState.bonds.find((bond) => bond.id === repeatBondId); const maxedVisitReportBond = maxedVisitState.firstBattleReport?.bonds.find((bond) => bond.id === repeatBondId); assert( maxedVisitBond?.level === 100 && maxedVisitBond.exp === 100 && maxedVisitBond.battleExp === 23 && maxedVisitReportBond?.exp === 100 && maxedVisitReportBond.battleExp === 23, `Expected max-level visit bond exp to cap instead of wrapping: ${JSON.stringify(maxedVisitState)}` ); resetCampaignState(); const seventhScenario = battleScenarios['seventh-battle-xuzhou-rescue']; const seventhAlliedUnits = seventhScenario.units.filter((unit) => unit.faction === 'ally'); const campaignRecruitNameById = new Map(campaignRecruitUnits.map((unit) => [unit.id, unit.name])); const seventhRecruitRewards = (seventhScenario.campaignReward?.recruits ?? []).map((unitId) => ({ unitId, name: campaignRecruitNameById.get(unitId) ?? unitId })); const seventhEquipmentRewards = seventhScenario.campaignReward?.equipment ?? []; const seventhBattleReport = { battleId: seventhScenario.id, battleTitle: seventhScenario.title, outcome: 'victory', turnNumber: 7, rewardGold: 180, defeatedEnemies: 5, totalEnemies: 8, objectives: [], units: seventhAlliedUnits, bonds: seventhScenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), itemRewards: seventhEquipmentRewards, campaignRewards: { supplies: [], equipment: seventhEquipmentRewards, reputation: [], recruits: seventhRecruitRewards, unlocks: [] }, completedCampDialogues: [], completedCampVisits: [], createdAt: '2026-07-03T11:00:00.000Z' }; setFirstBattleReport(seventhBattleReport); setFirstBattleReport(seventhBattleReport); const repeatedRecruitSettlement = getCampaignState(); const repeatedRecruitRosterIds = repeatedRecruitSettlement.roster.map((unit) => unit.id); assert( seventhRecruitRewards.every((recruit) => repeatedRecruitRosterIds.includes(recruit.unitId)) && seventhRecruitRewards.every((recruit) => repeatedRecruitRosterIds.filter((unitId) => unitId === recruit.unitId).length === 1) && repeatedRecruitSettlement.battleHistory[seventhScenario.id]?.campaignRewards?.recruits.length === seventhRecruitRewards.length, `Expected campaign recruit rewards to merge from templates once during repeated settlement: ${JSON.stringify(repeatedRecruitSettlement)}` ); const orderPerformance = { selectedUnitIds: [performanceUnit.id], formationAssignments: { [performanceUnit.id]: 'front' }, unitStats: [sortieStatsFixture(performanceUnit.id, { hpBefore: performanceUnit.maxHp, maxHpBefore: performanceUnit.maxHp, damageDealt: 24, sortiePreventedDamage: 4 })] }; const orderReview = { version: 1, score: 88, grade: 'A', activeBondCount: 1, enemyCount: 4, sourcePresetIds: ['elite'] }; const successfulEliteOrder = { version: 1, orderId: 'elite', score: 88, achieved: true, rewardGranted: false, progress: [ { id: 'victory', achieved: true, value: 1, target: 1 }, { id: 'elite-grade', achieved: true, value: 88, target: 75 }, { id: 'elite-survival', achieved: true, value: 1, target: 1 } ], command: { source: 'sortie-order', role: 'front', actorId: performanceUnit.id, usedTurn: 3, effectPending: false, effectValue: 8, statusesCleared: 0, effectLost: false } }; const successfulMobileOrder = { version: 1, orderId: 'mobile', score: 88, achieved: true, rewardGranted: false, progress: [ { id: 'victory', achieved: true, value: 1, target: 1 }, { id: 'mobile-quick', achieved: true, value: 5, target: 15 }, { id: 'mobile-breakthrough', achieved: true, value: 24, target: 1 } ] }; const orderBattleReport = { ...firstBattleReport, sortiePerformance: orderPerformance, sortieReview: orderReview, sortieOrder: successfulEliteOrder, createdAt: '2026-07-03T11:20:00.000Z' }; const orderBattleReplay = JSON.parse(JSON.stringify(orderBattleReport)); resetCampaignState(); setCampaignSortieOrderSelection(firstScenario.id, 'mobile'); setFirstBattleReport({ ...orderBattleReplay, outcome: 'defeat', rewardGold: 0, itemRewards: [], sortieOrder: { ...successfulMobileOrder, achieved: false, progress: successfulMobileOrder.progress.map((entry) => entry.id === 'victory' ? { ...entry, achieved: false, value: 0 } : { ...entry }) }, createdAt: '2026-07-03T11:19:00.000Z' }); const defeatedOrderSettlement = getCampaignState(); assert( JSON.stringify(defeatedOrderSettlement.sortieOrderSelection) === JSON.stringify({ battleId: firstScenario.id, orderId: 'mobile' }) && defeatedOrderSettlement.sortieOrderHistory[firstScenario.id]?.mobile?.achieved === false && defeatedOrderSettlement.claimedSortieOrderRewardIds.length === 0 && defeatedOrderSettlement.gold === 0 && defeatedOrderSettlement.battleHistory[firstScenario.id] === undefined, `Expected defeat to record the failed order without granting rewards or clearing the retry-scoped selection: ${JSON.stringify(defeatedOrderSettlement)}` ); resetCampaignState(); setCampaignSortieOrderSelection(firstScenario.id, 'elite'); const firstOrderCanonicalReport = setFirstBattleReport(orderBattleReport); const firstOrderSettlement = getCampaignState(); assert( firstOrderCanonicalReport?.sortieOrder?.rewardGranted === true && firstOrderSettlement.gold === 280 && firstOrderSettlement.inventory.Bean === 2 && firstOrderSettlement.inventory['Iron Sword'] === 1 && firstOrderSettlement.inventory['상처약'] === 1 && firstOrderSettlement.claimedSortieOrderRewardIds.length === 1 && firstOrderSettlement.claimedSortieOrderRewardIds[0] === `${firstScenario.id}:elite` && firstOrderSettlement.sortieOrderSelection === undefined && firstOrderSettlement.firstBattleReport?.sortieOrder?.rewardGranted === true && firstOrderSettlement.firstBattleReport.sortieOrder.command?.source === 'sortie-order' && firstOrderSettlement.battleHistory[firstScenario.id]?.sortieOrder?.rewardGranted === true && firstOrderSettlement.battleHistory[firstScenario.id].sortieOrder.command?.effectValue === 8 && firstOrderSettlement.sortieOrderHistory[firstScenario.id]?.elite?.rewardGranted === true, `Expected the first battle/order success to grant its independent reward once, persist every snapshot, and clear the matching selection: ${JSON.stringify(firstOrderSettlement)}` ); const reloadedFirstOrderSettlement = loadCampaignState(); assert( reloadedFirstOrderSettlement.firstBattleReport?.sortieOrder?.achieved === true && reloadedFirstOrderSettlement.firstBattleReport.sortieOrder.rewardGranted === true && reloadedFirstOrderSettlement.firstBattleReport.sortieOrder.command?.actorId === performanceUnit.id && reloadedFirstOrderSettlement.battleHistory[firstScenario.id]?.sortieOrder?.achieved === true && reloadedFirstOrderSettlement.battleHistory[firstScenario.id].turnNumber === orderBattleReport.turnNumber && reloadedFirstOrderSettlement.battleHistory[firstScenario.id].sortieOrder.rewardGranted === true && reloadedFirstOrderSettlement.battleHistory[firstScenario.id].sortieOrder.command?.effectValue === 8 && reloadedFirstOrderSettlement.sortieOrderHistory[firstScenario.id]?.elite?.achieved === true && reloadedFirstOrderSettlement.sortieOrderHistory[firstScenario.id].elite.command?.source === 'sortie-order' && reloadedFirstOrderSettlement.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:elite`), `Expected report, settlement, independent history, and reward claim to survive save normalization together: ${JSON.stringify(reloadedFirstOrderSettlement)}` ); const validContextualCommandStateRaw = storage.get(campaignStorageKey); assert(validContextualCommandStateRaw, 'Expected a persisted command state for contextual corruption checks.'); const corruptedContextualCommandState = JSON.parse(validContextualCommandStateRaw); corruptedContextualCommandState.firstBattleReport.sortieOrder.command.actorId = 'ghost-unit'; corruptedContextualCommandState.battleHistory[firstScenario.id].sortieOrder.command.role = 'support'; corruptedContextualCommandState.sortieOrderHistory[firstScenario.id].elite.command.usedTurn = orderBattleReport.turnNumber + 1; storage.set(campaignStorageKey, JSON.stringify(corruptedContextualCommandState)); const normalizedContextualCommandState = loadCampaignState(); assert( normalizedContextualCommandState.firstBattleReport?.sortieOrder?.command === undefined && normalizedContextualCommandState.battleHistory[firstScenario.id]?.sortieOrder?.command === undefined && normalizedContextualCommandState.sortieOrderHistory[firstScenario.id]?.elite?.command === undefined && normalizedContextualCommandState.firstBattleReport?.sortieOrder?.achieved === true && normalizedContextualCommandState.battleHistory[firstScenario.id]?.sortieOrder?.achieved === true && normalizedContextualCommandState.sortieOrderHistory[firstScenario.id]?.elite?.achieved === true, `Expected contextual normalization to remove ghost actors, role mismatches, and future command turns without dropping their order results: ${JSON.stringify(normalizedContextualCommandState)}` ); storage.set(campaignStorageKey, validContextualCommandStateRaw); loadCampaignState(); orderBattleReport.sortieOrder.progress[0].achieved = false; orderBattleReport.sortieOrder.command.effectValue = 999; firstOrderCanonicalReport.sortieOrder.progress[1].achieved = false; firstOrderCanonicalReport.sortieOrder.command.actorId = 'ghost-unit'; const deepCopyOrderState = getCampaignState(); const detachedOrderState = getCampaignState(); detachedOrderState.firstBattleReport.sortieOrder.progress[2].achieved = false; detachedOrderState.firstBattleReport.sortieOrder.command.effectLost = true; assert( deepCopyOrderState.firstBattleReport?.sortieOrder?.progress.every((entry) => entry.achieved) && deepCopyOrderState.firstBattleReport.sortieOrder.command?.effectValue === 8 && deepCopyOrderState.firstBattleReport.sortieOrder.command.actorId === performanceUnit.id && deepCopyOrderState.battleHistory[firstScenario.id]?.sortieOrder?.progress.every((entry) => entry.achieved) && deepCopyOrderState.battleHistory[firstScenario.id].sortieOrder.command?.effectLost === false && deepCopyOrderState.sortieOrderHistory[firstScenario.id]?.elite?.progress.every((entry) => entry.achieved) && deepCopyOrderState.sortieOrderHistory[firstScenario.id].elite.command?.effectValue === 8 && detachedOrderState.battleHistory[firstScenario.id]?.sortieOrder?.progress[2].achieved === true && detachedOrderState.battleHistory[firstScenario.id].sortieOrder.command?.effectLost === false && getCampaignState().firstBattleReport?.sortieOrder?.progress[2].achieved === true, `Expected input, returned report, report/history, and returned campaign sortie-order progress to be deeply detached: ${JSON.stringify(deepCopyOrderState)}` ); const repeatedOrderCanonicalReport = setFirstBattleReport(orderBattleReplay); const repeatedOrderSettlement = getCampaignState(); assert( repeatedOrderCanonicalReport?.sortieOrder?.rewardGranted === false && repeatedOrderSettlement.gold === 280 && repeatedOrderSettlement.inventory['상처약'] === 1 && repeatedOrderSettlement.claimedSortieOrderRewardIds.length === 1, `Expected repeated settlement of the same battle/order to avoid duplicate gold and supplies: ${JSON.stringify(repeatedOrderSettlement)}` ); const revisedOrderReport = { ...orderBattleReplay, rewardGold: 140, itemRewards: ['Bean x3', 'Wine', 'Iron Armor 1'], campaignRewards: { supplies: ['Bean x3', 'Wine'], equipment: ['Iron Armor 1'], reputation: [], recruits: [], unlocks: firstScenarioUnlocks }, createdAt: '2026-07-03T11:21:00.000Z' }; setFirstBattleReport(revisedOrderReport); const revisedOrderSettlement = getCampaignState(); assert( revisedOrderSettlement.gold === 320 && revisedOrderSettlement.inventory.Bean === 3 && revisedOrderSettlement.inventory.Wine === 1 && revisedOrderSettlement.inventory['Iron Sword'] === undefined && revisedOrderSettlement.inventory['Iron Armor'] === 1 && revisedOrderSettlement.inventory['상처약'] === 1 && revisedOrderSettlement.claimedSortieOrderRewardIds.length === 1, `Expected ordinary reward replacement to preserve the separately claimed sortie-order reward: ${JSON.stringify(revisedOrderSettlement)}` ); setFirstBattleReport({ ...revisedOrderReport, sortieOrder: successfulMobileOrder, createdAt: '2026-07-03T11:22:00.000Z' }); const secondOrderSameBattle = getCampaignState(); assert( secondOrderSameBattle.gold === 480 && secondOrderSameBattle.inventory['상처약'] === 1 && secondOrderSameBattle.inventory['탁주'] === 1 && secondOrderSameBattle.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:elite`) && secondOrderSameBattle.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:mobile`) && secondOrderSameBattle.sortieOrderHistory[firstScenario.id]?.elite?.achieved === true && secondOrderSameBattle.sortieOrderHistory[firstScenario.id]?.mobile?.achieved === true, `Expected a different order on the same battle to own an independent first reward and history cell: ${JSON.stringify(secondOrderSameBattle)}` ); const failedEliteOrder = { ...successfulEliteOrder, achieved: false, progress: successfulEliteOrder.progress.map((entry) => entry.id === 'elite-survival' ? { ...entry, achieved: false, value: 0 } : { ...entry }) }; setFirstBattleReport({ ...revisedOrderReport, sortieOrder: failedEliteOrder, createdAt: '2026-07-03T11:23:00.000Z' }); const failedEliteReplay = getCampaignState(); assert( failedEliteReplay.gold === 480 && failedEliteReplay.inventory['상처약'] === 1 && failedEliteReplay.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:elite`) && failedEliteReplay.firstBattleReport?.sortieOrder?.achieved === false && failedEliteReplay.battleHistory[firstScenario.id]?.sortieOrder?.achieved === false && failedEliteReplay.sortieOrderHistory[firstScenario.id]?.elite?.achieved === true && failedEliteReplay.sortieOrderHistory[firstScenario.id]?.elite?.score === 88 && failedEliteReplay.sortieOrderHistory[firstScenario.id]?.mobile?.achieved === true, `Expected a failed retry to remain current in report/settlement while preserving the best earned history and first reward: ${JSON.stringify(failedEliteReplay)}` ); const secondScenario = battleScenarios['second-battle-yellow-turban-pursuit']; setCampaignSortieOrderSelection(secondScenario.id, 'elite'); setFirstBattleReport({ ...orderBattleReplay, battleId: secondScenario.id, battleTitle: secondScenario.title, rewardGold: 120, itemRewards: [], campaignRewards: { supplies: [], equipment: [], reputation: [], recruits: [], unlocks: [] }, units: secondScenario.units.filter((unit) => unit.faction === 'ally'), bonds: secondScenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), sortieOrder: orderBattleReplay.sortieOrder, createdAt: '2026-07-03T11:24:00.000Z' }); const sameOrderDifferentBattle = getCampaignState(); assert( sameOrderDifferentBattle.gold === 780 && sameOrderDifferentBattle.inventory['상처약'] === 2 && sameOrderDifferentBattle.claimedSortieOrderRewardIds.includes(`${secondScenario.id}:elite`) && sameOrderDifferentBattle.claimedSortieOrderRewardIds.length === 3 && sameOrderDifferentBattle.sortieOrderSelection === undefined, `Expected the same order on a different battle to receive its own first reward exactly once: ${JSON.stringify(sameOrderDifferentBattle)}` ); resetCampaignState(); setFirstBattleReport({ ...orderBattleReplay, rewardGold: 930, itemRewards: ['탁주 1'], sortieOrder: undefined, createdAt: '2026-07-03T11:25:00.000Z' }); const spentPreviousRewards = getCampaignState(); spentPreviousRewards.gold = 0; delete spentPreviousRewards.inventory['탁주']; setCampaignState(spentPreviousRewards); setFirstBattleReport({ ...orderBattleReplay, rewardGold: 600, itemRewards: ['탁주 1'], sortieOrder: successfulMobileOrder, createdAt: '2026-07-03T11:26:00.000Z' }); const spentRewardReplay = getCampaignState(); assert( spentRewardReplay.gold === 160 && spentRewardReplay.inventory['탁주'] === 2 && spentRewardReplay.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:mobile`) && spentRewardReplay.firstBattleReport?.sortieOrder?.rewardGranted === true, `Expected a first order reward to remain intact even when previously settled ordinary gold/items were already spent: ${JSON.stringify(spentRewardReplay)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ updatedAt: '2026-07-01T00:00:00.000Z', step: 'first-camp', activeSaveSlot: 1, gold: 180, inventory: { bean: 2 }, completedCampDialogues: ['bond-a', 'bond-a'], completedCampVisits: ['visit-a', 'visit-a'] }) ); const legacy = loadCampaignState(); assert(legacy.version === 1, `Expected legacy save without version to normalize to v1: ${JSON.stringify(legacy)}`); assert(legacy.step === 'first-camp' && legacy.gold === 180, `Expected legacy save progress to survive: ${JSON.stringify(legacy)}`); assert(Array.isArray(legacy.roster) && Array.isArray(legacy.bonds), `Expected legacy save arrays to be restored: ${JSON.stringify(legacy)}`); assert( legacy.completedCampDialogues.length === 1 && legacy.completedCampVisits.length === 1, `Expected legacy save duplicate completion flags to be deduped: ${JSON.stringify(legacy)}` ); assert( Array.isArray(legacy.selectedSortieUnitIds) && typeof legacy.sortieFormationAssignments === 'object' && typeof legacy.sortieItemAssignments === 'object' && typeof legacy.sortieFormationPresets === 'object' && Object.keys(legacy.sortieFormationPresets).length === 0 && legacy.sortieOrderSelection === undefined && typeof legacy.sortieOrderHistory === 'object' && Object.keys(legacy.sortieOrderHistory).length === 0 && Array.isArray(legacy.claimedSortieOrderRewardIds) && legacy.claimedSortieOrderRewardIds.length === 0 && typeof legacy.reserveTrainingAssignments === 'object', `Expected modern sortie fields to be restored: ${JSON.stringify(legacy)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '', step: 'third-camp', activeSaveSlot: '2', gold: '1000000', roster: { corrupted: true }, bonds: [ { id: 'broken-bond', title: 'Broken Bond', unitIds: { corrupted: true }, level: 3, exp: 12, battleExp: 2 }, { id: 'self-bond', title: 'Self Bond', unitIds: ['liu-bei', 'liu-bei'], level: 2, exp: 12, battleExp: 2 }, { id: 'zero-bond', title: 'Zero Bond', unitIds: ['liu-bei', 'guan-yu'], level: 0, exp: '999', battleExp: 2 }, { id: 'oath-bond', title: 'Oath Bond', unitIds: ['liu-bei', 'guan-yu'], level: '2', exp: '999', battleExp: '5' }, 9 ], completedCampDialogues: ['dialogue-a', 17, 'dialogue-a', '', 'x'.repeat(97)], completedCampVisits: 'visit-a', inventory: { bean: '3', ' bean ': '4', ['\uCF69']: '200', [' \uCF69 ']: '20', rareSupply: '2000', ['x'.repeat(97)]: 5, wine: -2, empty: 0 }, selectedSortieUnitIds: { corrupted: true }, sortieFormationAssignments: 'front', sortieFormationPresets: 'elite', reserveTrainingFocus: 'ghost-focus', reserveTrainingAssignments: { 'liu-bei': 'bond-practice', 'guan-yu': 'unknown-focus', ['x'.repeat(97)]: 'balanced', '': 'balanced' }, sortieItemAssignments: { 'liu-bei': { ' bean ': '5', wine: -1, salve: '1', stone: '9', ['x'.repeat(97)]: '1' }, 'guan-yu': '2', 'zhang-fei': ['bean', '2'], ['x'.repeat(97)]: { bean: 1 }, '': { bean: 4 } }, battleHistory: [] }) ); const malformed = loadCampaignState(); assert(malformed.step === 'third-camp', `Expected malformed save progress to survive: ${JSON.stringify(malformed)}`); assert(malformed.activeSaveSlot === 2 && malformed.gold === 999999, `Expected malformed numeric fields to normalize and clamp: ${JSON.stringify(malformed)}`); assert(Array.isArray(malformed.roster) && malformed.roster.length === 0, `Expected malformed roster to reset to an array: ${JSON.stringify(malformed)}`); assert( Array.isArray(malformed.bonds) && malformed.bonds.length === 2 && malformed.bonds.some((bond) => bond.id === 'zero-bond' && bond.level === 1 && bond.exp === 99 && bond.unitIds.length === 2) && malformed.bonds.some((bond) => bond.id === 'oath-bond' && bond.level === 2 && bond.exp === 99 && bond.unitIds.length === 2), `Expected malformed bond entries to be filtered while valid bonds normalize: ${JSON.stringify(malformed)}` ); assert( malformed.completedCampDialogues.length === 1 && malformed.completedCampDialogues[0] === 'dialogue-a' && malformed.completedCampVisits.length === 0, `Expected malformed completion lists to keep unique strings only: ${JSON.stringify(malformed)}` ); assert( malformed.inventory.bean === 7 && malformed.inventory['\uCF69'] === 120 && malformed.inventory.rareSupply === 999 && malformed.inventory[' bean '] === undefined && malformed.inventory[' \uCF69 '] === undefined && malformed.inventory['x'.repeat(97)] === undefined && malformed.inventory.wine === undefined && malformed.sortieItemAssignments['liu-bei']?.bean === 2 && malformed.sortieItemAssignments['liu-bei']?.salve === 1 && malformed.sortieItemAssignments['liu-bei']?.stone === undefined && malformed.sortieItemAssignments['liu-bei']?.wine === undefined && malformed.sortieItemAssignments['liu-bei']?.['x'.repeat(97)] === undefined && malformed.sortieItemAssignments['guan-yu'] === undefined && malformed.sortieItemAssignments['zhang-fei'] === undefined && malformed.sortieItemAssignments['x'.repeat(97)] === undefined && Object.keys(malformed.sortieItemAssignments).length === 1 && Object.keys(malformed.sortieFormationAssignments).length === 0 && Object.keys(malformed.sortieFormationPresets).length === 0, `Expected malformed inventory and sortie assignments to normalize: ${JSON.stringify(malformed)}` ); assert( malformed.reserveTrainingFocus === 'balanced' && malformed.reserveTrainingAssignments['liu-bei'] === 'bond-practice' && malformed.reserveTrainingAssignments['guan-yu'] === undefined && malformed.reserveTrainingAssignments['x'.repeat(97)] === undefined, `Expected malformed reserve drill focus fields to normalize: ${JSON.stringify(malformed)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T12:30:00.000Z', step: 'third-camp', activeSaveSlot: 1, completedCampDialogues: Array.from({ length: 140 }, (_, index) => `dialogue-${index}`), completedCampVisits: ['visit-a', 'x'.repeat(97), 'visit-a'] }) ); const cappedStringLists = loadCampaignState(); assert( cappedStringLists.completedCampDialogues.length === 128 && cappedStringLists.completedCampDialogues[0] === 'dialogue-0' && cappedStringLists.completedCampDialogues[127] === 'dialogue-127' && cappedStringLists.completedCampVisits.length === 1, `Expected campaign string lists to filter long entries and cap excessive saves: ${JSON.stringify(cappedStringLists)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T12:00:00.000Z', step: 'third-camp', activeSaveSlot: 1, roster: [{ id: 'liu-bei', name: 'Liu Bei', faction: 'ally' }], selectedSortieUnitIds: ['liu-bei', 'ghost-unit', 'guan-yu', 'liu-bei'], sortieFormationAssignments: { 'liu-bei': 'front', 'ghost-unit': 'flank', 'guan-yu': 'support', broken: 'center' }, sortieFormationPresets: { elite: { selectedUnitIds: ['ghost-unit', 'liu-bei', 'guan-yu', 'liu-bei'], formationAssignments: { 'liu-bei': 'front', 'ghost-unit': 'flank', 'guan-yu': 'support' }, sortieItemAssignments: { 'liu-bei': { bean: 2 } } }, mobile: { selectedUnitIds: ['ghost-unit'], formationAssignments: { 'ghost-unit': 'reserve' } } }, reserveTrainingAssignments: { 'liu-bei': 'bond-practice', 'ghost-unit': 'balanced', 'guan-yu': 'class-practice' }, sortieItemAssignments: { 'liu-bei': { bean: '2' }, 'ghost-unit': { bean: '1' }, 'guan-yu': { wine: '1' } } }) ); const staleSortie = loadCampaignState(); assert( staleSortie.selectedSortieUnitIds.length === 1 && staleSortie.selectedSortieUnitIds[0] === 'liu-bei' && staleSortie.sortieFormationAssignments['liu-bei'] === 'front' && staleSortie.sortieFormationAssignments['ghost-unit'] === undefined && JSON.stringify(staleSortie.sortieFormationPresets.elite?.selectedUnitIds) === JSON.stringify(['liu-bei']) && JSON.stringify(staleSortie.sortieFormationPresets.elite?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front' }) && staleSortie.sortieFormationPresets.mobile === undefined && !Object.prototype.hasOwnProperty.call(staleSortie.sortieFormationPresets.elite ?? {}, 'sortieItemAssignments') && staleSortie.reserveTrainingAssignments['liu-bei'] === 'bond-practice' && staleSortie.reserveTrainingAssignments['ghost-unit'] === undefined && staleSortie.reserveTrainingAssignments['guan-yu'] === undefined && staleSortie.sortieItemAssignments['liu-bei']?.bean === 2 && staleSortie.sortieItemAssignments['ghost-unit'] === undefined && staleSortie.sortieItemAssignments['guan-yu'] === undefined, `Expected stale sortie ids to be filtered against the recovered roster: ${JSON.stringify(staleSortie)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T13:00:00.000Z', step: 'third-camp', activeSaveSlot: 1, roster: [ { id: 'liu-bei', name: 'Liu Bei', faction: 'ally' }, { id: 'guan-yu', name: 'Guan Yu', faction: 'ally' } ], bonds: [ { id: 'liu-bei__guan-yu', title: 'Oath Bond', unitIds: ['liu-bei', 'guan-yu'], level: '2', exp: '30', battleExp: '4' }, { id: 'liu-bei__ghost', title: 'Ghost Bond', unitIds: ['liu-bei', 'ghost-unit'], level: '3', exp: '40', battleExp: '5' } ] }) ); const staleBonds = loadCampaignState(); assert( staleBonds.bonds.length === 1 && staleBonds.bonds[0].id === 'liu-bei__guan-yu' && staleBonds.bonds[0].level === 2 && staleBonds.bonds[0].battleExp === 4, `Expected stale bonds to be filtered against the recovered roster while valid bonds survive: ${JSON.stringify(staleBonds)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T13:30:00.000Z', step: 'third-camp', activeSaveSlot: 1, roster: [ { id: 'liu-bei', name: 'Liu Bei', faction: 'ally' }, { id: 'guan-yu', name: 'Guan Yu', faction: 'ally' } ], firstBattleReport: { battleId: 'first-battle-zhuo-commandery', outcome: 'victory', createdAt: '2026-07-03T13:30:00.000Z', units: [{ id: 'rebel-a', name: 'Rebel', faction: 'enemy' }], bonds: [ { id: 'liu-bei__guan-yu', title: 'Oath Bond', unitIds: ['liu-bei', 'guan-yu'], level: 2, exp: 30, battleExp: 4 }, { id: 'liu-bei__ghost', title: 'Ghost Bond', unitIds: ['liu-bei', 'ghost-unit'], level: 3, exp: 40, battleExp: 5 } ], mvp: { unitId: 'ghost-unit', name: 'Ghost', damageDealt: 99, defeats: 4 }, sortiePerformance: { selectedUnitIds: ['liu-bei', 'ghost-unit', 'liu-bei'], formationAssignments: { 'liu-bei': 'front', 'ghost-unit': 'support' }, unitStats: [sortieStatsFixture('liu-bei', { damageDealt: 31 }), sortieStatsFixture('ghost-unit', { damageDealt: 99 })] } } }) ); const staleReportRefs = loadCampaignState(); assert( staleReportRefs.firstBattleReport?.bonds.length === 1 && staleReportRefs.firstBattleReport.bonds[0].id === 'liu-bei__guan-yu' && staleReportRefs.firstBattleReport.mvp === undefined && JSON.stringify(staleReportRefs.firstBattleReport.sortiePerformance?.selectedUnitIds) === JSON.stringify(['liu-bei']) && JSON.stringify(staleReportRefs.firstBattleReport.sortiePerformance?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front' }) && staleReportRefs.firstBattleReport.sortiePerformance?.unitStats.length === 1 && staleReportRefs.firstBattleReport.sortiePerformance.unitStats[0].unitId === 'liu-bei' && staleReportRefs.firstBattleReport.sortiePerformance.unitStats[0].damageDealt === 31, `Expected first battle report bonds, MVP, and sortie performance references to be filtered against the recovered roster: ${JSON.stringify(staleReportRefs)}` ); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-03T14:00:00.000Z', step: 'third-camp', activeSaveSlot: 1, roster: [ { id: 'liu-bei', name: 'Liu Bei', faction: 'ally', className: 'Lord', classKey: 'lost-class', level: '6', exp: '999', hp: '42', maxHp: '45', attack: '22', move: '5', stats: { might: '19', intelligence: '17', leadership: '21', agility: '13', luck: '12' }, equipment: { weapon: { itemId: 'short-bow', level: '3', exp: '999' }, armor: { itemId: 'short-bow', level: '2', exp: '999' }, accessory: { itemId: 'grain-pouch', level: '0', exp: '999' } }, x: '2', y: '3' }, { id: 'guan-yu', name: 'Guan Yu', faction: 'ally', className: 'General', classKey: 'spearman', level: '99', exp: '999', hp: '50', maxHp: '50', attack: '30', move: '5', stats: { might: '24', intelligence: '13', leadership: '22', agility: '14', luck: '11' }, equipment: { weapon: { itemId: 'iron-spear', level: '9', exp: '999' }, armor: { itemId: 'lamellar-armor', level: '9', exp: '999' }, accessory: { itemId: 'grain-pouch', level: '9', exp: '999' } }, x: '3', y: '3' }, { id: 'ghost-unit', name: 'Ghost', faction: 'neutral' } ] }) ); const malformedRoster = loadCampaignState(); const normalizedLiuBei = malformedRoster.roster.find((unit) => unit.id === 'liu-bei'); const normalizedGuanYu = malformedRoster.roster.find((unit) => unit.id === 'guan-yu'); assert( malformedRoster.roster.length === 2 && normalizedLiuBei?.classKey === 'infantry' && normalizedLiuBei.level === 6 && normalizedLiuBei.exp === 99 && normalizedLiuBei.equipment.weapon.itemId === 'short-bow' && normalizedLiuBei.equipment.weapon.level === 3 && normalizedLiuBei.equipment.weapon.exp === 109 && normalizedLiuBei.equipment.armor.itemId === 'cloth-armor' && normalizedLiuBei.equipment.armor.exp === 89 && normalizedLiuBei.equipment.accessory.itemId === 'grain-pouch' && normalizedLiuBei.equipment.accessory.level === 1 && normalizedLiuBei.equipment.accessory.exp === 69 && normalizedGuanYu?.level === 99 && normalizedGuanYu.exp === 100 && normalizedGuanYu.equipment.weapon.level === 9 && normalizedGuanYu.equipment.weapon.exp === 230, `Expected malformed roster units and equipment to be normalized safely: ${JSON.stringify(malformedRoster)}` ); 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: 1, updatedAt: '2026-07-04T00:00:00.000Z', step: 'fourth-camp', activeSaveSlot: 1, gold: 320, roster: alliedFirstBattleUnits, firstBattleReport: { battleId: 'first-battle-zhuo-commandery', battleTitle: ' ', outcome: 'victory', turnNumber: '4', rewardGold: '1000000', defeatedEnemies: '3', totalEnemies: '5', objectives: [ { id: 'village', label: 'Village', achieved: true, detail: 12, summary: 'x'.repeat(220), rewardGold: '1000000' }, { id: '', label: 'Broken', achieved: true }, ...Array.from({ length: 30 }, (_, index) => ({ id: `bonus-${index}`, label: `Bonus ${index}`, achieved: false, detail: 'Hold the ridge', rewardGold: 10 })) ], units: [ { id: 'liu-bei', name: 'Liu Bei', faction: 'ally', className: 'Lord', classKey: 'lord', level: '5', exp: '15', hp: '41', maxHp: '44', attack: '21', move: '5', stats: { might: '18', intelligence: '17', leadership: '20', agility: '12', luck: '11' }, equipment: {}, x: '2', y: '3' }, { id: 'broken-unit', name: 'Broken' }, ...Array.from({ length: 120 }, (_, index) => ({ id: `enemy-${index}`, name: `Enemy ${index}`, faction: 'enemy', className: 'Infantry', classKey: 'infantry', level: 1, exp: 0, hp: 20, maxHp: 20, attack: 12, move: 4, stats: { might: 8, intelligence: 5, leadership: 6, agility: 7, luck: 5 }, equipment: {}, x: index % 12, y: Math.floor(index / 12) })) ], bonds: [ { id: 'liu-bei__guan-yu', title: 'Oath Bond', unitIds: ['liu-bei', 'guan-yu'], level: '2', exp: '30', battleExp: '4' }, { id: 'broken-bond', title: 'Broken Bond', unitIds: ['liu-bei'] }, ...Array.from({ length: 120 }, (_, index) => ({ id: `bond-${index}`, title: `Bond ${index}`, unitIds: ['liu-bei', 'guan-yu'], level: 1, exp: 0, battleExp: 1 })) ], mvp: { unitId: 'liu-bei', name: 'Liu Bei', damageDealt: '1000000', defeats: '2' }, sortiePerformance: { selectedUnitIds: [' liu-bei ', 'ghost-unit', 'liu-bei', '', 9], formationAssignments: { ' liu-bei ': 'front', 'ghost-unit': 'support', broken: 'center' }, unitStats: [ sortieStatsFixture(' liu-bei ', { hpBefore: '40', maxHpBefore: '30', damageDealt: '1000000', damageTaken: '-2', defeats: '3.8', actions: '4', support: '5' }), sortieStatsFixture('liu-bei', { damageDealt: 1 }), sortieStatsFixture('ghost-unit', { damageDealt: 88 }), 'broken-performance' ], sortieItemAssignments: { 'liu-bei': { bean: 2 } } }, sortieReview: { version: 1, score: '88.9', grade: 'D', activeBondCount: '999', enemyCount: '1000', sourcePresetIds: ['siege', 'elite', 'siege', 'ghost', 'mobile', 7], itemAssignments: { 'liu-bei': { bean: 2 } } }, itemRewards: ['Bean', 'Bean', 'Bean -2', 'Bean +0', 10, 'x'.repeat(97)], campaignRewards: { supplies: [' Bean ', 'Bean', 'Bean -2', 'Bean +0', 20, 'x'.repeat(97)], equipment: [' Iron Sword ', 'Iron Sword', 'Iron Sword -2'], reputation: ['Village Thanks', ' Village Thanks ', 'Village Thanks +0'], recruits: [ { unitId: ' guan-yu ', name: ' Guan Yu ' }, { unitId: 'guan-yu', name: 'Duplicate Guan Yu' }, { unitId: 'ghost-recruit', name: 'Ghost Recruit' }, { unitId: '', name: 'Broken' }, ...Array.from({ length: 140 }, (_, index) => ({ unitId: `recruit-${index}`, name: `Recruit ${index}` })) ], unlocks: [ { battleId: ' second-battle-yellow-turban-pursuit ', title: ' Next ' }, { battleId: 'second-battle-yellow-turban-pursuit', title: 'Duplicate Next' }, { battleId: 'ghost-battle', title: 'Broken Next' }, { battleId: '', title: 'Broken' }, ...Array.from({ length: 140 }, (_, index) => ({ battleId: `battle-${index}`, title: `Battle ${index}` })) ], note: ' Reward note ' }, completedCampDialogues: ['intro', 'intro', 7], completedCampVisits: { corrupted: true }, createdAt: 42 } }) ); const malformedReport = loadCampaignState(); assert( malformedReport.step === 'fourth-camp' && malformedReport.gold === 320, `Expected report recovery to keep campaign progress: ${JSON.stringify(malformedReport)}` ); assert( malformedReport.firstBattleReport?.battleId === 'first-battle-zhuo-commandery' && malformedReport.firstBattleReport.battleTitle === firstScenario.title && malformedReport.firstBattleReport.turnNumber === 4 && malformedReport.firstBattleReport.rewardGold === 999999 && malformedReport.firstBattleReport.objectives.length === 24 && malformedReport.firstBattleReport.objectives[0].rewardGold === 999999 && malformedReport.firstBattleReport.objectives[0].summary.length === 180 && malformedReport.firstBattleReport.units.length === 96 && malformedReport.firstBattleReport.units[0].level === 5 && malformedReport.firstBattleReport.bonds.length === 96 && malformedReport.firstBattleReport.bonds[0].battleExp === 4 && malformedReport.firstBattleReport.mvp?.damageDealt === 999999 && malformedReport.firstBattleReport.itemRewards.length === 1 && malformedReport.firstBattleReport.campaignRewards?.supplies.length === 1 && malformedReport.firstBattleReport.campaignRewards?.supplies[0] === 'Bean' && malformedReport.firstBattleReport.campaignRewards?.equipment.length === 1 && malformedReport.firstBattleReport.campaignRewards?.reputation.length === 1 && malformedReport.firstBattleReport.campaignRewards?.recruits.length === 1 && malformedReport.firstBattleReport.campaignRewards?.recruits[0].unitId === 'guan-yu' && malformedReport.firstBattleReport.campaignRewards?.recruits[0].name === 'Guan Yu' && malformedReport.firstBattleReport.campaignRewards?.recruits.filter((recruit) => recruit.unitId === 'guan-yu').length === 1 && malformedReport.firstBattleReport.campaignRewards?.unlocks.length === 1 && malformedReport.firstBattleReport.campaignRewards?.unlocks[0].battleId === 'second-battle-yellow-turban-pursuit' && malformedReport.firstBattleReport.campaignRewards?.unlocks[0].title === 'Next' && malformedReport.firstBattleReport.campaignRewards?.unlocks.filter((unlock) => unlock.battleId === 'second-battle-yellow-turban-pursuit').length === 1 && malformedReport.firstBattleReport.campaignRewards?.note === 'Reward note' && malformedReport.firstBattleReport.completedCampDialogues.length === 1 && malformedReport.firstBattleReport.completedCampVisits.length === 0, `Expected malformed report fields to be normalized without discarding the report: ${JSON.stringify(malformedReport)}` ); const malformedReportPerformance = malformedReport.firstBattleReport?.sortiePerformance; const malformedReportReview = malformedReport.firstBattleReport?.sortieReview; assert( JSON.stringify(malformedReportPerformance?.selectedUnitIds) === JSON.stringify(['liu-bei']) && JSON.stringify(malformedReportPerformance?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front' }) && malformedReportPerformance?.unitStats.length === 1 && malformedReportPerformance.unitStats[0].unitId === 'liu-bei' && malformedReportPerformance.unitStats[0].hpBefore === 40 && malformedReportPerformance.unitStats[0].maxHpBefore === 40 && malformedReportPerformance.unitStats[0].damageDealt === 999999 && malformedReportPerformance.unitStats[0].damageTaken === 0 && malformedReportPerformance.unitStats[0].defeats === 3 && malformedReportPerformance.unitStats[0].actions === 4 && malformedReportPerformance.unitStats[0].support === 5 && !Object.prototype.hasOwnProperty.call(malformedReportPerformance, 'sortieItemAssignments'), `Expected malformed report sortie performance to normalize numbers, dedupe entries, filter stale roster ids, and exclude supplies: ${JSON.stringify(malformedReportPerformance)}` ); assert( malformedReportReview === undefined, `Expected report reviews to be discarded when roster recovery removes part of their recorded sortie: ${JSON.stringify(malformedReportReview)}` ); 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, JSON.stringify({ version: 1, updatedAt: '2026-07-04T01:00:00.000Z', step: 'fifth-camp', latestBattleId: 'missing-history-entry', roster: alliedFirstBattleUnits, bonds: firstScenario.bonds, battleHistory: { 'wrong-history-key': { battleId: 'first-battle-zhuo-commandery', battleTitle: ' ', outcome: 'victory', rewardGold: '120', itemRewards: ['Bean', 12, 'Bean', 'Bean -2', 'Bean +0'], campaignRewards: { supplies: { corrupted: true }, equipment: ['Iron Sword'], reputation: 'Fame', recruits: [ { unitId: 'test-recruit', name: 'Test Recruit' }, { unitId: ' guan-yu ', name: ' Guan Yu ' }, { broken: true } ], unlocks: [ { battleId: 'ghost-battle', title: 'Ghost Battle' }, { battleId: 'second-battle-yellow-turban-pursuit', title: 'Next Pursuit' } ], note: 'x'.repeat(181) }, objectives: [ { id: 'secure-village', label: 'Secure village', achieved: 'yes', status: 'lost', detail: 12, rewardGold: '80', targetTile: { x: '4', y: '5', radius: '200' } }, { id: 'broken-target', label: 'Broken target', achieved: false, detail: 'Bad target tile', rewardGold: 0, targetTile: { x: '-1', y: '5', radius: '2' } }, 'broken-objective', ...Array.from({ length: 30 }, (_, index) => ({ id: `history-bonus-${index}`, label: `History Bonus ${index}`, achieved: false, detail: 'Hold formation', rewardGold: 5 })) ], units: [ { unitId: 'liu-bei', name: 'Liu Bei', level: '4', exp: '33', hp: '28', maxHp: '30', equipment: 'broken' }, { unitId: '', name: 'Broken Unit', level: 99 }, ...Array.from({ length: 120 }, (_, index) => ({ unitId: `history-unit-${index}`, name: `History Unit ${index}`, level: 1, exp: 0, hp: 12, maxHp: 12, equipment: {} })) ], bonds: [ { id: firstScenario.bonds[0].id, title: firstScenario.bonds[0].title, level: '0', exp: '200', battleExp: '4' }, { id: '', title: 'Broken Bond' }, ...Array.from({ length: 120 }, (_, index) => ({ id: `history-bond-${index}`, title: `History Bond ${index}`, level: 1, exp: 0, battleExp: 1 })) ], sortiePerformance: { selectedUnitIds: ['liu-bei', 'ghost-unit', 'liu-bei', ''], formationAssignments: { 'liu-bei': 'front', 'ghost-unit': 'reserve', broken: 'center' }, unitStats: [ sortieStatsFixture('liu-bei', { hpBefore: '26', maxHpBefore: '20', damageDealt: '1000000', damageTaken: '-3', defeats: '2.9', actions: '6', support: '4' }), sortieStatsFixture('liu-bei', { damageDealt: 1 }), sortieStatsFixture('ghost-unit', { damageDealt: 77 }), 11 ], itemAssignments: { 'liu-bei': { bean: 2 } } }, sortieReview: { version: 1, score: '101.2', grade: 'D', activeBondCount: '999', enemyCount: '-4', sourcePresetIds: ['mobile', 'elite', 'mobile', 'ghost'], sortieItemAssignments: { 'liu-bei': { bean: 2 } } }, reserveTraining: [ { unitId: 'guan-yu', name: 'Guan Yu', expGained: '6', equipmentExpGained: '2', bondExpGained: '1', focusId: 'unknown-focus', focusLabel: 9, level: '5', exp: '44' }, 'broken-training', ...Array.from({ length: 120 }, (_, index) => ({ unitId: `reserve-${index}`, name: `Reserve ${index}`, expGained: 3, equipmentExpGained: 1, focusId: 'balanced', focusLabel: 'Balanced', level: 1, exp: 3 })) ], 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' }, 'invalid-date-history-key': { battleId: 'second-battle-yellow-turban-pursuit', battleTitle: 'Invalid Date Battle', outcome: 'victory', rewardGold: 999, itemRewards: [], campaignRewards: { supplies: [], equipment: [], reputation: [], recruits: [], unlocks: [] }, objectives: [], units: [], bonds: [], reserveTraining: [], completedAt: 'not-a-date' }, corrupted: 'not-a-settlement' } }) ); const malformedHistory = loadCampaignState(); assert( Object.keys(malformedHistory.battleHistory).length === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.rewardGold === 120 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.battleTitle === firstScenario.title && malformedHistory.battleHistory['wrong-history-key'] === undefined, `Expected malformed battle history entries to be filtered while valid settlements are keyed by battleId: ${JSON.stringify(malformedHistory)}` ); assert( malformedHistory.battleHistory['first-battle-zhuo-commandery']?.itemRewards.length === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.equipment[0] === 'Iron Sword' && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.recruits.length === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.recruits[0].unitId === 'guan-yu' && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.unlocks.length === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.unlocks[0].battleId === 'second-battle-yellow-turban-pursuit' && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.campaignRewards?.note === undefined, `Expected nested battle history rewards to normalize: ${JSON.stringify(malformedHistory)}` ); assert( malformedHistory.latestBattleId === 'first-battle-zhuo-commandery', `Expected stale latestBattleId to recover to the latest valid settlement: ${JSON.stringify(malformedHistory)}` ); assert( malformedHistory.battleHistory['first-battle-zhuo-commandery']?.objectives.length === 24 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.objectives[0].rewardGold === 80 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.objectives[0].targetTile?.x === 4 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.objectives[0].targetTile?.radius === 99 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.objectives[1].id === 'broken-target' && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.objectives[1].targetTile === undefined && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.objectives[0].status === undefined && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.units.length === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.units[0].level === 4 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.units[0].equipment.weapon.itemId === 'training-sword' && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.units[0].equipment.armor.itemId === 'cloth-armor' && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.units[0].equipment.accessory.itemId === 'grain-pouch' && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds.length === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds[0].level === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds[0].exp === 99 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.bonds[0].battleExp === 4 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.reserveTraining?.length === 1 && malformedHistory.battleHistory['first-battle-zhuo-commandery']?.reserveTraining?.[0].focusId === 'balanced', `Expected nested battle history progress arrays to filter invalid entries and normalize numbers: ${JSON.stringify(malformedHistory)}` ); const malformedHistoryPerformance = malformedHistory.battleHistory['first-battle-zhuo-commandery']?.sortiePerformance; const malformedHistoryReview = malformedHistory.battleHistory['first-battle-zhuo-commandery']?.sortieReview; assert( JSON.stringify(malformedHistoryPerformance?.selectedUnitIds) === JSON.stringify(['liu-bei']) && JSON.stringify(malformedHistoryPerformance?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front' }) && malformedHistoryPerformance?.unitStats.length === 1 && malformedHistoryPerformance.unitStats[0].unitId === 'liu-bei' && malformedHistoryPerformance.unitStats[0].hpBefore === 26 && malformedHistoryPerformance.unitStats[0].maxHpBefore === 26 && malformedHistoryPerformance.unitStats[0].damageDealt === 999999 && malformedHistoryPerformance.unitStats[0].damageTaken === 0 && malformedHistoryPerformance.unitStats[0].defeats === 2 && malformedHistoryPerformance.unitStats[0].actions === 6 && malformedHistoryPerformance.unitStats[0].support === 4 && !Object.prototype.hasOwnProperty.call(malformedHistoryPerformance, 'itemAssignments'), `Expected malformed historical sortie performance to normalize and filter roster references without persisting supplies: ${JSON.stringify(malformedHistoryPerformance)}` ); assert( malformedHistoryReview === undefined, `Expected historical reviews to be discarded when roster recovery removes part of their recorded sortie: ${JSON.stringify(malformedHistoryReview)}` ); storage.clear(); storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' })); const unsupported = loadCampaignState(); assert(unsupported.step === 'new', `Expected unsupported future save version to be ignored: ${JSON.stringify(unsupported)}`); assert(!hasCampaignSave(), 'Expected unsupported base save without valid slots to be ignored by hasCampaignSave'); storage.clear(); storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' })); storage.set( `${campaignStorageKey}:slot-2`, JSON.stringify({ version: 1, updatedAt: '2026-07-02T00:00:00.000Z', step: 'second-camp', activeSaveSlot: 2, gold: 260 }) ); assert(hasCampaignSave(), 'Expected valid slotted save fallback to be visible to hasCampaignSave'); const fallback = loadCampaignState(); assert(fallback.step === 'second-camp' && fallback.activeSaveSlot === 2, `Expected valid slotted save fallback: ${JSON.stringify(fallback)}`); storage.clear(); storage.set( campaignStorageKey, JSON.stringify({ version: 1, updatedAt: '2026-07-01T00:00:00.000Z', step: 'first-camp', activeSaveSlot: 1, gold: 100 }) ); storage.set( `${campaignStorageKey}:slot-1`, JSON.stringify({ version: 1, updatedAt: '2026-07-02T00:00:00.000Z', step: 'second-camp', activeSaveSlot: 1, gold: 200 }) ); const normalizedSlotFallback = loadCampaignState(0); assert( normalizedSlotFallback.step === 'second-camp' && normalizedSlotFallback.gold === 200, `Expected explicit slot boundary values to normalize to slot 1 instead of base save: ${JSON.stringify(normalizedSlotFallback)}` ); storage.clear(); storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' })); storage.set( `${campaignStorageKey}:slot-1`, JSON.stringify({ version: 1, updatedAt: 'zzzz-latest-looking-corruption', step: 'sixty-sixth-camp', activeSaveSlot: 1, gold: 999 }) ); storage.set( `${campaignStorageKey}:slot-2`, JSON.stringify({ version: 1, updatedAt: '2026-07-02T00:00:00.000Z', step: 'second-camp', activeSaveSlot: 2, gold: 260 }) ); const timestampSafeFallback = loadCampaignState(); assert( timestampSafeFallback.step === 'second-camp' && timestampSafeFallback.activeSaveSlot === 2, `Expected latest slotted fallback to ignore invalid timestamp saves: ${JSON.stringify(timestampSafeFallback)}` ); const corruptedSlot = loadCampaignState(1); assert( !Number.isNaN(Date.parse(corruptedSlot.updatedAt)) && corruptedSlot.updatedAt !== 'zzzz-latest-looking-corruption', `Expected explicitly loaded corrupted slot timestamp to be normalized: ${JSON.stringify(corruptedSlot)}` ); console.log('Verified campaign save normalization, reward resettlement, camp reward idempotence, malformed-field/roster-equipment/bond-growth/bond-roster/report-reference/sortie-roster/sortie-preset/sortie-performance/sortie-review/sortie-order/latest-history/detail recovery, sortie performance/review/order legacy/deep-clone/retry/reward-idempotence safety, timestamp-safe history/slot recovery, unknown-step/report/history recovery, unsupported-version rejection, and slotted fallback.'); } finally { await server.close(); } function assert(condition, message) { if (!condition) { throw new Error(message); } }