Validate equipment catalog data

This commit is contained in:
2026-07-05 08:32:21 +09:00
parent 86d107a7e4
commit 5ddcef68b2
3 changed files with 134 additions and 0 deletions

View File

@@ -15,6 +15,7 @@
"verify:campaign-data": "node scripts/verify-campaign-flow-data.mjs",
"verify:camp-rewards": "node scripts/verify-camp-reward-data.mjs",
"verify:campaign-recruits": "node scripts/verify-campaign-recruit-data.mjs",
"verify:equipment-catalog": "node scripts/verify-equipment-catalog-data.mjs",
"verify:story-assets": "node scripts/verify-story-asset-data.mjs",
"verify:story-images": "node scripts/verify-story-image-asset-data.mjs",
"verify:visual-assets": "node scripts/verify-visual-asset-data.mjs",

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

View File

@@ -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']);