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, getCampaignState, loadCampaignState, resetCampaignState, 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 { campaignBattleRouteEntries } = await server.ssrLoadModule('/src/game/state/campaignRouting.ts'); const scenarios = Object.values(battleScenarios); const routeEntries = campaignBattleRouteEntries(); assert(scenarios.length === 66, `Expected 66 campaign battles, received ${scenarios.length}.`); assert(routeEntries.length === 66, `Expected 66 campaign battle routes, received ${routeEntries.length}.`); assert( JSON.stringify(routeEntries.map((entry) => entry.battleId)) === JSON.stringify(scenarios.map((scenario) => scenario.id)), 'Battle scenario order differs from the independent campaign routing table.' ); assert(scenarios[0]?.id === 'first-battle-zhuo-commandery', `Unexpected first battle: ${scenarios[0]?.id}`); assert(scenarios.at(-1)?.id === 'sixty-sixth-battle-wuzhang-final', `Unexpected final battle: ${scenarios.at(-1)?.id}`); const recruitById = new Map(campaignRecruitUnits.map((unit) => [unit.id, unit])); const expectedRecruitIds = new Set(); const checkpointBattles = new Set([1, 11, 22, 33, 44, 55, 66]); let expectedGold = 0; resetCampaignState(); for (const [index, scenario] of scenarios.entries()) { const campaignReward = createCampaignRewardSnapshot(scenario, recruitById, battleScenarios); campaignReward.recruits.forEach((recruit) => expectedRecruitIds.add(recruit.unitId)); const objectives = scenario.objectives.map((objective) => ({ id: objective.id, label: objective.label, achieved: true, status: 'done', category: objective.kind === 'defeat-leader' || objective.kind === 'pacify-leader' ? 'primary' : 'bonus', detail: objective.label, rewardGold: objective.rewardGold, ...(objective.targetTile ? { targetTile: { ...objective.targetTile } } : {}) })); const rewardGold = scenario.baseVictoryGold + objectives.reduce((sum, objective) => sum + objective.rewardGold, 0); expectedGold += rewardGold; setFirstBattleReport({ battleId: scenario.id, battleTitle: scenario.title, outcome: 'victory', turnNumber: Math.max(1, Math.min(scenario.quickVictoryTurnLimit, 8 + (index % 5))), rewardGold, defeatedEnemies: scenario.units.filter((unit) => unit.faction === 'enemy').length, totalEnemies: scenario.units.filter((unit) => unit.faction === 'enemy').length, objectives, units: structuredClone(scenario.units), bonds: structuredClone(scenario.bonds), itemRewards: [...scenario.itemRewards], campaignRewards: campaignReward, completedCampDialogues: [], completedCampVisits: [], createdAt: new Date(Date.UTC(2026, 0, 1, 0, 0, index)).toISOString() }); let state = getCampaignState(); const battleNumber = index + 1; assert(state.latestBattleId === scenario.id, `Battle ${battleNumber} did not become the latest settlement.`); assert(state.battleHistory[scenario.id]?.outcome === 'victory', `Battle ${battleNumber} victory was not recorded.`); assert(Object.keys(state.battleHistory).length === battleNumber, `Battle history stopped at ${Object.keys(state.battleHistory).length}/${battleNumber}.`); assert(state.step === expectedCampStep(scenario.id), `Battle ${battleNumber} routed to ${state.step}, expected ${expectedCampStep(scenario.id)}.`); assert(state.roster.every((unit, unitIndex, roster) => roster.findIndex((candidate) => candidate.id === unit.id) === unitIndex), `Battle ${battleNumber} produced duplicate roster entries.`); assert(Object.values(state.inventory).every((amount) => Number.isInteger(amount) && amount > 0), `Battle ${battleNumber} produced invalid inventory amounts.`); state = checkpointBattles.has(battleNumber) ? roundTripLegacyCompatibleSave(campaignStorageKey, state, loadCampaignState) : loadCampaignState(1); const expectedHistoryIds = scenarios.slice(0, battleNumber).map((candidate) => candidate.id); assert( JSON.stringify(Object.keys(state.battleHistory)) === JSON.stringify(expectedHistoryIds), `Save reload changed campaign order at battle ${battleNumber}.` ); assert(state.latestBattleId === scenario.id, `Save reload lost the latest battle at ${battleNumber}.`); assert(state.step === expectedCampStep(scenario.id), `Save reload changed the route at battle ${battleNumber}.`); } const finalState = getCampaignState(); const scenarioIds = new Set(scenarios.map((scenario) => scenario.id)); assert(finalState.step === 'sixty-sixth-camp', `Expected the final victory to reach the final camp, received ${finalState.step}.`); assert(finalState.latestBattleId === 'sixty-sixth-battle-wuzhang-final', `Unexpected final latest battle: ${finalState.latestBattleId}`); assert(Object.keys(finalState.battleHistory).length === 66, `Expected 66 settlements, received ${Object.keys(finalState.battleHistory).length}.`); assert(Object.keys(finalState.battleHistory).every((battleId) => scenarioIds.has(battleId)), 'Final history contains an unknown battle id.'); assert(Object.values(finalState.battleHistory).every((settlement) => settlement.outcome === 'victory'), 'Final history contains a non-victory settlement.'); const expectedStoredGold = Math.min(999999, expectedGold); assert(finalState.gold === expectedStoredGold, `Expected capped gold ${expectedStoredGold} from ${expectedGold}, received ${finalState.gold}.`); assert([...expectedRecruitIds].every((unitId) => finalState.roster.some((unit) => unit.id === unitId)), 'Final roster is missing a campaign recruit.'); assert(finalState.firstBattleReport?.battleId === finalState.latestBattleId, 'Final report and latest settlement are out of sync.'); const finalSlotRaw = storage.get(`${campaignStorageKey}:slot-1`); assert(finalSlotRaw === storage.get(campaignStorageKey), 'Base save and active slot diverged after campaign completion.'); assert(JSON.parse(finalSlotRaw).battleHistory['sixty-sixth-battle-wuzhang-final'], 'Serialized final save is missing the last settlement.'); console.log( `Verified all ${scenarios.length} campaign settlements in route order, every-battle save reloads, seven legacy migrations, ${expectedRecruitIds.size} recruit unlocks, reward accumulation, and the final-camp state.` ); } finally { await server.close(); } function createCampaignRewardSnapshot(scenario, recruitById, battleScenarios) { const reward = scenario.campaignReward; return { supplies: [...(reward?.supplies ?? [])], equipment: [...(reward?.equipment ?? [])], reputation: [...(reward?.reputation ?? [])], recruits: (reward?.recruits ?? []).map((unitId) => { const unit = scenario.units.find((candidate) => candidate.id === unitId) ?? recruitById.get(unitId); assert(unit, `${scenario.id} references unknown recruit ${unitId}.`); return { unitId, name: unit.name }; }), unlocks: reward?.unlockBattleId ? [{ battleId: reward.unlockBattleId, title: reward.unlockLabel ?? battleScenarios[reward.unlockBattleId]?.title ?? reward.unlockBattleId }] : [], ...(reward?.note ? { note: reward.note } : {}) }; } function roundTripLegacyCompatibleSave(campaignStorageKey, state, loadCampaignState) { const legacySnapshot = structuredClone(state); delete legacySnapshot.sortieFormationAssignments; delete legacySnapshot.sortieItemAssignments; delete legacySnapshot.sortieFormationPresets; delete legacySnapshot.sortieOrderSelection; delete legacySnapshot.sortieResonanceSelection; delete legacySnapshot.sortieRecommendationSelection; delete legacySnapshot.sortieOrderHistory; delete legacySnapshot.claimedSortieOrderRewardIds; delete legacySnapshot.reserveTrainingAssignments; delete legacySnapshot.completedTutorialIds; delete legacySnapshot.acknowledgedVictoryRewardBattleIds; delete legacySnapshot.acknowledgedVictoryRewardCategories; const serialized = JSON.stringify(legacySnapshot); storage.set(campaignStorageKey, serialized); storage.set(`${campaignStorageKey}:slot-1`, serialized); return loadCampaignState(1); } function expectedCampStep(battleId) { const prefix = battleId.split('-battle-')[0]; return `${prefix}-camp`; } function assert(condition, message) { if (!condition) { throw new Error(message); } }