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

226 lines
11 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,
dismissCampaignVictoryRewardNotice,
getCampaignState,
getPendingCampaignVictoryRewardBattleIds,
getPendingCampaignVictoryRewardCategories,
getPendingCampaignVictoryRewardNoticeReport,
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) &&
getPendingCampaignVictoryRewardNoticeReport()?.battleId === scenario.id,
`Every populated reward category must begin pending in stable display order: ${JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id))}`
);
const dismissedNotice = dismissCampaignVictoryRewardNotice(scenario.id);
const repeatedNoticeDismissal = dismissCampaignVictoryRewardNotice(scenario.id);
const persistedDismissedNotice = JSON.parse(values.get(campaignStorageKey));
const persistedDismissedNoticeSlot = JSON.parse(values.get(`${campaignStorageKey}:slot-1`));
loadCampaignState();
assert(
JSON.stringify(dismissedNotice.dismissedVictoryRewardNoticeBattleIds) === JSON.stringify([scenario.id]) &&
JSON.stringify(repeatedNoticeDismissal.dismissedVictoryRewardNoticeBattleIds) === JSON.stringify([scenario.id]) &&
persistedDismissedNotice.dismissedVictoryRewardNoticeBattleIds.includes(scenario.id) &&
persistedDismissedNoticeSlot.dismissedVictoryRewardNoticeBattleIds.includes(scenario.id) &&
JSON.stringify(getPendingCampaignVictoryRewardCategories(scenario.id)) === JSON.stringify(expectedCategories) &&
getPendingCampaignVictoryRewardReport()?.battleId === scenario.id &&
getPendingCampaignVictoryRewardNoticeReport() === undefined,
`Dismissing a victory notice must persist once without consuming destination-specific NEW categories: ${JSON.stringify({ dismissedNotice, persistedDismissedNotice })}`
);
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;
delete legacySave.dismissedVictoryRewardNoticeBattleIds;
values.set(campaignStorageKey, JSON.stringify(legacySave));
const migratedLegacySave = loadCampaignState();
assert(
migratedLegacySave.acknowledgedVictoryRewardBattleIds.includes(scenario.id) &&
JSON.stringify(migratedLegacySave.acknowledgedVictoryRewardCategories[scenario.id]) === JSON.stringify(expectedCategories) &&
JSON.stringify(migratedLegacySave.dismissedVictoryRewardNoticeBattleIds) === JSON.stringify([]) &&
getPendingCampaignVictoryRewardReport() === undefined,
`A legacy all-or-nothing acknowledgement must migrate safely without requiring a notice-dismissal field: ${JSON.stringify(migratedLegacySave)}`
);
const partialSave = structuredClone(persistedFullState);
partialSave.acknowledgedVictoryRewardBattleIds = [];
partialSave.acknowledgedVictoryRewardCategories = {
[scenario.id]: ['equipment', 'unknown-category'],
'second-battle-yellow-turban-pursuit': ['equipment']
};
partialSave.dismissedVictoryRewardNoticeBattleIds = [
scenario.id,
scenario.id,
'second-battle-yellow-turban-pursuit',
'unknown-battle'
];
values.set(campaignStorageKey, JSON.stringify(partialSave));
const normalizedPartialSave = loadCampaignState();
assert(
JSON.stringify(normalizedPartialSave.acknowledgedVictoryRewardCategories) === JSON.stringify({ [scenario.id]: ['equipment'] }) &&
!normalizedPartialSave.acknowledgedVictoryRewardBattleIds.includes(scenario.id) &&
JSON.stringify(normalizedPartialSave.dismissedVictoryRewardNoticeBattleIds) === JSON.stringify([scenario.id]) &&
getPendingCampaignVictoryRewardCategories(scenario.id).includes('supplies') &&
getPendingCampaignVictoryRewardNoticeReport() === undefined,
`Normalization must retain a valid dismissal while dropping duplicate, unknown, and non-victory battle ids: ${JSON.stringify(normalizedPartialSave)}`
);
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 &&
getPendingCampaignVictoryRewardNoticeReport()?.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.'
);
dismissCampaignVictoryRewardNotice(secondScenario.id);
assert(
getPendingCampaignVictoryRewardReport()?.battleId === secondScenario.id &&
getPendingCampaignVictoryRewardNoticeReport() === undefined &&
JSON.stringify(getPendingCampaignVictoryRewardCategories(secondScenario.id)) === JSON.stringify(['gold', 'supplies']),
'Dismissing the latest notice must preserve all NEW categories without resurfacing an older reward as another arrival modal.'
);
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, persisted notice dismissal, cross-victory visibility, normalization, and legacy migration.');
} finally {
await server.close();
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}