fix: align battle objective terrain centers
This commit is contained in:
@@ -8,6 +8,7 @@ const server = await createServer({
|
||||
|
||||
try {
|
||||
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
||||
const { secureTerrainCenterOverrides } = await server.ssrLoadModule('/src/game/data/secureTerrainOverrides.ts');
|
||||
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');
|
||||
@@ -47,11 +48,17 @@ try {
|
||||
|
||||
validateMap(errors, scenario, terrainKeys, context);
|
||||
validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, context);
|
||||
validateObjectives(errors, scenario, context);
|
||||
validateObjectives(errors, scenario, terrainKeys, context);
|
||||
validateTacticalGuide(errors, scenario, tacticalGuideEventKeys, context);
|
||||
validateSortie(errors, scenario, classKeys, formationRoles, terrainRules, context);
|
||||
validateRewards(errors, scenario, scenarioIds, itemNames, knownRewardUnitIds, context);
|
||||
});
|
||||
const secureTerrainOverrideCount = validateSecureTerrainOverrides(
|
||||
errors,
|
||||
battleScenarios,
|
||||
secureTerrainCenterOverrides,
|
||||
terrainKeys
|
||||
);
|
||||
|
||||
if (errors.length) {
|
||||
console.error(`Battle scenario data verification failed with ${errors.length} issue(s):`);
|
||||
@@ -67,8 +74,13 @@ try {
|
||||
0
|
||||
);
|
||||
const tacticalGuideCount = scenarioEntries.reduce((total, [, scenario]) => total + (scenario.tacticalGuide ? 1 : 0), 0);
|
||||
const secureTerrainObjectiveCount = scenarioEntries.reduce(
|
||||
(total, [, scenario]) =>
|
||||
total + scenario.objectives.filter((objective) => objective.kind === 'secure-terrain').length,
|
||||
0
|
||||
);
|
||||
console.log(
|
||||
`Verified ${scenarioEntries.length} battle scenarios, ${unitCount} unit placements, maps, opening briefs, objectives, sortie data, ${tacticalGuideCount} tactical guides, equipment, and ${rewardCount} reward labels.`
|
||||
`Verified ${scenarioEntries.length} battle scenarios, ${unitCount} unit placements, maps, opening briefs, objectives (${secureTerrainObjectiveCount} secure-terrain centers, ${secureTerrainOverrideCount} explicit overrides), sortie data, ${tacticalGuideCount} tactical guides, equipment, and ${rewardCount} reward labels.`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -171,9 +183,10 @@ function validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, co
|
||||
}
|
||||
}
|
||||
|
||||
function validateObjectives(errors, scenario, context) {
|
||||
function validateObjectives(errors, scenario, terrainKeys, context) {
|
||||
const unitIds = new Set(scenario.units.map((unit) => unit.id));
|
||||
const objectiveIds = new Set();
|
||||
const secureTerrainCenters = new Map();
|
||||
|
||||
if (!scenario.leaderUnitId || !unitIds.has(scenario.leaderUnitId)) {
|
||||
errors.push(`${context}: leaderUnitId "${scenario.leaderUnitId}" is missing from units`);
|
||||
@@ -196,12 +209,139 @@ function validateObjectives(errors, scenario, context) {
|
||||
if (objective.unitId && !unitIds.has(objective.unitId)) {
|
||||
errors.push(`${context}/${objective.id}: references missing unit "${objective.unitId}"`);
|
||||
}
|
||||
if (objective.targetTile && !isInBounds(objective.targetTile, scenario.map)) {
|
||||
if (objective.kind === 'secure-terrain') {
|
||||
const center = objective.targetTile;
|
||||
if (center && Number.isInteger(center.x) && Number.isInteger(center.y)) {
|
||||
const centerKey = `${center.x},${center.y}`;
|
||||
const existingObjectiveId = secureTerrainCenters.get(centerKey);
|
||||
if (secureTerrainCenters.has(centerKey)) {
|
||||
errors.push(
|
||||
`${context}/${objective.id}: secure-terrain center ${centerKey} duplicates objective "${existingObjectiveId}"`
|
||||
);
|
||||
} else {
|
||||
secureTerrainCenters.set(centerKey, objective.id);
|
||||
}
|
||||
}
|
||||
validateSecureTerrainObjective(errors, scenario, objective, terrainKeys, `${context}/${objective.id}`);
|
||||
} else if (objective.targetTile && !isInBounds(objective.targetTile, scenario.map)) {
|
||||
errors.push(`${context}/${objective.id}: target tile ${objective.targetTile.x},${objective.targetTile.y} is outside map`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validateSecureTerrainOverrides(errors, battleScenarios, overridesByBattle, terrainKeys) {
|
||||
if (!overridesByBattle || typeof overridesByBattle !== 'object' || Array.isArray(overridesByBattle)) {
|
||||
errors.push('secureTerrainCenterOverrides must be an object keyed by battle id');
|
||||
return 0;
|
||||
}
|
||||
|
||||
let overrideCount = 0;
|
||||
Object.entries(overridesByBattle).forEach(([battleId, overrides]) => {
|
||||
const context = `secureTerrainCenterOverrides/${battleId}`;
|
||||
const scenario = battleScenarios[battleId];
|
||||
if (!scenario) {
|
||||
errors.push(`${context}: references unknown battle id`);
|
||||
}
|
||||
if (!Array.isArray(overrides)) {
|
||||
errors.push(`${context}: overrides must be an array`);
|
||||
return;
|
||||
}
|
||||
|
||||
overrideCount += overrides.length;
|
||||
const centers = new Set();
|
||||
overrides.forEach((override, index) => {
|
||||
const overrideContext = `${context}[${index}]`;
|
||||
if (!override || typeof override !== 'object' || Array.isArray(override)) {
|
||||
errors.push(`${overrideContext}: override must be an object`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y, terrain } = override;
|
||||
const hasIntegerCenter = Number.isInteger(x) && Number.isInteger(y);
|
||||
if (!hasIntegerCenter) {
|
||||
errors.push(`${overrideContext}: center must use integer coordinates, received ${x},${y}`);
|
||||
} else {
|
||||
const centerKey = `${x},${y}`;
|
||||
if (centers.has(centerKey)) {
|
||||
errors.push(`${overrideContext}: duplicate override center ${centerKey}`);
|
||||
}
|
||||
centers.add(centerKey);
|
||||
}
|
||||
|
||||
const hasValidTerrain = typeof terrain === 'string' && terrainKeys.has(terrain);
|
||||
if (!hasValidTerrain) {
|
||||
errors.push(`${overrideContext}: unknown terrain "${terrain}"`);
|
||||
}
|
||||
if (!scenario || !hasIntegerCenter) {
|
||||
return;
|
||||
}
|
||||
if (!isInBounds({ x, y }, scenario.map)) {
|
||||
errors.push(`${overrideContext}: center ${x},${y} is outside ${scenario.map.width}x${scenario.map.height} map`);
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingObjective = hasValidTerrain
|
||||
? scenario.objectives.find(
|
||||
(objective) =>
|
||||
objective.kind === 'secure-terrain' &&
|
||||
objective.terrain === terrain &&
|
||||
objective.targetTile?.x === x &&
|
||||
objective.targetTile.y === y
|
||||
)
|
||||
: undefined;
|
||||
if (hasValidTerrain && !matchingObjective) {
|
||||
errors.push(`${overrideContext}: center ${x},${y}/"${terrain}" has no matching secure-terrain objective`);
|
||||
}
|
||||
|
||||
const centerTerrain = scenario.map.terrain?.[y]?.[x];
|
||||
if (hasValidTerrain && centerTerrain !== terrain) {
|
||||
errors.push(`${overrideContext}: final map center ${x},${y} is "${centerTerrain}", expected "${terrain}"`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return overrideCount;
|
||||
}
|
||||
|
||||
function validateSecureTerrainObjective(errors, scenario, objective, terrainKeys, context) {
|
||||
let hasValidTerrain = true;
|
||||
if (typeof objective.terrain !== 'string' || objective.terrain.trim().length === 0) {
|
||||
errors.push(`${context}: secure-terrain objective requires a terrain`);
|
||||
hasValidTerrain = false;
|
||||
} else if (!terrainKeys.has(objective.terrain)) {
|
||||
errors.push(`${context}: secure-terrain objective references unknown terrain "${objective.terrain}"`);
|
||||
hasValidTerrain = false;
|
||||
}
|
||||
|
||||
const center = objective.targetTile;
|
||||
if (!center || typeof center !== 'object') {
|
||||
errors.push(`${context}: secure-terrain objective requires a targetTile center`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(center.x) || !Number.isInteger(center.y)) {
|
||||
errors.push(`${context}: secure-terrain center must use integer coordinates, received ${center.x},${center.y}`);
|
||||
return;
|
||||
}
|
||||
if (!isInBounds(center, scenario.map)) {
|
||||
errors.push(
|
||||
`${context}: secure-terrain center ${center.x},${center.y} is outside ${scenario.map.width}x${scenario.map.height} map`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (center.radius !== undefined && (!Number.isInteger(center.radius) || center.radius < 0)) {
|
||||
errors.push(`${context}: secure-terrain radius must be a non-negative integer, received ${center.radius}`);
|
||||
}
|
||||
|
||||
const terrainRow = scenario.map.terrain?.[center.y];
|
||||
const centerTerrain = Array.isArray(terrainRow) ? terrainRow[center.x] : undefined;
|
||||
if (hasValidTerrain && centerTerrain !== undefined && centerTerrain !== objective.terrain) {
|
||||
errors.push(
|
||||
`${context}: secure-terrain center ${center.x},${center.y} is "${centerTerrain}", expected "${objective.terrain}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateOpeningObjectiveLines(errors, scenario, context) {
|
||||
if (!Array.isArray(scenario.openingObjectiveLines) || scenario.openingObjectiveLines.length === 0) {
|
||||
errors.push(`${context}: openingObjectiveLines must include at least one line`);
|
||||
|
||||
Reference in New Issue
Block a user