feat: add second-victory relief exploration
This commit is contained in:
657
scripts/verify-second-battle-relief-exploration.mjs
Normal file
657
scripts/verify-second-battle-relief-exploration.mjs
Normal file
@@ -0,0 +1,657 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user