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, hmr: false }, appType: 'custom' }); try { const memoryModule = await server.ssrLoadModule( '/src/game/data/firstBattleCamaraderieMemory.ts' ); const campaignModule = await server.ssrLoadModule( '/src/game/state/campaignState.ts' ); const battleSaveModule = await server.ssrLoadModule( '/src/game/state/battleSaveState.ts' ); const { battleScenarios } = await server.ssrLoadModule( '/src/game/data/battles.ts' ); verifyDominantSelection(memoryModule); verifyMemoryAndBonus(memoryModule); verifyCampaignRoundTrip(memoryModule, campaignModule, battleScenarios); verifyBattleSaveRoundTrip(battleSaveModule); console.log( 'First-battle camaraderie memory verification passed (selection, campaign settlement, legacy battle save, and +5%p gate).' ); } finally { await server.close(); } function verifyDominantSelection(memoryModule) { const bonds = [ { id: 'bond-c', unitIds: ['liu-bei', 'guan-yu'] }, { id: 'bond-a', unitIds: ['liu-bei', 'zhang-fei'] }, { id: 'bond-b', unitIds: ['guan-yu', 'zhang-fei'] } ]; const statsByBond = { 'bond-c': { attempts: 8, successes: 0, additionalDamage: 80 }, 'bond-b': { attempts: 2, successes: 1, additionalDamage: 6 }, 'bond-a': { attempts: 3, successes: 1, additionalDamage: 6 } }; const snapshot = memoryModule.selectDominantSortieCooperationSnapshot({ bonds, statsByBond }); assert( snapshot?.bondId === 'bond-a' && snapshot.attempts === 3 && snapshot.successes === 1 && snapshot.additionalDamage === 6, `A successful pair must outrank a failure-only pair, then use success, damage, attempts, and stable id: ${JSON.stringify(snapshot)}` ); snapshot.unitIds[0] = 'mutated'; assert( bonds[1].unitIds[0] === 'liu-bei', 'Dominant selection must clone the canonical bond pair.' ); const failureOnly = memoryModule.selectDominantSortieCooperationSnapshot({ bonds: [{ id: 'bond-c', unitIds: ['liu-bei', 'guan-yu'] }], statsByBond: { 'bond-c': { attempts: 2.9, successes: -4, additionalDamage: 99 } } }); assert( failureOnly?.attempts === 2 && failureOnly.successes === 0 && failureOnly.additionalDamage === 0, `Failure-only cooperation must discard impossible damage: ${JSON.stringify(failureOnly)}` ); } function verifyMemoryAndBonus(memoryModule) { const snapshot = { version: 1, bondId: 'liu-bei__zhang-fei', unitIds: ['liu-bei', 'zhang-fei'], attempts: 1, successes: 1, additionalDamage: 7 }; const campaign = { firstBattleReport: { battleId: 'second-battle-yellow-turban-pursuit', outcome: 'defeat' }, battleHistory: { 'first-battle-zhuo-commandery': { battleId: 'first-battle-zhuo-commandery', outcome: 'victory', units: [ { unitId: 'liu-bei', name: '유비' }, { unitId: 'zhang-fei', name: '장비' } ], sortieCooperation: snapshot }, 'second-battle-yellow-turban-pursuit': { battleId: 'second-battle-yellow-turban-pursuit', outcome: 'defeat', units: [] } }, completedCampDialogues: [] }; const memory = memoryModule.resolveFirstBattleCamaraderieMemory(campaign); assert( memory?.bondId === snapshot.bondId && memory.rememberedOutcome === 'success' && memory.completed === false && memory.dialogue.id === memoryModule.firstBattleCamaraderieDialogueId && memory.dialogue.lines.length === 3 && memory.dialogue.choices.every((choice) => choice.response.includes('+5%p')), `Memory must come from the first victory settlement and explain the unlock for either choice: ${JSON.stringify(memory)}` ); assert( memoryModule.firstBattleCamaraderieBonusFor({ campaign, battleId: memoryModule.firstBattleCamaraderieTargetBattleId, coreBondId: snapshot.bondId }) === 0, 'The remembered pair must not receive a bonus before its camp dialogue is completed.' ); campaign.completedCampDialogues.push( memoryModule.firstBattleCamaraderieDialogueId ); assert( memoryModule.firstBattleCamaraderieBonusFor({ campaign, battleId: memoryModule.firstBattleCamaraderieTargetBattleId, coreBondId: snapshot.bondId }) === 5 && memoryModule.firstBattleCamaraderieBonusFor({ campaign, battleId: memoryModule.firstBattleCamaraderieTargetBattleId, coreBondId: 'liu-bei__guan-yu' }) === 0 && memoryModule.firstBattleCamaraderieBonusFor({ campaign, battleId: 'third-battle-guangzong-road', coreBondId: snapshot.bondId }) === 0, 'The +5%p bonus must require dialogue completion, the exact next battle, and the same manually selected core pair.' ); assert( memoryModule.resolveFirstBattleCamaraderieMemory({ battleHistory: { 'first-battle-zhuo-commandery': { ...campaign.battleHistory['first-battle-zhuo-commandery'], outcome: 'defeat' } } }) === undefined, 'A first-battle defeat must never create a camaraderie memory.' ); const attemptedCampaign = structuredClone(campaign); attemptedCampaign.completedCampDialogues = []; attemptedCampaign.battleHistory[ memoryModule.firstBattleCamaraderieSourceBattleId ].sortieCooperation = { ...snapshot, attempts: 2, successes: 0, additionalDamage: 99 }; const attemptedMemory = memoryModule.resolveFirstBattleCamaraderieMemory(attemptedCampaign); assert( attemptedMemory?.rememberedOutcome === 'attempted' && attemptedMemory.attempts === 2 && attemptedMemory.successes === 0 && attemptedMemory.additionalDamage === 0 && attemptedMemory.dialogue.title.includes('엇갈린') && attemptedMemory.dialogue.lines.length === 3 && attemptedMemory.dialogue.choices.every( (choice) => choice.response.includes('+5%p') ), `A failed-only pursuit must become a safe learning dialogue with no phantom damage: ${JSON.stringify(attemptedMemory)}` ); } function verifyCampaignRoundTrip( memoryModule, campaignModule, battleScenarios ) { storage.clear(); campaignModule.resetCampaignState(); const scenario = battleScenarios[memoryModule.firstBattleCamaraderieSourceBattleId]; const cooperation = { version: 1, bondId: 'liu-bei__zhang-fei', unitIds: ['liu-bei', 'zhang-fei'], attempts: 3, successes: 1, additionalDamage: 8 }; campaignModule.setFirstBattleReport({ battleId: scenario.id, battleTitle: scenario.title, outcome: 'victory', turnNumber: 5, rewardGold: 100, defeatedEnemies: scenario.units.filter((unit) => unit.faction === 'enemy') .length, totalEnemies: scenario.units.filter((unit) => unit.faction === 'enemy') .length, objectives: [], units: scenario.units, bonds: scenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), itemRewards: [], sortieCooperation: cooperation, completedCampDialogues: [], completedCampVisits: [], createdAt: '2026-07-27T00:00:00.000Z' }); let campaign = campaignModule.getCampaignState(); const settlement = campaign.battleHistory[memoryModule.firstBattleCamaraderieSourceBattleId]; assert( JSON.stringify(campaign.firstBattleReport?.sortieCooperation) === JSON.stringify(cooperation) && JSON.stringify(settlement?.sortieCooperation) === JSON.stringify(cooperation) && campaign.firstBattleReport.sortieCooperation !== settlement.sortieCooperation && campaign.firstBattleReport.sortieCooperation.unitIds !== settlement.sortieCooperation.unitIds, 'Report-to-settlement creation must preserve and deep-clone the selected cooperation snapshot.' ); campaign = campaignModule.loadCampaignState(); assert( campaign.battleHistory[memoryModule.firstBattleCamaraderieSourceBattleId] ?.sortieCooperation?.bondId === cooperation.bondId, 'A valid cooperation snapshot must survive a persisted campaign round trip.' ); const failedOnly = campaignModule.normalizeCampaignSortieCooperationSnapshot( { ...cooperation, attempts: 2.8, successes: -1, additionalDamage: 100 }, { allowedUnitIds: new Set(cooperation.unitIds), bonds: scenario.bonds } ); assert( failedOnly?.attempts === 2 && failedOnly.successes === 0 && failedOnly.additionalDamage === 0, `Campaign normalization must canonicalize failed-only totals: ${JSON.stringify(failedOnly)}` ); const raw = JSON.parse(storage.get(campaignModule.campaignStorageKey)); raw.firstBattleReport.sortieCooperation.unitIds[1] = 'ghost-unit'; raw.battleHistory[ memoryModule.firstBattleCamaraderieSourceBattleId ].sortieCooperation.bondId = 'ghost-bond'; storage.set(campaignModule.campaignStorageKey, JSON.stringify(raw)); campaign = campaignModule.loadCampaignState(); assert( campaign.firstBattleReport?.sortieCooperation === undefined && campaign.battleHistory[ memoryModule.firstBattleCamaraderieSourceBattleId ]?.sortieCooperation === undefined, 'Unknown roster and bond references must be removed without rejecting the campaign save.' ); } function verifyBattleSaveRoundTrip(battleSaveModule) { const options = { expectedBattleId: 'first-battle-zhuo-commandery', mapWidth: 12, mapHeight: 8, validUnitIds: new Set(['liu-bei', 'guan-yu', 'rebel-1']), validAllyUnitIds: new Set(['liu-bei', 'guan-yu']), validEnemyUnitIds: new Set(['rebel-1']), validUsableIds: new Set(['bean']), validItemIds: new Set(['bean']), validEquipmentItemIds: new Set([ 'training-sword', 'cloth-armor', 'grain-pouch' ]), validEquipmentSlotItems: { weapon: new Set(['training-sword']), armor: new Set(['cloth-armor']), accessory: new Set(['grain-pouch']) }, validTriggeredBattleEventIds: new Set(['opening']) }; const base = createValidBattleSaveState(); const normalized = battleSaveModule.normalizeBattleSaveState( { ...base, cooperationStatsByBond: { taoyuan: { attempts: 2.9, successes: -1, additionalDamage: 100 }, 'ghost-bond': { attempts: 9, successes: 9, additionalDamage: 99 } } }, options ); assert( JSON.stringify(normalized?.cooperationStatsByBond) === JSON.stringify({ taoyuan: { attempts: 2, successes: 0, additionalDamage: 0 } }), `Battle saves must normalize per-bond totals and discard unknown bonds: ${JSON.stringify(normalized?.cooperationStatsByBond)}` ); const legacy = battleSaveModule.normalizeBattleSaveState( { ...base, sortieResonanceBondId: 'taoyuan', coreResonanceStats: { attempts: 3, successes: 1, failures: 2, additionalDamage: 9 } }, options ); assert( JSON.stringify(legacy?.cooperationStatsByBond) === JSON.stringify({ taoyuan: { attempts: 3, successes: 1, additionalDamage: 9 } }), `Legacy core-resonance totals must backfill the generic per-bond record: ${JSON.stringify(legacy?.cooperationStatsByBond)}` ); assert( !battleSaveModule.isValidBattleSaveState( { ...base, cooperationStatsByBond: { taoyuan: { attempts: 1, successes: 0, additionalDamage: 7 } } }, options ), 'Direct validation must reject impossible failure-only additional damage.' ); } function createValidBattleSaveState() { return { version: 1, battleId: 'first-battle-zhuo-commandery', campaignStep: 'first-battle', sortieOrderId: 'elite', savedAt: '2026-07-27T00:00:00.000Z', turnNumber: 2, activeFaction: 'ally', rosterTab: 'ally', actedUnitIds: [], attackIntents: [], battleLog: [], units: [ savedUnit('liu-bei', 1, 2), savedUnit('guan-yu', 2, 2), savedUnit('rebel-1', 8, 4) ], bonds: [ { id: 'taoyuan', unitIds: ['liu-bei', 'guan-yu'], title: 'Oath', level: 30, exp: 0, battleExp: 0, description: 'Shared oath.' } ], itemStocks: {}, battleBuffs: [], battleStatuses: [], battleStats: {}, enemyUsableUseKeys: [], triggeredBattleEvents: [] }; } function savedUnit(id, x, y) { return { id, level: 2, exp: 0, hp: 30, maxHp: 30, attack: 10, move: 4, stats: { might: 70, intelligence: 70, leadership: 70, agility: 70, luck: 70 }, x, y, direction: 'south', equipment: { weapon: { itemId: 'training-sword', level: 1, exp: 0 }, armor: { itemId: 'cloth-armor', level: 1, exp: 0 }, accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } } }; } function assert(condition, message) { if (!condition) { throw new Error(message); } }