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', 'guan-yu', 'rebel-1']), validUsableIds: new Set(['roar', 'fireTactic', 'aid', 'bean', 'salve', 'wine']), validItemIds: new Set(['bean', 'salve', 'wine']), validEquipmentItemIds: new Set(['training-sword', 'cloth-armor', 'grain-pouch']), validEquipmentSlotItems: { weapon: new Set(['training-sword']), armor: new Set(['cloth-armor']), accessory: new Set(['grain-pouch']) }, validTriggeredBattleEventIds: new Set(['opening', 'leader-wavering', 'objective-village-approach', 'objective-village-achieved']) }; const validState = createValidBattleSaveState(); const patchUnit = (index, patch) => validState.units.map((unit, unitIndex) => (unitIndex === index ? { ...unit, ...patch } : unit)); 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' }], ['unknown acted unit id', { actedUnitIds: ['ghost-unit'] }], ['duplicate acted unit id', { actedUnitIds: ['liu-bei', 'liu-bei'] }], ['invalid battleLog', { battleLog: 'hit' }], ['too many battle log entries', { battleLog: Array.from({ length: 11 }, (_, index) => `Log ${index}`) }], ['too long battle log entry', { battleLog: ['x'.repeat(97)] }], ['invalid attackIntents', { attackIntents: [{ attackerId: 'liu-bei' }] }], ['unknown attack intent unit id', { attackIntents: [{ attackerId: 'liu-bei', targetId: 'ghost-unit' }] }], ['duplicate attack intent attacker id', { attackIntents: [{ attackerId: 'liu-bei', targetId: 'rebel-1' }, { attackerId: 'liu-bei', targetId: 'rebel-1' }] }], ['invalid units array', { units: {} }], ['invalid unit exp threshold', { units: patchUnit(0, { exp: 100 }) }], ['invalid max-level unit exp threshold', { units: patchUnit(0, { level: 99, exp: 101 }) }], ['invalid unit hp', { units: patchUnit(0, { hp: 99, maxHp: 30 }) }], ['invalid unit x', { units: patchUnit(0, { x: 12 }) }], ['invalid unit direction', { units: patchUnit(0, { direction: 'down' }) }], ['missing equipment slot', { units: patchUnit(0, { equipment: { weapon: createEquipmentSet().weapon, armor: createEquipmentSet().armor } }) }], ['extra equipment slot', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), charm: createEquipmentSet().accessory } }) }], ['unknown equipment item id', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'phantom-sword', level: 1, exp: 0 } } }) }], ['wrong equipment slot item id', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'cloth-armor', level: 1, exp: 0 } } }) }], ['invalid equipment level', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 0, exp: 0 } } }) }], ['too high equipment level', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 10, exp: 0 } } }) }], ['invalid equipment exp', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 1, exp: -1 } } }) }], ['invalid equipment exp threshold', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 1, exp: 70 } } }) }], ['invalid max-level equipment exp threshold', { units: patchUnit(0, { equipment: { ...createEquipmentSet(), weapon: { itemId: 'training-sword', level: 9, exp: 231 } } }) }], ['unknown unit id', { units: patchUnit(0, { id: 'ghost-unit' }) }], ['duplicate unit id', { units: [validState.units[0], { ...validState.units[0], x: 2 }, validState.units[2]] }], ['invalid bonds', { bonds: [{ ...validState.bonds[0], unitIds: ['liu-bei'] }] }], ['unknown bond unit id', { bonds: [{ ...validState.bonds[0], unitIds: ['liu-bei', 'ghost-unit'] }] }], ['invalid bond level', { bonds: [{ ...validState.bonds[0], level: 0 }] }], ['too high bond level', { bonds: [{ ...validState.bonds[0], level: 101 }] }], ['invalid bond exp', { bonds: [{ ...validState.bonds[0], exp: -1 }] }], ['invalid bond exp threshold', { bonds: [{ ...validState.bonds[0], level: 1, exp: 100 }] }], ['invalid max-level bond exp threshold', { bonds: [{ ...validState.bonds[0], level: 100, exp: 101 }] }], ['invalid bond battle exp', { bonds: [{ ...validState.bonds[0], battleExp: 1.5 }] }], ['invalid item stock count', { itemStocks: { 'liu-bei': { bean: -1 } } }], ['unknown item stock unit id', { itemStocks: { 'ghost-unit': { bean: 1 } } }], ['unknown item stock item id', { itemStocks: { 'liu-bei': { phantomItem: 1 } } }], ['invalid buff turns', { battleBuffs: [{ ...validState.battleBuffs[0], turns: 0 }] }], ['unknown buff unit id', { battleBuffs: [{ ...validState.battleBuffs[0], unitId: 'ghost-unit' }] }], ['duplicate buff unit id', { battleBuffs: [validState.battleBuffs[0], { ...validState.battleBuffs[0] }] }], ['invalid status kind', { battleStatuses: [{ ...validState.battleStatuses[0], kind: 'poison' }] }], ['unknown status unit id', { battleStatuses: [{ ...validState.battleStatuses[0], unitId: 'ghost-unit' }] }], ['duplicate status unit kind', { battleStatuses: [validState.battleStatuses[0], { ...validState.battleStatuses[0] }] }], ['invalid stats shape', { battleStats: { 'liu-bei': { damageDealt: 10 } } }], ['unknown stats unit id', { battleStats: { 'ghost-unit': validState.battleStats['liu-bei'] } }], ['invalid enemy usable keys', { enemyUsableUseKeys: [1] }], ['malformed enemy usable key', { enemyUsableUseKeys: ['rebel-1:shout'] }], ['wrong battle enemy usable key', { enemyUsableUseKeys: ['other-battle:rebel-1:roar'] }], ['unknown enemy usable unit id', { enemyUsableUseKeys: ['first-battle-zhuo-commandery:ghost-unit:roar'] }], ['unknown enemy usable id', { enemyUsableUseKeys: ['first-battle-zhuo-commandery:rebel-1:phantom'] }], ['duplicate enemy usable key', { enemyUsableUseKeys: ['first-battle-zhuo-commandery:rebel-1:roar', 'first-battle-zhuo-commandery:rebel-1:roar'] }], ['invalid triggered events', { triggeredBattleEvents: [1] }], ['unknown triggered event', { triggeredBattleEvents: ['phantom-event'] }], ['duplicate triggered event', { triggeredBattleEvents: ['opening', 'opening'] }], ['too long triggered event', { triggeredBattleEvents: ['x'.repeat(97)] }] ]; 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', 'rebel-1', 'zhang-fei']) }), '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: createEquipmentSet() }, { id: 'guan-yu', level: 2, exp: 5, hp: 32, maxHp: 34, attack: 14, move: 4, x: 2, y: 2, direction: 'south', equipment: createEquipmentSet() }, { id: 'rebel-1', level: 1, exp: 0, hp: 20, maxHp: 20, attack: 8, move: 3, x: 8, y: 4, direction: 'west', equipment: createEquipmentSet() } ], 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: ['first-battle-zhuo-commandery:rebel-1:roar'], triggeredBattleEvents: ['opening'] }; } function createEquipmentSet() { return { weapon: { itemId: 'training-sword', level: 1, exp: 0 }, armor: { itemId: 'cloth-armor', level: 1, exp: 0 }, accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } }; } function assert(condition, message) { if (!condition) { throw new Error(message); } }