Tighten sortie scenario data gates

This commit is contained in:
2026-07-05 12:32:59 +09:00
parent b383dbf245
commit 43a25f4703

View File

@@ -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);
}
});
}