import { createServer } from 'vite'; const values = new Map(); globalThis.window = { localStorage: { getItem(key) { return values.has(key) ? values.get(key) : null; }, setItem(key, value) { values.set(key, String(value)); }, removeItem(key) { values.delete(key); }, clear() { values.clear(); } } }; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true, hmr: false }, appType: 'custom' }); try { const { acknowledgeCampaignVictoryReward, campaignStorageKey, campaignVictoryRewardCategoriesFor, dismissCampaignVictoryRewardNotice, getCampaignState, getPendingCampaignVictoryRewardBattleIds, getPendingCampaignVictoryRewardCategories, getPendingCampaignVictoryRewardNoticeReport, getPendingCampaignVictoryRewardReport, loadCampaignState, resetCampaignState, setFirstBattleReport } = await server.ssrLoadModule('/src/game/state/campaignState.ts'); const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts'); resetCampaignState(); const scenario = battleScenarios['first-battle-zhuo-commandery']; const unlock = { battleId: scenario.campaignReward.unlockBattleId, title: scenario.campaignReward.unlockLabel ?? battleScenarios[scenario.campaignReward.unlockBattleId].title }; const report = { battleId: scenario.id, battleTitle: scenario.title, outcome: 'victory', turnNumber: 4, rewardGold: 100, defeatedEnemies: 2, totalEnemies: 2, objectives: [], units: scenario.units.filter((unit) => unit.faction === 'ally'), bonds: scenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), itemRewards: ['Bean x2', 'Iron Sword 1'], campaignRewards: { supplies: ['Bean x2'], equipment: ['Iron Sword 1'], reputation: ['황건 토벌 명성'], recruits: [{ unitId: 'jian-yong', name: '간옹' }], unlocks: [unlock] }, completedCampDialogues: [], completedCampVisits: [], createdAt: '2026-07-19T02:00:00.000Z' }; setFirstBattleReport(report); const expectedCategories = ['gold', 'equipment', 'supplies', 'reputation', 'recruits', 'unlocks']; assert( JSON.stringify(campaignVictoryRewardCategoriesFor(report)) === JSON.stringify(expectedCategories) && JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id)) === JSON.stringify(expectedCategories) && getPendingCampaignVictoryRewardNoticeReport()?.battleId === scenario.id, `Every populated reward category must begin pending in stable display order: ${JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id))}` ); const dismissedNotice = dismissCampaignVictoryRewardNotice(scenario.id); const repeatedNoticeDismissal = dismissCampaignVictoryRewardNotice(scenario.id); const persistedDismissedNotice = JSON.parse(values.get(campaignStorageKey)); const persistedDismissedNoticeSlot = JSON.parse(values.get(`${campaignStorageKey}:slot-1`)); loadCampaignState(); assert( JSON.stringify(dismissedNotice.dismissedVictoryRewardNoticeBattleIds) === JSON.stringify([scenario.id]) && JSON.stringify(repeatedNoticeDismissal.dismissedVictoryRewardNoticeBattleIds) === JSON.stringify([scenario.id]) && persistedDismissedNotice.dismissedVictoryRewardNoticeBattleIds.includes(scenario.id) && persistedDismissedNoticeSlot.dismissedVictoryRewardNoticeBattleIds.includes(scenario.id) && JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id)) === JSON.stringify(expectedCategories) && getPendingCampaignVictoryRewardReport()?.battleId === scenario.id && getPendingCampaignVictoryRewardNoticeReport() === undefined, `Dismissing a victory notice must persist once without consuming destination-specific NEW categories: ${JSON.stringify({ dismissedNotice, persistedDismissedNotice })}` ); const equipmentOnly = acknowledgeCampaignVictoryReward(scenario.id, ['equipment']); assert( JSON.stringify(equipmentOnly.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(['equipment']) && !equipmentOnly.acknowledgedVictoryRewardBattleIds.includes(scenario.id) && !getPendingCampaignVictoryRewardCategories(scenario.id).includes('equipment') && getPendingCampaignVictoryRewardCategories(scenario.id).includes('supplies') && getPendingCampaignVictoryRewardReport()?.battleId === scenario.id, `Acknowledging equipment must preserve every other pending NEW category: ${JSON.stringify(equipmentOnly)}` ); const destinationCategories = acknowledgeCampaignVictoryReward(scenario.id, ['supplies', 'recruits', 'unlocks']); assert( !destinationCategories.acknowledgedVictoryRewardBattleIds.includes(scenario.id) && JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id)) === JSON.stringify(['gold', 'reputation']), `Opening supply and formation destinations must leave passive categories independent: ${JSON.stringify(destinationCategories)}` ); const fullyAcknowledged = acknowledgeCampaignVictoryReward(scenario.id, ['gold', 'reputation']); const persistedFullState = JSON.parse(values.get(campaignStorageKey)); assert( fullyAcknowledged.acknowledgedVictoryRewardBattleIds.includes(scenario.id) && JSON.stringify(fullyAcknowledged.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(expectedCategories) && getPendingCampaignVictoryRewardCategories(scenario.id).length === 0 && getPendingCampaignVictoryRewardReport() === undefined && persistedFullState.acknowledgedVictoryRewardBattleIds.includes(scenario.id), `The legacy battle-level flag must be written only after every actual category is acknowledged: ${JSON.stringify(fullyAcknowledged)}` ); const legacySave = structuredClone(persistedFullState); delete legacySave.acknowledgedVictoryRewardCategories; delete legacySave.dismissedVictoryRewardNoticeBattleIds; values.set(campaignStorageKey, JSON.stringify(legacySave)); const migratedLegacySave = loadCampaignState(); assert( migratedLegacySave.acknowledgedVictoryRewardBattleIds.includes(scenario.id) && JSON.stringify(migratedLegacySave.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(expectedCategories) && JSON.stringify(migratedLegacySave.dismissedVictoryRewardNoticeBattleIds) === JSON.stringify([]) && getPendingCampaignVictoryRewardReport() === undefined, `A legacy all-or-nothing acknowledgement must migrate safely without requiring a notice-dismissal field: ${JSON.stringify(migratedLegacySave)}` ); const partialSave = structuredClone(persistedFullState); partialSave.acknowledgedVictoryRewardBattleIds = []; partialSave.acknowledgedVictoryRewardCategories = { [scenario.id]: ['equipment', 'unknown-category'], 'second-battle-yellow-turban-pursuit': ['equipment'] }; partialSave.dismissedVictoryRewardNoticeBattleIds = [ scenario.id, scenario.id, 'second-battle-yellow-turban-pursuit', 'unknown-battle' ]; values.set(campaignStorageKey, JSON.stringify(partialSave)); const normalizedPartialSave = loadCampaignState(); assert( JSON.stringify(normalizedPartialSave.acknowledgedVictoryRewardCategories) === JSON.stringify({ [scenario.id]: ['equipment'] }) && !normalizedPartialSave.acknowledgedVictoryRewardBattleIds.includes(scenario.id) && JSON.stringify(normalizedPartialSave.dismissedVictoryRewardNoticeBattleIds) === JSON.stringify([scenario.id]) && getPendingCampaignVictoryRewardCategories(scenario.id).includes('supplies') && getPendingCampaignVictoryRewardNoticeReport() === undefined, `Normalization must retain a valid dismissal while dropping duplicate, unknown, and non-victory battle ids: ${JSON.stringify(normalizedPartialSave)}` ); const repeatedAll = acknowledgeCampaignVictoryReward(scenario.id); assert( repeatedAll.acknowledgedVictoryRewardBattleIds.includes(scenario.id) && JSON.stringify(repeatedAll.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(expectedCategories), 'The legacy one-argument acknowledgement API must remain an idempotent acknowledge-all operation.' ); const current = getCampaignState(); assert(current.acknowledgedVictoryRewardBattleIds.includes(scenario.id), 'Acknowledgement state must remain available through cloned campaign reads.'); resetCampaignState(); setFirstBattleReport(report); acknowledgeCampaignVictoryReward(scenario.id, ['gold', 'reputation', 'supplies', 'recruits', 'unlocks']); const secondScenario = battleScenarios['second-battle-yellow-turban-pursuit']; const secondReport = { ...report, battleId: secondScenario.id, battleTitle: secondScenario.title, units: secondScenario.units.filter((unit) => unit.faction === 'ally'), bonds: secondScenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), itemRewards: ['Bean x1'], campaignRewards: { supplies: ['Bean x1'], equipment: [], reputation: [], recruits: [], unlocks: [] }, createdAt: '2026-07-19T03:00:00.000Z' }; setFirstBattleReport(secondReport); assert( getPendingCampaignVictoryRewardReport()?.battleId === secondScenario.id && getPendingCampaignVictoryRewardNoticeReport()?.battleId === secondScenario.id && JSON.stringify(getPendingCampaignVictoryRewardBattleIds()) === JSON.stringify([scenario.id, secondScenario.id]) && getPendingCampaignVictoryRewardCategories().includes('equipment') && getPendingCampaignVictoryRewardCategories().includes('supplies'), 'A newer victory must not hide unreviewed reward categories from an earlier settlement.' ); dismissCampaignVictoryRewardNotice(secondScenario.id); assert( getPendingCampaignVictoryRewardReport()?.battleId === secondScenario.id && getPendingCampaignVictoryRewardNoticeReport() === undefined && JSON.stringify(getPendingCampaignVictoryRewardCategories(secondScenario.id)) === JSON.stringify(['gold', 'supplies']), 'Dismissing the latest notice must preserve all NEW categories without resurfacing an older reward as another arrival modal.' ); acknowledgeCampaignVictoryReward(secondScenario.id); assert( getPendingCampaignVictoryRewardReport()?.battleId === scenario.id && JSON.stringify(getPendingCampaignVictoryRewardCategories()) === JSON.stringify(['equipment']), 'After reviewing the latest victory, the next pending reward report and aggregated NEW category must resurface.' ); console.log('Verified category-level victory reward acknowledgements, persisted notice dismissal, cross-victory visibility, normalization, and legacy migration.'); } finally { await server.close(); } function assert(condition, message) { if (!condition) { throw new Error(message); } }