148 lines
5.5 KiB
JavaScript
148 lines
5.5 KiB
JavaScript
import { createServer } from 'vite';
|
|
|
|
const validRanks = new Set(['common', 'treasure']);
|
|
|
|
const server = await createServer({
|
|
logLevel: 'error',
|
|
server: { middlewareMode: true },
|
|
appType: 'custom'
|
|
});
|
|
|
|
try {
|
|
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
|
const { campaignRecruitUnits } = await server.ssrLoadModule('/src/game/data/scenario.ts');
|
|
const { itemCatalog, equipmentSlots, equipmentExpToNext } = await server.ssrLoadModule('/src/game/data/battleItems.ts');
|
|
|
|
const errors = [];
|
|
const itemEntries = Object.entries(itemCatalog);
|
|
const itemKeys = new Set(itemEntries.map(([itemId]) => itemId));
|
|
const slotKeys = new Set(equipmentSlots);
|
|
|
|
validateItemCatalog(errors, itemEntries, slotKeys);
|
|
validateUnitEquipment(errors, collectBattleUnits(battleScenarios), itemCatalog, itemKeys, equipmentSlots, equipmentExpToNext);
|
|
validateUnitEquipment(
|
|
errors,
|
|
(campaignRecruitUnits ?? []).map((unit) => ({ context: `campaignRecruitUnits/${unit.id}`, unit })),
|
|
itemCatalog,
|
|
itemKeys,
|
|
equipmentSlots,
|
|
equipmentExpToNext
|
|
);
|
|
|
|
if (errors.length) {
|
|
console.error(`Equipment catalog data verification failed with ${errors.length} issue(s):`);
|
|
errors.slice(0, 120).forEach((error) => console.error(`- ${error}`));
|
|
if (errors.length > 120) {
|
|
console.error(`- ...and ${errors.length - 120} more`);
|
|
}
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log(
|
|
`Verified ${itemEntries.length} equipment catalog items and ${Object.keys(battleScenarios).length} battle equipment loadouts.`
|
|
);
|
|
}
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
|
|
function validateItemCatalog(errors, itemEntries, slotKeys) {
|
|
const names = new Map();
|
|
|
|
itemEntries.forEach(([itemId, item]) => {
|
|
const context = `itemCatalog/${itemId}`;
|
|
if (!item || typeof item !== 'object') {
|
|
errors.push(`${context}: must be an item object`);
|
|
return;
|
|
}
|
|
|
|
if (item.id !== itemId) {
|
|
errors.push(`${context}: item.id "${item.id}" does not match catalog key`);
|
|
}
|
|
assertNonEmptyString(errors, item.name, `${context}: name`);
|
|
assertNonEmptyString(errors, item.description, `${context}: description`);
|
|
|
|
if (!slotKeys.has(item.slot)) {
|
|
errors.push(`${context}: unknown equipment slot "${item.slot}"`);
|
|
}
|
|
if (!validRanks.has(item.rank)) {
|
|
errors.push(`${context}: unknown rank "${item.rank}"`);
|
|
}
|
|
validateSlotBonus(errors, item, context);
|
|
if (!Array.isArray(item.effects) || item.effects.length === 0) {
|
|
errors.push(`${context}: effects must contain at least one entry`);
|
|
} else {
|
|
item.effects.forEach((effect, index) => {
|
|
assertNonEmptyString(errors, effect, `${context}: effects[${index}]`);
|
|
});
|
|
}
|
|
|
|
['attackBonus', 'defenseBonus', 'strategyBonus'].forEach((bonusKey) => {
|
|
const value = item[bonusKey];
|
|
if (value !== undefined && (!Number.isInteger(value) || value < 0 || value > 20)) {
|
|
errors.push(`${context}: invalid ${bonusKey} ${value}`);
|
|
}
|
|
});
|
|
|
|
if (typeof item.name === 'string') {
|
|
const existingItemId = names.get(item.name);
|
|
if (existingItemId) {
|
|
errors.push(`${context}: duplicate item name "${item.name}" also used by ${existingItemId}`);
|
|
}
|
|
names.set(item.name, itemId);
|
|
}
|
|
});
|
|
}
|
|
|
|
function validateSlotBonus(errors, item, context) {
|
|
if (item.slot === 'weapon' && !Number.isInteger(item.attackBonus) && !Number.isInteger(item.strategyBonus)) {
|
|
errors.push(`${context}: weapon should provide attackBonus or strategyBonus`);
|
|
}
|
|
if (item.slot === 'armor' && !Number.isInteger(item.defenseBonus)) {
|
|
errors.push(`${context}: armor should provide defenseBonus`);
|
|
}
|
|
}
|
|
|
|
function validateUnitEquipment(errors, unitEntries, itemCatalog, itemKeys, equipmentSlots, equipmentExpToNext) {
|
|
unitEntries.forEach(({ context, unit }) => {
|
|
if (!unit?.equipment) {
|
|
errors.push(`${context}: missing equipment`);
|
|
return;
|
|
}
|
|
|
|
equipmentSlots.forEach((slot) => {
|
|
const state = unit.equipment[slot];
|
|
if (!state) {
|
|
errors.push(`${context}: missing ${slot} equipment`);
|
|
return;
|
|
}
|
|
if (!itemKeys.has(state.itemId)) {
|
|
errors.push(`${context}: unknown ${slot} item "${state.itemId}"`);
|
|
} else if (itemCatalog[state.itemId].slot !== slot) {
|
|
errors.push(`${context}: ${slot} item "${state.itemId}" belongs to ${itemCatalog[state.itemId].slot}`);
|
|
}
|
|
if (!Number.isInteger(state.level) || state.level < 1 || state.level > 9) {
|
|
errors.push(`${context}: invalid ${slot} level ${state.level}`);
|
|
}
|
|
if (!Number.isInteger(state.exp) || state.exp < 0) {
|
|
errors.push(`${context}: invalid ${slot} exp ${state.exp}`);
|
|
} else if (state.level < 9 && state.exp >= equipmentExpToNext(state.level)) {
|
|
errors.push(`${context}: ${slot} exp ${state.exp} should be below next level threshold ${equipmentExpToNext(state.level)}`);
|
|
} else if (state.level >= 9 && state.exp > equipmentExpToNext(state.level)) {
|
|
errors.push(`${context}: max-level ${slot} exp ${state.exp} should be at most ${equipmentExpToNext(state.level)}`);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function collectBattleUnits(battleScenarios) {
|
|
return Object.entries(battleScenarios).flatMap(([battleId, scenario]) =>
|
|
scenario.units.map((unit) => ({ context: `${battleId}/${unit.id}`, unit }))
|
|
);
|
|
}
|
|
|
|
function assertNonEmptyString(errors, value, context) {
|
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
errors.push(`${context} must be a non-empty string`);
|
|
}
|
|
}
|