189 lines
8.2 KiB
JavaScript
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);
|
|
}
|
|
}
|