Files
heros_web/scripts/verify-third-camp-preparation.mjs

964 lines
28 KiB
JavaScript

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 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,
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.equal(availability[0].selectable, true);
assert.deepEqual(availability[0].evidence, {
kind: 'victory-reward-category',
battleId: dataModule.thirdCampPreparationSourceBattleId,
category: 'unlocks'
});
assert.equal(availability[1].selectable, true);
assert.deepEqual(availability[1].evidence, {
kind: 'victory-reward-category',
battleId: dataModule.thirdCampPreparationSourceBattleId,
category: '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, {
completePriorityDialogue: true
});
availability =
dataModule.resolveThirdCampPreparationAvailability(companion);
const companionAvailability = availability.find(
({ priority }) => priority.id === 'companion'
);
assert.equal(companionAvailability?.selectable, true);
assert.equal(
companionAvailability?.evidence?.kind,
'priority-return-dialogue'
);
assert.equal(
companionAvailability?.evidence?.dialogueId,
'guan-zhang-after-guangzong-road'
);
assert.equal(
companionAvailability?.evidence?.bondId,
'guan-yu__zhang-fei'
);
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.acknowledgedVictoryRewardCategories = {};
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?.evidence.kind === 'priority-return-dialogue') {
assert.notEqual(fresh.evidence.unitIds[0], 'mutated');
}
});
}
function verifyGuardedPersistentOverwrite(
dataModule,
actionModule,
campaignModule,
battleScenarios
) {
prepareThirdVictory(campaignModule, battleScenarios, dataModule);
assert.equal(
actionModule.setThirdCampPreparationPriority('forged').reason,
'invalid-priority'
);
assert.equal(
actionModule.setThirdCampPreparationPriority('information').reason,
'activity-required'
);
campaignModule.acknowledgeCampaignVictoryReward(
dataModule.thirdCampPreparationSourceBattleId,
['unlocks']
);
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.'
);
campaignModule.acknowledgeCampaignVictoryReward(
dataModule.thirdCampPreparationSourceBattleId,
['equipment']
);
result = actionModule.setThirdCampPreparationPriority('equipment');
assert.equal(result.ok, true);
assert.equal(result.changed, true);
assert.equal(result.memory.priorityId, 'equipment');
completePriorityDialogue(
dataModule,
campaignModule,
battleScenarios
);
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, {
acknowledgedCategories:
priorityId === 'information'
? ['unlocks']
: priorityId === 'equipment'
? ['equipment']
: [],
completePriorityDialogue: priorityId === 'companion',
selectedPriorityId: priorityId
});
}
function literalCampaign(
dataModule,
{
acknowledgedCategories = [],
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: [],
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
) ?? []
};
}