147 lines
4.9 KiB
JavaScript
147 lines
4.9 KiB
JavaScript
import { createServer } from 'vite';
|
|
|
|
const server = await createServer({
|
|
logLevel: 'error',
|
|
server: { middlewareMode: true },
|
|
appType: 'custom'
|
|
});
|
|
|
|
try {
|
|
const { isValidBattleSaveState, normalizeBattleSaveSlot, parseBattleSaveState } = await server.ssrLoadModule(
|
|
'/src/game/state/battleSaveState.ts'
|
|
);
|
|
|
|
const options = {
|
|
expectedBattleId: 'first-battle-zhuo-commandery',
|
|
mapWidth: 12,
|
|
mapHeight: 8,
|
|
validUnitIds: new Set(['liu-bei'])
|
|
};
|
|
const validState = createValidBattleSaveState();
|
|
|
|
assert(normalizeBattleSaveSlot(0, 3) === 1, 'Expected slot 0 to normalize to slot 1.');
|
|
assert(normalizeBattleSaveSlot(Number.NaN, 3) === 1, 'Expected NaN slot to normalize to slot 1.');
|
|
assert(normalizeBattleSaveSlot(99, 3) === 3, 'Expected overflowing slot to clamp to slot count.');
|
|
assert(isValidBattleSaveState(validState, options), 'Expected valid battle save to pass validation.');
|
|
assert(parseBattleSaveState(JSON.stringify(validState), options)?.battleId === options.expectedBattleId, 'Expected valid JSON save to parse.');
|
|
assert(parseBattleSaveState('{', options) === undefined, 'Expected malformed JSON to be ignored.');
|
|
assert(parseBattleSaveState('', options) === undefined, 'Expected empty save payload to be ignored.');
|
|
|
|
const rejectedCases = [
|
|
['wrong battle id', { battleId: 'other-battle' }],
|
|
['missing battle id', { battleId: undefined }],
|
|
['invalid savedAt', { savedAt: 'not-a-date' }],
|
|
['invalid turnNumber', { turnNumber: 0 }],
|
|
['invalid activeFaction', { activeFaction: 'neutral' }],
|
|
['invalid rosterTab', { rosterTab: 'neutral' }],
|
|
['invalid actedUnitIds', { actedUnitIds: 'liu-bei' }],
|
|
['invalid battleLog', { battleLog: 'hit' }],
|
|
['invalid attackIntents', { attackIntents: [{ attackerId: 'liu-bei' }] }],
|
|
['invalid units array', { units: {} }],
|
|
['invalid unit hp', { units: [{ ...validState.units[0], hp: 99, maxHp: 30 }] }],
|
|
['invalid unit x', { units: [{ ...validState.units[0], x: 12 }] }],
|
|
['invalid unit direction', { units: [{ ...validState.units[0], direction: 'down' }] }],
|
|
['unknown unit id', { units: [{ ...validState.units[0], id: 'ghost-unit' }] }],
|
|
['duplicate unit id', { units: [{ ...validState.units[0] }, { ...validState.units[0], x: 2 }] }],
|
|
['invalid bonds', { bonds: [{ ...validState.bonds[0], unitIds: ['liu-bei'] }] }],
|
|
['invalid item stock count', { itemStocks: { 'liu-bei': { bean: -1 } } }],
|
|
['invalid buff turns', { battleBuffs: [{ ...validState.battleBuffs[0], turns: 0 }] }],
|
|
['invalid status kind', { battleStatuses: [{ ...validState.battleStatuses[0], kind: 'poison' }] }],
|
|
['invalid stats shape', { battleStats: { 'liu-bei': { damageDealt: 10 } } }],
|
|
['invalid enemy usable keys', { enemyUsableUseKeys: [1] }],
|
|
['invalid triggered events', { triggeredBattleEvents: [1] }]
|
|
];
|
|
|
|
rejectedCases.forEach(([label, patch]) => {
|
|
const candidate = { ...validState, ...patch };
|
|
assert(!isValidBattleSaveState(candidate, options), `Expected invalid battle save to be rejected: ${label}`);
|
|
});
|
|
|
|
assert(
|
|
!isValidBattleSaveState(validState, { ...options, validUnitIds: new Set(['liu-bei', 'guan-yu']) }),
|
|
'Expected save missing a current battle unit to be rejected.'
|
|
);
|
|
|
|
console.log('Verified battle save normalization and corrupted battle save rejection.');
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
|
|
function createValidBattleSaveState() {
|
|
return {
|
|
version: 1,
|
|
battleId: 'first-battle-zhuo-commandery',
|
|
campaignStep: 'first-battle',
|
|
savedAt: '2026-07-05T00:00:00.000Z',
|
|
turnNumber: 2,
|
|
activeFaction: 'ally',
|
|
rosterTab: 'ally',
|
|
actedUnitIds: ['liu-bei'],
|
|
attackIntents: [{ attackerId: 'liu-bei', targetId: 'rebel-1' }],
|
|
battleLog: ['Liu Bei attacked.'],
|
|
units: [
|
|
{
|
|
id: 'liu-bei',
|
|
level: 2,
|
|
exp: 10,
|
|
hp: 28,
|
|
maxHp: 30,
|
|
attack: 10,
|
|
move: 4,
|
|
x: 1,
|
|
y: 2,
|
|
direction: 'south',
|
|
equipment: {}
|
|
}
|
|
],
|
|
bonds: [
|
|
{
|
|
id: 'taoyuan',
|
|
unitIds: ['liu-bei', 'guan-yu'],
|
|
title: 'Oath',
|
|
level: 1,
|
|
exp: 0,
|
|
battleExp: 0,
|
|
description: 'Shared oath.'
|
|
}
|
|
],
|
|
itemStocks: { 'liu-bei': { bean: 2 } },
|
|
battleBuffs: [
|
|
{
|
|
unitId: 'liu-bei',
|
|
label: 'Focus',
|
|
turns: 2,
|
|
attackBonus: 1,
|
|
hitBonus: 10,
|
|
criticalBonus: 0
|
|
}
|
|
],
|
|
battleStatuses: [
|
|
{
|
|
unitId: 'rebel-1',
|
|
kind: 'burn',
|
|
label: 'Burn',
|
|
turns: 2,
|
|
power: 4
|
|
}
|
|
],
|
|
battleStats: {
|
|
'liu-bei': {
|
|
damageDealt: 12,
|
|
damageTaken: 3,
|
|
defeats: 1,
|
|
actions: 2,
|
|
support: 0
|
|
}
|
|
},
|
|
enemyUsableUseKeys: ['rebel-1:shout'],
|
|
triggeredBattleEvents: ['first-objective-near']
|
|
};
|
|
}
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(message);
|
|
}
|
|
}
|