Files
heros_web/scripts/verify-victory-reward-acknowledgements.mjs

189 lines
8.2 KiB
JavaScript

import { createServer } from 'vite';
const values = new Map();
globalThis.window = {
localStorage: {
getItem(key) {
return values.has(key) ? values.get(key) : null;
},
setItem(key, value) {
values.set(key, String(value));
},
removeItem(key) {
values.delete(key);
},
clear() {
values.clear();
}
}
};
const server = await createServer({
logLevel: 'error',
server: { middlewareMode: true, hmr: false },
appType: 'custom'
});
try {
const {
acknowledgeCampaignVictoryReward,
campaignStorageKey,
campaignVictoryRewardCategoriesFor,
getCampaignState,
getPendingCampaignVictoryRewardBattleIds,
getPendingCampaignVictoryRewardCategories,
getPendingCampaignVictoryRewardReport,
loadCampaignState,
resetCampaignState,
setFirstBattleReport
} = await server.ssrLoadModule('/src/game/state/campaignState.ts');
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
resetCampaignState();
const scenario = battleScenarios['first-battle-zhuo-commandery'];
const unlock = {
battleId: scenario.campaignReward.unlockBattleId,
title: scenario.campaignReward.unlockLabel ?? battleScenarios[scenario.campaignReward.unlockBattleId].title
};
const report = {
battleId: scenario.id,
battleTitle: scenario.title,
outcome: 'victory',
turnNumber: 4,
rewardGold: 100,
defeatedEnemies: 2,
totalEnemies: 2,
objectives: [],
units: scenario.units.filter((unit) => unit.faction === 'ally'),
bonds: scenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })),
itemRewards: ['Bean x2', 'Iron Sword 1'],
campaignRewards: {
supplies: ['Bean x2'],
equipment: ['Iron Sword 1'],
reputation: ['황건 토벌 명성'],
recruits: [{ unitId: 'jian-yong', name: '간옹' }],
unlocks: [unlock]
},
completedCampDialogues: [],
completedCampVisits: [],
createdAt: '2026-07-19T02:00:00.000Z'
};
setFirstBattleReport(report);
const expectedCategories = ['gold', 'equipment', 'supplies', 'reputation', 'recruits', 'unlocks'];
assert(
JSON.stringify(campaignVictoryRewardCategoriesFor(report)) === JSON.stringify(expectedCategories) &&
JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id)) === JSON.stringify(expectedCategories),
`Every populated reward category must begin pending in stable display order: ${JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id))}`
);
const equipmentOnly = acknowledgeCampaignVictoryReward(scenario.id, ['equipment']);
assert(
JSON.stringify(equipmentOnly.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(['equipment']) &&
!equipmentOnly.acknowledgedVictoryRewardBattleIds.includes(scenario.id) &&
!getPendingCampaignVictoryRewardCategories(scenario.id).includes('equipment') &&
getPendingCampaignVictoryRewardCategories(scenario.id).includes('supplies') &&
getPendingCampaignVictoryRewardReport()?.battleId === scenario.id,
`Acknowledging equipment must preserve every other pending NEW category: ${JSON.stringify(equipmentOnly)}`
);
const destinationCategories = acknowledgeCampaignVictoryReward(scenario.id, ['supplies', 'recruits', 'unlocks']);
assert(
!destinationCategories.acknowledgedVictoryRewardBattleIds.includes(scenario.id) &&
JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id)) === JSON.stringify(['gold', 'reputation']),
`Opening supply and formation destinations must leave passive categories independent: ${JSON.stringify(destinationCategories)}`
);
const fullyAcknowledged = acknowledgeCampaignVictoryReward(scenario.id, ['gold', 'reputation']);
const persistedFullState = JSON.parse(values.get(campaignStorageKey));
assert(
fullyAcknowledged.acknowledgedVictoryRewardBattleIds.includes(scenario.id) &&
JSON.stringify(fullyAcknowledged.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(expectedCategories) &&
getPendingCampaignVictoryRewardCategories(scenario.id).length === 0 &&
getPendingCampaignVictoryRewardReport() === undefined &&
persistedFullState.acknowledgedVictoryRewardBattleIds.includes(scenario.id),
`The legacy battle-level flag must be written only after every actual category is acknowledged: ${JSON.stringify(fullyAcknowledged)}`
);
const legacySave = structuredClone(persistedFullState);
delete legacySave.acknowledgedVictoryRewardCategories;
values.set(campaignStorageKey, JSON.stringify(legacySave));
const migratedLegacySave = loadCampaignState();
assert(
migratedLegacySave.acknowledgedVictoryRewardBattleIds.includes(scenario.id) &&
JSON.stringify(migratedLegacySave.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(expectedCategories) &&
getPendingCampaignVictoryRewardReport() === undefined,
`A legacy all-or-nothing acknowledgement must migrate to every actual reward category: ${JSON.stringify(migratedLegacySave)}`
);
const partialSave = structuredClone(persistedFullState);
partialSave.acknowledgedVictoryRewardBattleIds = [];
partialSave.acknowledgedVictoryRewardCategories = {
[scenario.id]: ['equipment', 'unknown-category'],
'second-battle-yellow-turban-pursuit': ['equipment']
};
values.set(campaignStorageKey, JSON.stringify(partialSave));
const normalizedPartialSave = loadCampaignState();
assert(
JSON.stringify(normalizedPartialSave.acknowledgedVictoryRewardCategories) === JSON.stringify({ [scenario.id]: ['equipment'] }) &&
!normalizedPartialSave.acknowledgedVictoryRewardBattleIds.includes(scenario.id) &&
getPendingCampaignVictoryRewardCategories(scenario.id).includes('supplies'),
`Normalization must retain valid partial progress while dropping unknown categories and non-victory battles: ${JSON.stringify(normalizedPartialSave.acknowledgedVictoryRewardCategories)}`
);
const repeatedAll = acknowledgeCampaignVictoryReward(scenario.id);
assert(
repeatedAll.acknowledgedVictoryRewardBattleIds.includes(scenario.id) &&
JSON.stringify(repeatedAll.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(expectedCategories),
'The legacy one-argument acknowledgement API must remain an idempotent acknowledge-all operation.'
);
const current = getCampaignState();
assert(current.acknowledgedVictoryRewardBattleIds.includes(scenario.id), 'Acknowledgement state must remain available through cloned campaign reads.');
resetCampaignState();
setFirstBattleReport(report);
acknowledgeCampaignVictoryReward(scenario.id, ['gold', 'reputation', 'supplies', 'recruits', 'unlocks']);
const secondScenario = battleScenarios['second-battle-yellow-turban-pursuit'];
const secondReport = {
...report,
battleId: secondScenario.id,
battleTitle: secondScenario.title,
units: secondScenario.units.filter((unit) => unit.faction === 'ally'),
bonds: secondScenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })),
itemRewards: ['Bean x1'],
campaignRewards: {
supplies: ['Bean x1'],
equipment: [],
reputation: [],
recruits: [],
unlocks: []
},
createdAt: '2026-07-19T03:00:00.000Z'
};
setFirstBattleReport(secondReport);
assert(
getPendingCampaignVictoryRewardReport()?.battleId === secondScenario.id &&
JSON.stringify(getPendingCampaignVictoryRewardBattleIds()) === JSON.stringify([scenario.id, secondScenario.id]) &&
getPendingCampaignVictoryRewardCategories().includes('equipment') &&
getPendingCampaignVictoryRewardCategories().includes('supplies'),
'A newer victory must not hide unreviewed reward categories from an earlier settlement.'
);
acknowledgeCampaignVictoryReward(secondScenario.id);
assert(
getPendingCampaignVictoryRewardReport()?.battleId === scenario.id &&
JSON.stringify(getPendingCampaignVictoryRewardCategories()) === JSON.stringify(['equipment']),
'After reviewing the latest victory, the next pending reward report and aggregated NEW category must resurface.'
);
console.log('Verified category-level victory reward acknowledgements, cross-victory pending visibility, destination isolation, persistence, and legacy migration.');
} finally {
await server.close();
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}