Validate equipment catalog data
This commit is contained in:
132
scripts/verify-equipment-catalog-data.mjs
Normal file
132
scripts/verify-equipment-catalog-data.mjs
Normal file
@@ -0,0 +1,132 @@
|
||||
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 } = 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);
|
||||
validateUnitEquipment(
|
||||
errors,
|
||||
(campaignRecruitUnits ?? []).map((unit) => ({ context: `campaignRecruitUnits/${unit.id}`, unit })),
|
||||
itemCatalog,
|
||||
itemKeys,
|
||||
equipmentSlots
|
||||
);
|
||||
|
||||
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}"`);
|
||||
}
|
||||
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 validateUnitEquipment(errors, unitEntries, itemCatalog, itemKeys, equipmentSlots) {
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ try {
|
||||
runCommand('node', ['scripts/verify-battle-scenario-data.mjs']);
|
||||
runCommand('node', ['scripts/verify-camp-reward-data.mjs']);
|
||||
runCommand('node', ['scripts/verify-campaign-recruit-data.mjs']);
|
||||
runCommand('node', ['scripts/verify-equipment-catalog-data.mjs']);
|
||||
runCommand('node', ['scripts/verify-story-asset-data.mjs']);
|
||||
runCommand('node', ['scripts/verify-story-image-asset-data.mjs']);
|
||||
runCommand('node', ['scripts/verify-visual-asset-data.mjs']);
|
||||
|
||||
Reference in New Issue
Block a user