import { readFile } from 'node:fs/promises'; import { createServer } from 'vite'; const storage = new Map(); globalThis.window = { localStorage: { getItem(key) { return storage.has(key) ? storage.get(key) : null; }, setItem(key, value) { storage.set(key, String(value)); }, removeItem(key) { storage.delete(key); } } }; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true, hmr: false }, appType: 'custom' }); try { const { calculateEquipmentBonuses } = await server.ssrLoadModule('/src/game/data/battleItems.ts'); const { applyCampaignUnitProgress } = await server.ssrLoadModule('/src/game/data/unitProgress.ts'); const { campaignStorageKey, createInitialCampaignState, loadCampaignState } = await server.ssrLoadModule('/src/game/state/campaignState.ts'); const equipment = { weapon: { itemId: 'green-dragon-glaive', level: 4, exp: 12 }, armor: { itemId: 'oath-robe', level: 3, exp: 8 }, accessory: { itemId: 'bravery-token', level: 5, exp: 4 } }; const equipmentBonuses = calculateEquipmentBonuses(equipment); assert( sameJson(equipmentBonuses, { attack: 9, defense: 4, strategy: 5 }), `Expected item and slot-level equipment bonuses to share one calculation: ${JSON.stringify(equipmentBonuses)}` ); const scenarioUnit = createUnit({ equipment }); const savedProgress = createUnit({ level: 5, exp: 42, hp: 27, maxHp: 38, attack: 14, stats: { might: 81, intelligence: 85, leadership: 90, agility: 73, luck: 91 }, equipment }); const merged = applyCampaignUnitProgress(scenarioUnit, savedProgress); assert( merged.level === 5 && merged.exp === 42 && merged.hp === 27 && merged.maxHp === 38 && merged.attack === 14 && sameJson(merged.stats, savedProgress.stats) && sameJson(merged.equipment, savedProgress.equipment) && merged.x === scenarioUnit.x && merged.y === scenarioUnit.y, `Expected the next battle to preserve all saved growth while keeping authored deployment: ${JSON.stringify(merged)}` ); merged.stats.might += 20; merged.equipment.weapon.level += 1; assert( savedProgress.stats.might === 81 && savedProgress.equipment.weapon.level === 4, 'Expected merged campaign progress to be deeply cloned.' ); const legacyProgress = createUnit({ level: 5, exp: 17, hp: 28, equipment }); const recovered = applyCampaignUnitProgress(scenarioUnit, legacyProgress); assert( recovered.level === 5 && recovered.attack === 11 && recovered.maxHp === 36 && sameJson(recovered.stats, { might: 78, intelligence: 80, leadership: 86, agility: 70, luck: 88 }), `Expected older saves that lost level-up fields to recover authored growth floors: ${JSON.stringify(recovered)}` ); const rawSave = createInitialCampaignState(); rawSave.step = 'first-camp'; rawSave.roster = [{ ...savedProgress, attack: '14', stats: Object.fromEntries(Object.entries(savedProgress.stats).map(([key, value]) => [key, String(value)])), equipment: Object.fromEntries( Object.entries(savedProgress.equipment).map(([slot, state]) => [slot, { ...state, level: String(state.level), exp: String(state.exp) }]) ) }]; storage.set(campaignStorageKey, JSON.stringify(rawSave)); const loaded = loadCampaignState(); const loadedUnit = loaded.roster[0]; const loadedMerge = applyCampaignUnitProgress(scenarioUnit, loadedUnit); assert( loadedUnit.attack === 14 && sameJson(loadedUnit.stats, savedProgress.stats) && sameJson(loadedUnit.equipment, savedProgress.equipment) && loadedMerge.attack === 14 && sameJson(loadedMerge.stats, savedProgress.stats), `Expected legacy-compatible normalization and reload to retain every growth field: ${JSON.stringify({ loadedUnit, loadedMerge })}` ); const [battleSource, campSource] = await Promise.all([ readFile('src/game/scenes/BattleScene.ts', 'utf8'), readFile('src/game/scenes/CampScene.ts', 'utf8') ]); assert( battleSource.includes('return mergeCampaignUnitProgress(unit, progress);'), 'Expected BattleScene campaign hydration to use the shared full-progress merger.' ); assert( countOccurrences(battleSource, 'calculateEquipmentBonuses(unit.equipment).') === 3 && countOccurrences(campSource, 'calculateEquipmentBonuses(unit.equipment).') === 3, 'Expected CampScene display and BattleScene combat to use the same three equipment bonus projections.' ); console.log('Verified full character-growth carryover, legacy growth-floor recovery, save normalization, and shared camp/battle equipment level bonuses.'); } finally { await server.close(); } function createUnit(overrides = {}) { return { id: 'liu-bei', name: '유비', faction: 'ally', className: '군주', classKey: 'lord', level: 3, exp: 0, hp: 32, maxHp: 32, attack: 9, move: 4, stats: { might: 76, intelligence: 78, leadership: 84, agility: 70, luck: 88 }, equipment: { weapon: { itemId: 'training-sword', level: 1, exp: 0 }, armor: { itemId: 'cloth-armor', level: 1, exp: 0 }, accessory: { itemId: 'grain-pouch', level: 1, exp: 0 } }, x: 3, y: 14, ...overrides }; } function countOccurrences(source, needle) { return source.split(needle).length - 1; } function sameJson(actual, expected) { return JSON.stringify(actual) === JSON.stringify(expected); } function assert(condition, message) { if (!condition) { throw new Error(message); } }