Files
heros_web/scripts/verify-growth-consistency.mjs

165 lines
5.5 KiB
JavaScript

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);
}
}