From 5ddcef68b21391dfca86e96dd7144391a2161eb6 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 08:32:21 +0900 Subject: [PATCH] Validate equipment catalog data --- package.json | 1 + scripts/verify-equipment-catalog-data.mjs | 132 ++++++++++++++++++++++ scripts/verify-release-candidate.mjs | 1 + 3 files changed, 134 insertions(+) create mode 100644 scripts/verify-equipment-catalog-data.mjs diff --git a/package.json b/package.json index 4649e66..1cea8b7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/verify-equipment-catalog-data.mjs b/scripts/verify-equipment-catalog-data.mjs new file mode 100644 index 0000000..7b8cf52 --- /dev/null +++ b/scripts/verify-equipment-catalog-data.mjs @@ -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`); + } +} diff --git a/scripts/verify-release-candidate.mjs b/scripts/verify-release-candidate.mjs index acead7d..102b2d9 100644 --- a/scripts/verify-release-candidate.mjs +++ b/scripts/verify-release-candidate.mjs @@ -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']);