import { createServer } from 'vite'; const storage = new Map(); let rejectStorageWrites = false; globalThis.window = { localStorage: { getItem(key) { return storage.has(key) ? storage.get(key) : null; }, setItem(key, value) { if (rejectStorageWrites) { throw new Error('Simulated campaign storage write failure.'); } 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 dataModule = await server.ssrLoadModule( '/src/game/data/secondBattleReliefExploration.ts' ); const memoryModule = await server.ssrLoadModule( '/src/game/data/secondBattleReliefMemory.ts' ); const actionModule = await server.ssrLoadModule( '/src/game/state/secondBattleReliefActions.ts' ); const campaignModule = await server.ssrLoadModule( '/src/game/state/campaignState.ts' ); const { battleScenarios } = await server.ssrLoadModule( '/src/game/data/battles.ts' ); const { usableCatalog } = await server.ssrLoadModule( '/src/game/data/battleUsables.ts' ); const explorationAssets = await server.ssrLoadModule( '/src/game/data/explorationCharacterAssets.ts' ); const portraitAssets = await server.ssrLoadModule( '/src/game/data/portraitAssets.ts' ); const firstScoutModule = await server.ssrLoadModule( '/src/game/data/firstPursuitScoutMemory.ts' ); verifyCanonicalDefinition( dataModule, battleScenarios, usableCatalog, explorationAssets, portraitAssets ); verifyBattleResultAndScoutReview( dataModule, memoryModule, firstScoutModule ); verifyTargetBattleMemories(dataModule, memoryModule); verifyGuardedPersistentProgress( dataModule, memoryModule, actionModule, campaignModule, battleScenarios ); console.log( 'Second-battle relief exploration verification passed (canonical route data, ordered resumable objectives, first-scout result review, guarded rewards, and third-battle memory isolation).' ); } finally { await server.close(); } function verifyCanonicalDefinition( dataModule, battleScenarios, usableCatalog, explorationAssets, portraitAssets ) { const definition = dataModule.secondBattleReliefExplorationDefinition; const source = battleScenarios[dataModule.secondBattleReliefSourceBattleId]; const target = battleScenarios[dataModule.secondBattleReliefTargetBattleId]; assert( definition.id === dataModule.secondBattleReliefVisitId && definition.sourceBattleId === source.id && definition.targetBattleId === target.id, 'The relief route must stay scoped from the second victory to the third battle.' ); assert( source.objectives.some( (objective) => objective.id === dataModule.secondBattleReliefSourceObjectiveIds.village ) && source.objectives.some( (objective) => objective.id === dataModule.secondBattleReliefSourceObjectiveIds.quick ), 'The relief memory must reference the authored village and quick-victory objectives.' ); const actorIds = new Set(); definition.actors.forEach((actor) => { assert(!actorIds.has(actor.id), `Duplicate relief actor id ${actor.id}.`); actorIds.add(actor.id); assert( pointInside(actor, definition.movementBounds), `Actor ${actor.id} must stay inside the FHD exploration movement bounds.` ); assert( explorationAssets.hasExplorationCharacterAsset(actor.textureKey), `Actor ${actor.id} must use an existing exploration character sheet.` ); assert( typeof actor.portraitKey === 'string' && portraitAssets.portraitAssetEntriesForKey(actor.portraitKey) .length > 0, `Actor ${actor.id} must use an existing face portrait in dialogue.` ); assert( actor.dialogue.length >= 2 && actor.repeatDialogue.length >= 1, `Actor ${actor.id} must have first-time and repeat dialogue.` ); }); assert( pointInside(definition.player, definition.movementBounds) && pointInside(definition.exit, definition.movementBounds), 'The player start and camp exit must stay inside the movement bounds.' ); const objectiveIds = new Set(); definition.objectives.forEach((objective, index) => { assert( !objectiveIds.has(objective.id), `Duplicate relief objective id ${objective.id}.` ); assert( actorIds.has(objective.targetActorId), `Objective ${objective.id} must point to a canonical actor.` ); objective.prerequisiteObjectiveIds.forEach((prerequisiteId) => { assert( objectiveIds.has(prerequisiteId), `Objective ${objective.id} prerequisite ${prerequisiteId} must appear earlier in the route.` ); }); assert( objective.id === dataModule.secondBattleReliefObjectiveIds[index], `Objective ${objective.id} must retain the authored route order.` ); objectiveIds.add(objective.id); }); assert( JSON.stringify( dataModule.secondBattleReliefPrerequisiteObjectiveIds ) === JSON.stringify( dataModule.secondBattleReliefObjectiveIds.slice(0, -1) ), 'Every field interaction before the final Jian Yong choice must be a persisted prerequisite.' ); assert( JSON.stringify(definition.choices.map(({ id }) => id)) === JSON.stringify(dataModule.secondBattleReliefChoiceIds), 'The two relief choices must remain canonical and ordered.' ); definition.choices.forEach((choice) => { const effect = choice.thirdBattleEffect; assert( choice.bondExp > 0 && choice.itemRewards.length === 1 && /^.+\s+\d+$/.test(choice.itemRewards[0]), `Choice ${choice.id} must provide valid bond and item feedback.` ); assert( target.objectives.some( (objective) => objective.id === effect.focusObjectiveId ), `Choice ${choice.id} must focus an objective in the third battle.` ); assert( target.units.some( (unit) => unit.id === effect.recommendedUnitId && unit.faction === 'ally' ), `Choice ${choice.id} must recommend an allied third-battle unit.` ); if (effect.kind === 'village-supply-line') { assert( usableCatalog[effect.carriedUsableId]?.command === 'item', 'The village supply payoff must reference a real battle item.' ); } else { assert( target.units.some( (unit) => unit.id === effect.trackedEnemyUnitId && unit.faction === 'enemy' ), 'The ferry intelligence payoff must track the authored third-battle leader.' ); } }); } function verifyBattleResultAndScoutReview( dataModule, memoryModule, firstScoutModule ) { const cases = [ { choiceId: firstScoutModule.firstPursuitScoutChoiceIds[0], villageAchieved: true, quickAchieved: true, expectedFocus: 'river', expectedOutcome: 'confirmed' }, { choiceId: firstScoutModule.firstPursuitScoutChoiceIds[0], villageAchieved: true, quickAchieved: false, expectedFocus: 'river', expectedOutcome: 'missed' }, { choiceId: firstScoutModule.firstPursuitScoutChoiceIds[1], villageAchieved: true, quickAchieved: false, expectedFocus: 'village', expectedOutcome: 'confirmed' }, { choiceId: firstScoutModule.firstPursuitScoutChoiceIds[1], villageAchieved: false, quickAchieved: true, expectedFocus: 'village', expectedOutcome: 'missed' } ]; cases.forEach((testCase) => { const campaign = literalCampaign(dataModule, { villageAchieved: testCase.villageAchieved, quickAchieved: testCase.quickAchieved, firstScoutVisitId: firstScoutModule.firstPursuitScoutVisitId, firstScoutChoiceId: testCase.choiceId }); const context = memoryModule.resolveSecondBattleReliefContext(campaign); assert( context?.scoutReview.choiceId === testCase.choiceId && context.scoutReview.focus === testCase.expectedFocus && context.scoutReview.outcome === testCase.expectedOutcome, `The first scout choice must be reviewed against its matching second-battle result: ${JSON.stringify({ testCase, context })}` ); }); const unrecorded = memoryModule.resolveSecondBattleReliefContext( literalCampaign(dataModule, { villageAchieved: false, quickAchieved: false }) ); assert( unrecorded?.scoutReview.outcome === 'unrecorded', 'Legacy saves without a first-scout marker must receive a safe unrecorded fallback.' ); } function verifyTargetBattleMemories(dataModule, memoryModule) { dataModule.secondBattleReliefChoiceIds.forEach((choiceId) => { const campaign = literalCampaign(dataModule, { villageAchieved: choiceId === 'restore-northern-village', quickAchieved: choiceId === 'trace-ferry-messenger', reliefChoiceId: choiceId }); const memory = memoryModule.resolveSecondBattleReliefMemory({ campaign, battleId: dataModule.secondBattleReliefTargetBattleId }); assert( memory?.choiceId === choiceId && memory.effect.readiness === 'reinforced' && memory.effect.sourceObjectiveAchieved === true && memory.openingLines.length === 3 && memory.openingLines[0] === memory.campSummaryLine, `Choice ${choiceId} must resolve a reinforced third-battle memory.` ); const recoveredCampaign = literalCampaign(dataModule, { villageAchieved: false, quickAchieved: false, reliefChoiceId: choiceId }); const recovered = memoryModule.resolveSecondBattleReliefMemory({ campaign: recoveredCampaign, battleId: dataModule.secondBattleReliefTargetBattleId }); assert( recovered?.effect.readiness === 'recovered' && recovered.effect.sourceObjectiveAchieved === false, `Choice ${choiceId} must turn a missed source objective into a clearly labelled recovery, not a false success.` ); assert( memoryModule.resolveSecondBattleReliefMemory({ campaign, battleId: dataModule.secondBattleReliefSourceBattleId }) === undefined, `Choice ${choiceId} memory must not leak back into the source battle.` ); }); const forged = literalCampaign(dataModule, { villageAchieved: true, quickAchieved: true, reliefChoiceId: 'forged-choice' }); assert( memoryModule.resolveSecondBattleReliefMemory({ campaign: forged, battleId: dataModule.secondBattleReliefTargetBattleId }) === undefined, 'A forged relief choice must never produce a third-battle effect.' ); } function verifyGuardedPersistentProgress( dataModule, memoryModule, actionModule, campaignModule, battleScenarios ) { storage.clear(); campaignModule.resetCampaignState(); assert( actionModule.recordSecondBattleReliefObjective( dataModule.secondBattleReliefObjectiveIds[0] ).reason === 'invalid-campaign', 'Relief progress must reject completion before the second victory camp.' ); prepareSecondVictory( campaignModule, battleScenarios, dataModule, { villageAchieved: true, quickAchieved: false } ); assert( actionModule.recordSecondBattleReliefObjective( dataModule.secondBattleReliefObjectiveIds[1] ).reason === 'prerequisites-incomplete', 'The ferry testimony must not be recordable before the village check.' ); assert( actionModule.completeSecondBattleReliefExploration( dataModule.secondBattleReliefChoiceIds[0] ).reason === 'prerequisites-incomplete', 'The final choice must remain locked until every field objective is persisted.' ); dataModule.secondBattleReliefPrerequisiteObjectiveIds.forEach( (objectiveId, index) => { const result = actionModule.recordSecondBattleReliefObjective(objectiveId); assert( result.ok === true && result.completedObjectiveIds.length === index + 1, `Objective ${objectiveId} must persist in authored order.` ); } ); const resumedProgress = actionModule.secondBattleReliefProgress(); assert( resumedProgress.choiceReady === true && JSON.stringify(resumedProgress.completedObjectiveIds) === JSON.stringify( dataModule.secondBattleReliefPrerequisiteObjectiveIds ), 'Reloadable campaign markers must reconstruct all completed field objectives.' ); const choice = dataModule.secondBattleReliefExplorationDefinition.choices[0]; const inventoryBefore = campaignModule.getCampaignState().inventory; const completed = actionModule.completeSecondBattleReliefExploration(choice.id); assert( completed.ok === true && completed.memory.choiceId === choice.id && completed.memory.targetBattleId === dataModule.secondBattleReliefTargetBattleId && completed.campaign.completedCampVisits.includes( dataModule.secondBattleReliefVisitId ) && completed.campaign.campVisitChoiceIds[ dataModule.secondBattleReliefVisitId ] === choice.id, 'A valid final choice must atomically save the route, reward, and third-battle memory.' ); assertRewardApplied(choice, inventoryBefore, completed.campaign); const completedProgress = actionModule.secondBattleReliefProgress(); assert( completedProgress.completed === true && completedProgress.nextObjectiveId === null && completedProgress.choiceReady === false, 'A completed route must not keep advertising a stale final objective or another choice.' ); assert( actionModule.completeSecondBattleReliefExploration(choice.id) .reason === 'already-completed', 'The completed route must reject duplicate rewards.' ); prepareSecondVictory( campaignModule, battleScenarios, dataModule, { villageAchieved: false, quickAchieved: true } ); dataModule.secondBattleReliefPrerequisiteObjectiveIds.forEach( (objectiveId) => { const result = actionModule.recordSecondBattleReliefObjective(objectiveId); assert(result.ok === true, `Expected progress for ${objectiveId}.`); } ); const beforeFailedSave = progressSnapshot( campaignModule.getCampaignState(), dataModule ); let failedSave; rejectStorageWrites = true; try { failedSave = actionModule.completeSecondBattleReliefExploration( dataModule.secondBattleReliefChoiceIds[1] ); } finally { rejectStorageWrites = false; } const afterFailedSave = progressSnapshot( campaignModule.getCampaignState(), dataModule ); assert( failedSave?.ok === false && failedSave.reason === 'save-unavailable' && JSON.stringify(afterFailedSave) === JSON.stringify(beforeFailedSave), `A failed save must restore the route, rewards, and memory markers: ${JSON.stringify({ failedSave, beforeFailedSave, afterFailedSave })}` ); const finalCampaign = campaignModule.getCampaignState(); assert( memoryModule.resolveSecondBattleReliefMemory({ campaign: finalCampaign, battleId: dataModule.secondBattleReliefTargetBattleId }) === undefined, 'A failed final write must not leave a ghost third-battle memory.' ); } function prepareSecondVictory( campaignModule, battleScenarios, dataModule, { villageAchieved, quickAchieved } ) { storage.clear(); campaignModule.resetCampaignState(); const scenario = battleScenarios[dataModule.secondBattleReliefSourceBattleId]; campaignModule.setFirstBattleReport({ battleId: scenario.id, battleTitle: scenario.title, outcome: 'victory', turnNumber: quickAchieved ? 18 : 27, rewardGold: scenario.baseVictoryGold, defeatedEnemies: scenario.units.filter( (unit) => unit.faction === 'enemy' ).length, totalEnemies: scenario.units.filter( (unit) => unit.faction === 'enemy' ).length, objectives: scenario.objectives.map((objective) => { const achieved = objective.id === dataModule.secondBattleReliefSourceObjectiveIds.village ? villageAchieved : objective.id === dataModule.secondBattleReliefSourceObjectiveIds.quick ? quickAchieved : true; return { id: objective.id, label: objective.label, achieved, status: achieved ? 'done' : 'failed', detail: achieved ? '달성' : '미달', rewardGold: objective.rewardGold }; }), units: scenario.units, bonds: scenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), itemRewards: [], completedCampDialogues: [], completedCampVisits: [], createdAt: '2026-07-27T00:00:00.000Z' }); const campaign = campaignModule.getCampaignState(); assert( campaign.step === 'second-camp' && campaign.latestBattleId === scenario.id, 'The canonical second victory must route into second-camp.' ); return campaign; } function literalCampaign( dataModule, { villageAchieved, quickAchieved, reliefChoiceId, firstScoutVisitId, firstScoutChoiceId } ) { const completedCampVisits = []; const campVisitChoiceIds = {}; if (reliefChoiceId) { completedCampVisits.push(dataModule.secondBattleReliefVisitId); campVisitChoiceIds[dataModule.secondBattleReliefVisitId] = reliefChoiceId; } if (firstScoutVisitId && firstScoutChoiceId) { completedCampVisits.push(firstScoutVisitId); campVisitChoiceIds[firstScoutVisitId] = firstScoutChoiceId; } return { battleHistory: { [dataModule.secondBattleReliefSourceBattleId]: { battleId: dataModule.secondBattleReliefSourceBattleId, outcome: 'victory', turnNumber: quickAchieved ? 18 : 27, objectives: [ { id: dataModule.secondBattleReliefSourceObjectiveIds.village, achieved: villageAchieved, status: villageAchieved ? 'done' : 'failed' }, { id: dataModule.secondBattleReliefSourceObjectiveIds.quick, achieved: quickAchieved, status: quickAchieved ? 'done' : 'failed' } ] } }, completedCampVisits, campVisitChoiceIds }; } function assertRewardApplied(choice, inventoryBefore, campaign) { choice.itemRewards.forEach((reward) => { const match = /^(.+?)\s+(\d+)$/.exec(reward); assert(match, `Invalid reward label ${reward}.`); const [, label, amountText] = match; const amount = Number.parseInt(amountText, 10); assert( (campaign.inventory[label] ?? 0) === (inventoryBefore[label] ?? 0) + amount, `Expected ${reward} to be granted exactly once.` ); }); } function progressSnapshot(campaign, dataModule) { const relevantIds = campaign.completedCampVisits.filter( (visitId) => visitId === dataModule.secondBattleReliefVisitId || visitId.startsWith( `${dataModule.secondBattleReliefVisitId}:objective:` ) ); return { completedCampVisits: relevantIds, reportCompletedCampVisits: campaign.firstBattleReport?.completedCampVisits.filter( (visitId) => relevantIds.includes(visitId) ) ?? [], choiceId: campaign.campVisitChoiceIds[ dataModule.secondBattleReliefVisitId ] ?? null, inventory: { ...campaign.inventory }, bonds: campaign.bonds.map( ({ id, level, exp, battleExp }) => ({ id, level, exp, battleExp }) ) }; } function pointInside(point, bounds) { return ( point.x >= bounds.x && point.y >= bounds.y && point.x <= bounds.x + bounds.width && point.y <= bounds.y + bounds.height ); } function assert(condition, message) { if (!condition) { throw new Error(message); } }