import assert from 'node:assert/strict'; 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/thirdCampPreparation.ts' ); const actionModule = await server.ssrLoadModule( '/src/game/state/thirdCampPreparationActions.ts' ); const explorationDataModule = await server.ssrLoadModule( '/src/game/data/thirdCampExploration.ts' ); const explorationActionModule = await server.ssrLoadModule( '/src/game/state/thirdCampExplorationActions.ts' ); const campaignModule = await server.ssrLoadModule( '/src/game/state/campaignState.ts' ); const { battleScenarios } = await server.ssrLoadModule( '/src/game/data/battles.ts' ); verifyCanonicalDefinitions(dataModule, battleScenarios); verifyActivityGates(dataModule); verifyTargetBattleMemoryIsolation(dataModule); verifyBattleLifecycleContract(dataModule); verifyGuardedPersistentOverwrite( dataModule, actionModule, explorationDataModule, explorationActionModule, campaignModule, battleScenarios ); verifyOldSaveAndPollutionSafety( dataModule, actionModule, campaignModule, battleScenarios ); console.log( 'Third-camp preparation verification passed (persistent activity gates, guarded overwrite, fourth-battle isolation, three-turn lifecycle, applied/unused evaluation, and old-save pollution safety).' ); } finally { await server.close(); } function verifyCanonicalDefinitions(dataModule, battleScenarios) { const source = battleScenarios[dataModule.thirdCampPreparationSourceBattleId]; const target = battleScenarios[dataModule.thirdCampPreparationTargetBattleId]; assert.equal(source?.id, 'third-battle-guangzong-road'); assert.equal(target?.id, 'fourth-battle-guangzong-camp'); assert.equal( dataModule.thirdCampPreparationSelectionRecordId, 'third-camp-preparation-priority' ); assert.equal(dataModule.thirdCampPreparationActiveThroughTurn, 3); assert.deepEqual( dataModule.thirdCampPreparationPriorityIds, ['information', 'equipment', 'companion'] ); const definitions = dataModule.thirdCampPreparationPriorities; assert.equal(definitions.length, 3); assert.equal(new Set(definitions.map(({ id }) => id)).size, 3); assert.equal(new Set(definitions.map(({ label }) => label)).size, 3); definitions.forEach((definition) => { assert.equal( definition.effect.activeThroughTurn, dataModule.thirdCampPreparationActiveThroughTurn ); assert(definition.summary.length > 20); assert(definition.deploymentLine.includes(definition.label)); assert(definition.activeLine.includes(definition.label)); assert(definition.appliedLine.includes(definition.label)); assert(definition.unusedLine.includes(definition.label)); }); const information = dataModule.getThirdCampPreparationPriority('information'); assert.equal(information?.effect.kind, 'marked-vanguard-intel'); assert.equal(information?.effect.deploymentPreviewCount, 1); assert.equal(information?.effect.accuracyBonus, 10); assert.equal(information?.effect.maxBattleTriggers, 1); assert.equal( information?.effect.trackedEnemyUnitId, dataModule.thirdCampPreparationInformationEnemyUnitId ); assert( target.units.some( (unit) => unit.id === dataModule.thirdCampPreparationInformationEnemyUnitId && unit.faction === 'enemy' ), 'Information preparation must track a canonical fourth-battle enemy.' ); const equipment = dataModule.getThirdCampPreparationPriority('equipment'); assert.equal(equipment?.effect.kind, 'prepared-equipment-guard'); assert.equal(equipment?.effect.incomingDamageReductionPercent, 8); assert.equal(equipment?.effect.maxBattleTriggers, 1); assert.equal(equipment?.effect.targetRule, 'first-allied-hit'); const companion = dataModule.getThirdCampPreparationPriority('companion'); assert.equal(companion?.effect.kind, 'return-bond-opening'); assert.equal(companion?.effect.cooperationRateBonus, 5); const cloned = dataModule.getThirdCampPreparationPriority('information'); cloned.label = 'mutated'; cloned.effect.accuracyBonus = 99; const fresh = dataModule.getThirdCampPreparationPriority('information'); assert.equal(fresh.label, '본영 정보'); assert.equal(fresh.effect.accuracyBonus, 10); } function verifyActivityGates(dataModule) { const base = literalCampaign(dataModule); let availability = dataModule.resolveThirdCampPreparationAvailability(base); assert.deepEqual( availability.map(({ selectable, unavailableReason }) => ({ selectable, unavailableReason })), [ { selectable: false, unavailableReason: 'report-review-required' }, { selectable: false, unavailableReason: 'equipment-change-required' }, { selectable: false, unavailableReason: 'priority-dialogue-required' } ] ); const acknowledged = literalCampaign(dataModule, { acknowledgedCategories: ['unlocks', 'equipment'] }); availability = dataModule.resolveThirdCampPreparationAvailability(acknowledged); assert( availability.every(({ selectable }) => selectable === false), 'Opening reward categories alone must not complete walkable-camp activities.' ); const explored = literalCampaign(dataModule, { completedActivities: ['information', 'equipment'] }); availability = dataModule.resolveThirdCampPreparationAvailability(explored); assert.equal(availability[0].selectable, true); assert.deepEqual(availability[0].evidence, { kind: 'exploration-activity', battleId: dataModule.thirdCampPreparationSourceBattleId, visitId: 'third-guangzong-sortie-camp', activityId: 'information' }); assert.equal(availability[1].selectable, true); assert.deepEqual(availability[1].evidence, { kind: 'exploration-activity', battleId: dataModule.thirdCampPreparationSourceBattleId, visitId: 'third-guangzong-sortie-camp', activityId: 'equipment' }); assert.equal(availability[2].selectable, false); const wrongBattleCategories = literalCampaign(dataModule); wrongBattleCategories.acknowledgedVictoryRewardCategories = { 'fourth-battle-guangzong-camp': ['unlocks', 'equipment'] }; availability = dataModule.resolveThirdCampPreparationAvailability( wrongBattleCategories ); assert.equal(availability[0].selectable, false); assert.equal(availability[1].selectable, false); const companion = literalCampaign(dataModule, { completedActivities: ['companion'], completePriorityDialogue: true }); availability = dataModule.resolveThirdCampPreparationAvailability(companion); const companionAvailability = availability.find( ({ priority }) => priority.id === 'companion' ); assert.equal(companionAvailability?.selectable, true); assert.equal( companionAvailability?.evidence?.kind, 'exploration-activity' ); assert.equal( companionAvailability?.evidence?.dialogueId, 'guan-zhang-after-guangzong-road' ); assert.equal( companionAvailability?.evidence?.bondId, 'guan-yu__zhang-fei' ); const legacyCompanion = literalCampaign(dataModule, { completePriorityDialogue: true }); assert.equal( dataModule.resolveThirdCampPreparationAvailability( legacyCompanion ).find(({ priority }) => priority.id === 'companion') ?.evidence?.kind, 'priority-return-dialogue', 'An already completed result dialogue must remain valid for old saves.' ); const unrelatedDialogue = literalCampaign(dataModule); unrelatedDialogue.completedCampDialogues = [ 'liu-guan-after-guangzong-road' ]; availability = dataModule.resolveThirdCampPreparationAvailability( unrelatedDialogue ); assert.equal( availability.find(({ priority }) => priority.id === 'companion') ?.selectable, false, 'Only the result-selected priority return dialogue may unlock companion preparation.' ); const noVictory = literalCampaign(dataModule); noVictory.battleHistory[ dataModule.thirdCampPreparationSourceBattleId ].outcome = 'defeat'; availability = dataModule.resolveThirdCampPreparationAvailability(noVictory); assert( availability.every( ({ selectable, unavailableReason }) => selectable === false && unavailableReason === 'battle-record-required' ) ); } function verifyTargetBattleMemoryIsolation(dataModule) { dataModule.thirdCampPreparationPriorityIds.forEach((priorityId) => { const campaign = eligibleLiteralCampaign( dataModule, priorityId ); const memory = dataModule.resolveThirdCampPreparationMemory({ campaign, battleId: dataModule.thirdCampPreparationTargetBattleId }); assert.equal(memory?.priorityId, priorityId); assert.equal( memory?.sourceBattleId, dataModule.thirdCampPreparationSourceBattleId ); assert.equal( memory?.targetBattleId, dataModule.thirdCampPreparationTargetBattleId ); assert.equal( memory?.selectionRecordId, dataModule.thirdCampPreparationSelectionRecordId ); assert.equal( dataModule.resolveThirdCampPreparationMemory({ campaign, battleId: dataModule.thirdCampPreparationSourceBattleId }), undefined ); assert.equal( dataModule.resolveThirdCampPreparationMemory({ campaign, battleId: 'fifth-battle-sishui-vanguard' }), undefined ); }); const forged = eligibleLiteralCampaign(dataModule, 'information'); forged.thirdCampPreparationSelection.priorityId = 'forged'; assert.equal( dataModule.resolveThirdCampPreparationMemory({ campaign: forged, battleId: dataModule.thirdCampPreparationTargetBattleId }), undefined ); const stale = eligibleLiteralCampaign(dataModule, 'information'); stale.completedCampVisits = []; assert.equal( dataModule.resolveThirdCampPreparationMemory({ campaign: stale, battleId: dataModule.thirdCampPreparationTargetBattleId }), undefined, 'A stored choice without its persistent activity evidence must not create a battle effect.' ); } function verifyBattleLifecycleContract(dataModule) { dataModule.thirdCampPreparationPriorityIds.forEach((priorityId) => { const memory = dataModule.resolveThirdCampPreparationMemory({ campaign: eligibleLiteralCampaign(dataModule, priorityId), battleId: dataModule.thirdCampPreparationTargetBattleId }); assert(memory); const deployment = dataModule.resolveThirdCampPreparationBattlePresentation({ memory, stage: 'deployment' }); const turnOne = dataModule.resolveThirdCampPreparationBattlePresentation({ memory, stage: 'turn', turnNumber: 1 }); const turnThree = dataModule.resolveThirdCampPreparationBattlePresentation({ memory, stage: 'turn', turnNumber: 3 }); const turnFour = dataModule.resolveThirdCampPreparationBattlePresentation({ memory, stage: 'turn', turnNumber: 4 }); const applied = dataModule.resolveThirdCampPreparationBattlePresentation({ memory, stage: 'result', usageCount: 2 }); const unused = dataModule.resolveThirdCampPreparationBattlePresentation({ memory, stage: 'result', usageCount: 0 }); assert.deepEqual( [ deployment?.status, turnOne?.status, turnThree?.status, turnFour?.status, applied?.status, unused?.status ], [ 'deployment', 'active', 'active', 'expired', 'applied', 'unused' ] ); [ deployment, turnOne, turnThree, turnFour, applied, unused ].forEach((presentation) => { assert.equal( presentation?.selectionRecordId, memory.selectionRecordId ); assert.equal(presentation?.priorityId, memory.priorityId); assert.equal(presentation?.label, memory.label); assert.equal( presentation?.activeThroughTurn, dataModule.thirdCampPreparationActiveThroughTurn ); }); assert.equal( dataModule.resolveThirdCampPreparationBattlePresentation({ memory, stage: 'turn', turnNumber: 0 }), undefined ); const fresh = dataModule.resolveThirdCampPreparationMemory({ campaign: eligibleLiteralCampaign(dataModule, priorityId), battleId: dataModule.thirdCampPreparationTargetBattleId }); memory.label = 'mutated'; if ('unitIds' in memory.evidence) { memory.evidence.unitIds[0] = 'mutated'; } assert.notEqual(fresh?.label, 'mutated'); if (fresh && 'unitIds' in fresh.evidence) { assert.notEqual(fresh.evidence.unitIds[0], 'mutated'); } }); } function verifyGuardedPersistentOverwrite( dataModule, actionModule, explorationDataModule, explorationActionModule, campaignModule, battleScenarios ) { prepareThirdVictory(campaignModule, battleScenarios, dataModule); assert.equal( actionModule.setThirdCampPreparationPriority('forged').reason, 'invalid-priority' ); assert.equal( actionModule.setThirdCampPreparationPriority('information').reason, 'activity-required' ); const entry = explorationActionModule.enterThirdCampExploration(); assert.equal(entry.ok, true); const informationActivity = explorationActionModule.recordThirdCampExplorationActivity( 'information' ); assert.equal(informationActivity.ok, true); assert.equal( actionModule.clearThirdCampPreparationPriority().ok, true ); let result = actionModule.setThirdCampPreparationPriority('information'); assert.equal(result.ok, true); assert.equal(result.changed, true); assert.equal(result.memory.priorityId, 'information'); assert.deepEqual(result.campaign.thirdCampPreparationSelection, { sourceBattleId: dataModule.thirdCampPreparationSourceBattleId, targetBattleId: dataModule.thirdCampPreparationTargetBattleId, priorityId: 'information' }); assert.equal( result.campaign.completedCampVisits.filter( (id) => id === dataModule.thirdCampPreparationSelectionRecordId ).length, 0, 'Mutable preparation must never masquerade as a completed camp visit.' ); assert.equal( actionModule.setThirdCampPreparationPriority('equipment').reason, 'activity-required' ); assert.equal( campaignModule.getCampaignState() .thirdCampPreparationSelection?.priorityId, 'information', 'A rejected replacement must preserve the previous valid priority.' ); const equipmentActivity = explorationActionModule.recordThirdCampExplorationActivity( 'equipment' ); assert.equal(equipmentActivity.ok, true); assert.equal( actionModule.clearThirdCampPreparationPriority().ok, true ); result = actionModule.setThirdCampPreparationPriority('equipment'); assert.equal(result.ok, true); assert.equal(result.changed, true); assert.equal(result.memory.priorityId, 'equipment'); const explorationDefinition = explorationDataModule.resolveThirdCampExplorationDefinition( campaignModule.getCampaignState() ); const companionChoiceId = explorationDefinition?.companionDialogue.choices[0]?.id; assert(companionChoiceId); const companionActivity = explorationActionModule.completeThirdCampCompanionActivity( companionChoiceId ); assert.equal(companionActivity.ok, true); assert.equal( actionModule.clearThirdCampPreparationPriority().ok, true ); result = actionModule.setThirdCampPreparationPriority('companion'); assert.equal(result.ok, true); assert.equal(result.changed, true); assert.equal(result.memory.priorityId, 'companion'); assert.equal(result.memory.effect.bondId, 'guan-yu__zhang-fei'); const repeated = actionModule.setThirdCampPreparationPriority('companion'); assert.equal(repeated.ok, true); assert.equal(repeated.changed, false); let reloaded = campaignModule.loadCampaignState(); assert.equal( reloaded.thirdCampPreparationSelection?.priorityId, 'companion' ); assert.equal( dataModule.resolveThirdCampPreparationMemory({ campaign: reloaded, battleId: dataModule.thirdCampPreparationTargetBattleId })?.priorityId, 'companion' ); const cleared = actionModule.clearThirdCampPreparationPriority(); assert.equal(cleared.ok, true); assert.equal(cleared.changed, true); assert.equal( cleared.campaign.thirdCampPreparationSelection, undefined ); assert.equal( cleared.campaign.completedCampVisits.includes( dataModule.thirdCampPreparationSelectionRecordId ), false ); assert.equal( cleared.campaign.firstBattleReport?.completedCampVisits.includes( dataModule.thirdCampPreparationSelectionRecordId ), false ); result = actionModule.setThirdCampPreparationPriority('information'); assert.equal(result.ok, true); const beforeFailedOverwrite = campaignModule.getCampaignState(); rejectStorageWrites = true; let failedOverwrite; try { failedOverwrite = actionModule.setThirdCampPreparationPriority('equipment'); } finally { rejectStorageWrites = false; } assert.equal(failedOverwrite?.ok, false); assert.equal(failedOverwrite?.reason, 'save-unavailable'); assert.equal( campaignModule.getCampaignState() .thirdCampPreparationSelection?.priorityId, 'information' ); assert.equal( JSON.stringify( relevantSelectionState( campaignModule.getCampaignState(), dataModule ) ), JSON.stringify(relevantSelectionState(beforeFailedOverwrite, dataModule)) ); const wrongStep = campaignModule.getCampaignState(); wrongStep.step = 'fourth-battle'; campaignModule.setCampaignState(wrongStep); assert.equal( actionModule.setThirdCampPreparationPriority('equipment').reason, 'invalid-campaign' ); } function verifyOldSaveAndPollutionSafety( dataModule, actionModule, campaignModule, battleScenarios ) { prepareThirdVictory(campaignModule, battleScenarios, dataModule); let state = campaignModule.getCampaignState(); delete state.thirdCampPreparationSelection; state.campVisitChoiceIds = { 'existing-camp-visit': 'existing-choice' }; state.completedCampVisits = ['existing-camp-visit']; state.firstBattleReport.completedCampVisits = [ 'existing-camp-visit' ]; state = campaignModule.setCampaignState(state); assert.equal(state.thirdCampPreparationSelection, undefined); assert.equal( state.campVisitChoiceIds['existing-camp-visit'], 'existing-choice' ); assert.deepEqual(state.completedCampVisits, [ 'existing-camp-visit' ]); assert.equal( actionModule.thirdCampPreparationProgress(state) .selectedPriorityId, null ); state.campVisitChoiceIds[ dataModule.thirdCampPreparationSelectionRecordId ] = 'information'; state.completedCampVisits.push( dataModule.thirdCampPreparationSelectionRecordId ); state.firstBattleReport.completedCampVisits.push( dataModule.thirdCampPreparationSelectionRecordId ); state = campaignModule.setCampaignState(state); assert.equal( state.campVisitChoiceIds[ dataModule.thirdCampPreparationSelectionRecordId ], undefined, 'The abandoned camp-choice storage key must be scrubbed.' ); assert.equal(state.thirdCampPreparationSelection, undefined); assert.equal( state.completedCampVisits.includes( dataModule.thirdCampPreparationSelectionRecordId ), false ); assert.equal( state.firstBattleReport.completedCampVisits.includes( dataModule.thirdCampPreparationSelectionRecordId ), false ); assert.equal( state.campVisitChoiceIds['existing-camp-visit'], 'existing-choice', 'Legacy cleanup must not damage real camp history.' ); const corruptSelections = [ null, 'information', [], {}, { sourceBattleId: 'fourth-battle-guangzong-camp', targetBattleId: dataModule.thirdCampPreparationTargetBattleId, priorityId: 'information' }, { sourceBattleId: dataModule.thirdCampPreparationSourceBattleId, targetBattleId: 'fifth-battle-sishui-vanguard', priorityId: 'information' }, { sourceBattleId: dataModule.thirdCampPreparationSourceBattleId, targetBattleId: dataModule.thirdCampPreparationTargetBattleId, priorityId: 'forged' } ]; corruptSelections.forEach((selection) => { const candidate = campaignModule.getCampaignState(); candidate.thirdCampPreparationSelection = selection; const normalized = campaignModule.setCampaignState(candidate); assert.equal( normalized.thirdCampPreparationSelection, undefined ); }); state = campaignModule.getCampaignState(); state.thirdCampPreparationSelection = { sourceBattleId: dataModule.thirdCampPreparationSourceBattleId, targetBattleId: dataModule.thirdCampPreparationTargetBattleId, priorityId: 'information' }; state = campaignModule.setCampaignState(state); assert.equal( state.thirdCampPreparationSelection, undefined, 'A structurally valid but activity-locked priority must be removed.' ); campaignModule.acknowledgeCampaignVictoryReward( dataModule.thirdCampPreparationSourceBattleId, ['unlocks'] ); const validCandidate = campaignModule.getCampaignState(); validCandidate.thirdCampPreparationSelection = { sourceBattleId: dataModule.thirdCampPreparationSourceBattleId, targetBattleId: dataModule.thirdCampPreparationTargetBattleId, priorityId: 'information', polluted: true }; state = campaignModule.setCampaignState(validCandidate); assert.deepEqual(state.thirdCampPreparationSelection, { sourceBattleId: dataModule.thirdCampPreparationSourceBattleId, targetBattleId: dataModule.thirdCampPreparationTargetBattleId, priorityId: 'information' }); validCandidate.thirdCampPreparationSelection.priorityId = 'equipment'; assert.equal( campaignModule.getCampaignState() .thirdCampPreparationSelection?.priorityId, 'information', 'The normalized selection must be deep-cloned from caller state.' ); state = campaignModule.getCampaignState(); state.thirdCampPreparationSelection = { sourceBattleId: dataModule.thirdCampPreparationSourceBattleId, targetBattleId: dataModule.thirdCampPreparationTargetBattleId, priorityId: 'equipment' }; state = campaignModule.setCampaignState(state); assert.equal( state.thirdCampPreparationSelection, undefined, 'A different locked priority must not borrow another activity proof.' ); assert.equal( dataModule.resolveThirdCampPreparationSelection(state), undefined ); assert.equal( dataModule.resolveThirdCampPreparationMemory({ campaign: state, battleId: dataModule.thirdCampPreparationTargetBattleId }), undefined ); const legacy = literalCampaign(dataModule, { acknowledgedCategories: ['unlocks'], selectedPriorityId: 'information', legacyReportOnly: true }); const legacyMemory = dataModule.resolveThirdCampPreparationMemory({ campaign: legacy, battleId: dataModule.thirdCampPreparationTargetBattleId }); assert.equal(legacyMemory?.priorityId, 'information'); assert.equal( legacyMemory?.evidence.kind, 'victory-reward-category' ); const legacyCompanion = literalCampaign(dataModule, { completePriorityDialogue: true, selectedPriorityId: 'companion', legacyReportOnly: true }); assert.equal( dataModule.resolveThirdCampPreparationMemory({ campaign: legacyCompanion, battleId: dataModule.thirdCampPreparationTargetBattleId })?.priorityId, 'companion' ); const legacyWrongReport = structuredClone(legacy); legacyWrongReport.firstBattleReport.battleId = 'fourth-battle-guangzong-camp'; assert.equal( dataModule.resolveThirdCampPreparationMemory({ campaign: legacyWrongReport, battleId: dataModule.thirdCampPreparationTargetBattleId }), undefined ); } function prepareThirdVictory( campaignModule, battleScenarios, dataModule ) { storage.clear(); campaignModule.resetCampaignState(); const scenario = battleScenarios[dataModule.thirdCampPreparationSourceBattleId]; campaignModule.setFirstBattleReport({ battleId: scenario.id, battleTitle: scenario.title, outcome: 'victory', turnNumber: 22, 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) => ({ id: objective.id, label: objective.label, achieved: true, status: 'done', detail: '달성', rewardGold: objective.rewardGold })), units: scenario.units, bonds: scenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), itemRewards: [...scenario.itemRewards], campaignRewards: { supplies: [...(scenario.campaignReward?.supplies ?? [])], equipment: [...(scenario.campaignReward?.equipment ?? [])], reputation: [...(scenario.campaignReward?.reputation ?? [])], recruits: [], unlocks: scenario.campaignReward?.unlockBattleId ? [{ battleId: scenario.campaignReward.unlockBattleId, title: scenario.campaignReward.unlockLabel ?? scenario.campaignReward.unlockBattleId }] : [], note: scenario.campaignReward?.note }, completedCampDialogues: [], completedCampVisits: [], createdAt: '2026-07-27T00:00:00.000Z' }); const campaign = campaignModule.getCampaignState(); assert.equal(campaign.step, 'third-camp'); assert.equal( campaign.latestBattleId, dataModule.thirdCampPreparationSourceBattleId ); return campaign; } function completePriorityDialogue( dataModule, campaignModule, battleScenarios ) { const campaign = campaignModule.getCampaignState(); const dialogueModuleResult = priorityDialogueForCampaign( campaign, dataModule ); const bondByDialogueId = { 'liu-guan-after-guangzong-road': 'liu-bei__guan-yu', 'liu-zhang-after-guangzong-road': 'liu-bei__zhang-fei', 'guan-zhang-after-guangzong-road': 'guan-yu__zhang-fei' }; const bondId = bondByDialogueId[dialogueModuleResult.targetDialogueId]; assert( battleScenarios[ dataModule.thirdCampPreparationSourceBattleId ].bonds.some((bond) => bond.id === bondId) ); const report = campaignModule.applyCampBondExp( dialogueModuleResult.targetDialogueId, bondId, 1, 'review-third-battle-result' ); assert( report?.completedCampDialogues.includes( dialogueModuleResult.targetDialogueId ) ); } function priorityDialogueForCampaign(campaign, dataModule) { const record = campaign.battleHistory[ dataModule.thirdCampPreparationSourceBattleId ] ?? campaign.firstBattleReport; const fort = record.objectives.find(({ id }) => id === 'fort'); const quick = record.objectives.find(({ id }) => id === 'quick'); if (!fort.achieved) { return { targetDialogueId: 'liu-guan-after-guangzong-road' }; } if (!quick.achieved) { return { targetDialogueId: 'liu-zhang-after-guangzong-road' }; } return { targetDialogueId: 'guan-zhang-after-guangzong-road' }; } function eligibleLiteralCampaign(dataModule, priorityId) { return literalCampaign(dataModule, { completedActivities: [priorityId], completePriorityDialogue: priorityId === 'companion', selectedPriorityId: priorityId }); } function literalCampaign( dataModule, { acknowledgedCategories = [], completedActivities = [], completePriorityDialogue = false, selectedPriorityId, legacyReportOnly = false } = {} ) { const report = { battleId: dataModule.thirdCampPreparationSourceBattleId, outcome: 'victory', turnNumber: 22, objectives: [ { id: 'fort', achieved: true, status: 'done' }, { id: 'quick', achieved: true, status: 'done' } ], units: [] }; return { step: 'third-camp', latestBattleId: dataModule.thirdCampPreparationSourceBattleId, battleHistory: legacyReportOnly ? {} : { [dataModule.thirdCampPreparationSourceBattleId]: structuredClone(report) }, firstBattleReport: structuredClone(report), completedCampDialogues: completePriorityDialogue ? ['guan-zhang-after-guangzong-road'] : [], completedCampVisits: completedActivities.map( (activityId) => `third-guangzong-sortie-camp:activity:${activityId}` ), campVisitChoiceIds: {}, ...(selectedPriorityId ? { thirdCampPreparationSelection: { sourceBattleId: dataModule.thirdCampPreparationSourceBattleId, targetBattleId: dataModule.thirdCampPreparationTargetBattleId, priorityId: selectedPriorityId } } : {}), acknowledgedVictoryRewardCategories: acknowledgedCategories.length > 0 ? { [dataModule.thirdCampPreparationSourceBattleId]: [...acknowledgedCategories] } : {} }; } function relevantSelectionState(campaign, dataModule) { return { selection: campaign.thirdCampPreparationSelection ?? null, completed: campaign.completedCampVisits.filter( (id) => id === dataModule.thirdCampPreparationSelectionRecordId ), reportCompleted: campaign.firstBattleReport?.completedCampVisits.filter( (id) => id === dataModule.thirdCampPreparationSelectionRecordId ) ?? [] }; }