diff --git a/scripts/verify-battle-scenario-data.mjs b/scripts/verify-battle-scenario-data.mjs index 8a1abeb..8368643 100644 --- a/scripts/verify-battle-scenario-data.mjs +++ b/scripts/verify-battle-scenario-data.mjs @@ -11,6 +11,7 @@ try { const { campaignRecruitUnits } = await server.ssrLoadModule('/src/game/data/scenario.ts'); const { unitClasses, terrainRules } = await server.ssrLoadModule('/src/game/data/battleRules.ts'); const { itemCatalog, equipmentSlots } = await server.ssrLoadModule('/src/game/data/battleItems.ts'); + const { sortieFormationRoles } = await server.ssrLoadModule('/src/game/data/sortieDeployment.ts'); const scenarioEntries = Object.entries(battleScenarios); const scenarioIds = new Set(scenarioEntries.map(([id]) => id)); @@ -18,6 +19,7 @@ try { const terrainKeys = new Set(Object.keys(terrainRules)); const itemKeys = new Set(Object.keys(itemCatalog)); const itemNames = new Set(Object.values(itemCatalog).map((item) => item.name)); + const formationRoles = new Set(sortieFormationRoles); const knownRewardUnitIds = new Set([ ...scenarioEntries.flatMap(([, scenario]) => scenario.units.map((unit) => unit.id)), ...(campaignRecruitUnits ?? []).map((unit) => unit.id) @@ -36,7 +38,7 @@ try { validateMap(errors, scenario, terrainKeys, context); validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, context); validateObjectives(errors, scenario, context); - validateSortie(errors, scenario, classKeys, context); + validateSortie(errors, scenario, classKeys, formationRoles, context); validateRewards(errors, scenario, scenarioIds, itemNames, knownRewardUnitIds, context); }); @@ -190,7 +192,7 @@ function validateObjectives(errors, scenario, context) { }); } -function validateSortie(errors, scenario, classKeys, context) { +function validateSortie(errors, scenario, classKeys, formationRoles, context) { const sortie = scenario.sortie; if (!sortie) { return; @@ -204,20 +206,55 @@ function validateSortie(errors, scenario, classKeys, context) { errors.push(`${context}: requiredUnits count exceeds sortieLimit`); } - [...(sortie.requiredUnits ?? []), ...(sortie.excludedUnits ?? [])].forEach((unitId) => { + const requiredUnits = sortie.requiredUnits ?? []; + const excludedUnits = sortie.excludedUnits ?? []; + const requiredUnitIds = new Set(); + const excludedUnitIds = new Set(); + + requiredUnits.forEach((unitId) => { + if (requiredUnitIds.has(unitId)) { + errors.push(`${context}: duplicate required unit "${unitId}"`); + } + requiredUnitIds.add(unitId); + }); + excludedUnits.forEach((unitId) => { + if (excludedUnitIds.has(unitId)) { + errors.push(`${context}: duplicate excluded unit "${unitId}"`); + } + excludedUnitIds.add(unitId); + if (requiredUnitIds.has(unitId)) { + errors.push(`${context}: unit "${unitId}" cannot be both required and excluded`); + } + }); + + [...requiredUnits, ...excludedUnits].forEach((unitId) => { if (!unitIds.has(unitId)) { errors.push(`${context}: sortie references missing unit "${unitId}"`); } }); + const recommendedUnitIds = new Set(); sortie.recommendedUnits.forEach((recommendation) => { + if (recommendedUnitIds.has(recommendation.unitId)) { + errors.push(`${context}: duplicate recommended unit "${recommendation.unitId}"`); + } + recommendedUnitIds.add(recommendation.unitId); if (!unitIds.has(recommendation.unitId)) { errors.push(`${context}: recommended unit "${recommendation.unitId}" is missing from units`); } + if (excludedUnitIds.has(recommendation.unitId)) { + errors.push(`${context}: excluded unit "${recommendation.unitId}" cannot also be recommended`); + } + if (recommendation.role && !formationRoles.has(recommendation.role)) { + errors.push(`${context}/${recommendation.unitId}: invalid recommendation role "${recommendation.role}"`); + } assertNonEmptyString(errors, recommendation.reason, `${context}/${recommendation.unitId}: recommendation reason`); }); sortie.recommendedClasses.forEach((recommendation) => { assertNonEmptyString(errors, recommendation.label, `${context}: recommended class label`); assertNonEmptyString(errors, recommendation.reason, `${context}/${recommendation.label}: recommended class reason`); + if (!Array.isArray(recommendation.classKeys) || recommendation.classKeys.length === 0) { + errors.push(`${context}/${recommendation.label}: recommended class must include at least one classKey`); + } recommendation.classKeys.forEach((classKey) => { if (!classKeys.has(classKey)) { errors.push(`${context}: recommended class "${recommendation.label}" references unknown classKey "${classKey}"`); @@ -229,11 +266,15 @@ function validateSortie(errors, scenario, classKeys, context) { errors.push(`${context}: deploymentSlots count is lower than sortieLimit`); } const deploymentTiles = new Set(); + const deploymentUnitIds = new Set(); sortie.deploymentSlots.forEach((slot) => { if (!isInBounds(slot, scenario.map)) { errors.push(`${context}: deployment slot ${slot.x},${slot.y} is outside map`); return; } + if (slot.role && !formationRoles.has(slot.role)) { + errors.push(`${context}: deployment slot ${slot.x},${slot.y} has invalid role "${slot.role}"`); + } const tileKey = `${slot.x},${slot.y}`; if (deploymentTiles.has(tileKey)) { errors.push(`${context}: duplicate deployment slot ${tileKey}`); @@ -242,6 +283,12 @@ function validateSortie(errors, scenario, classKeys, context) { if (slot.unitId && !unitIds.has(slot.unitId)) { errors.push(`${context}: deployment slot references missing unit "${slot.unitId}"`); } + if (slot.unitId) { + if (deploymentUnitIds.has(slot.unitId)) { + errors.push(`${context}: duplicate deployment unit slot "${slot.unitId}"`); + } + deploymentUnitIds.add(slot.unitId); + } }); }