import { createServer } from 'vite'; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true }, appType: 'custom' }); try { const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts'); const { unitClasses, terrainRules } = await server.ssrLoadModule('/src/game/data/battleRules.ts'); const { itemCatalog, equipmentSlots } = await server.ssrLoadModule('/src/game/data/battleItems.ts'); const scenarioEntries = Object.entries(battleScenarios); const scenarioIds = new Set(scenarioEntries.map(([id]) => id)); const classKeys = new Set(Object.keys(unitClasses)); const terrainKeys = new Set(Object.keys(terrainRules)); const itemKeys = new Set(Object.keys(itemCatalog)); const errors = []; scenarioEntries.forEach(([scenarioId, scenario]) => { const context = scenarioId; if (scenario.id !== scenarioId) { errors.push(`${context}: scenario id does not match battleScenarios key`); } assertNonEmptyString(errors, scenario.title, `${context}: title`); assertNonEmptyString(errors, scenario.victoryConditionLabel, `${context}: victory condition label`); assertNonEmptyString(errors, scenario.defeatConditionLabel, `${context}: defeat condition label`); validateMap(errors, scenario, terrainKeys, context); validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, context); validateObjectives(errors, scenario, context); validateSortie(errors, scenario, classKeys, context); validateRewards(errors, scenario, scenarioIds, context); }); if (errors.length) { console.error(`Battle scenario data verification failed with ${errors.length} issue(s):`); errors.slice(0, 80).forEach((error) => console.error(`- ${error}`)); if (errors.length > 80) { console.error(`- ...and ${errors.length - 80} more`); } process.exitCode = 1; } else { const unitCount = scenarioEntries.reduce((total, [, scenario]) => total + scenario.units.length, 0); console.log(`Verified ${scenarioEntries.length} battle scenarios, ${unitCount} unit placements, maps, objectives, sortie data, and equipment.`); } } finally { await server.close(); } function validateMap(errors, scenario, terrainKeys, context) { const { map } = scenario; if (!map || !Number.isInteger(map.width) || !Number.isInteger(map.height) || map.width < 1 || map.height < 1) { errors.push(`${context}: invalid map dimensions`); return; } if (!Array.isArray(map.terrain) || map.terrain.length !== map.height) { errors.push(`${context}: terrain row count does not match map height`); return; } map.terrain.forEach((row, y) => { if (!Array.isArray(row) || row.length !== map.width) { errors.push(`${context}: terrain row ${y} does not match map width`); return; } row.forEach((terrain, x) => { if (!terrainKeys.has(terrain)) { errors.push(`${context}: unknown terrain "${terrain}" at ${x},${y}`); } }); }); } function validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, context) { const unitIds = new Set(); const occupiedTiles = new Map(); const allies = []; const enemies = []; scenario.units.forEach((unit) => { const unitContext = `${context}/${unit.id}`; if (!unit.id) { errors.push(`${context}: unit without id`); return; } if (unitIds.has(unit.id)) { errors.push(`${context}: duplicate unit id "${unit.id}"`); } unitIds.add(unit.id); if (unit.faction === 'ally') { allies.push(unit); } else if (unit.faction === 'enemy') { enemies.push(unit); } else { errors.push(`${unitContext}: invalid faction "${unit.faction}"`); } if (!classKeys.has(unit.classKey)) { errors.push(`${unitContext}: unknown classKey "${unit.classKey}"`); } if (!Number.isInteger(unit.level) || unit.level < 1) { errors.push(`${unitContext}: invalid level ${unit.level}`); } if (!Number.isInteger(unit.hp) || !Number.isInteger(unit.maxHp) || unit.hp < 1 || unit.maxHp < 1 || unit.hp > unit.maxHp) { errors.push(`${unitContext}: invalid hp ${unit.hp}/${unit.maxHp}`); } if (!Number.isInteger(unit.move) || unit.move < 1) { errors.push(`${unitContext}: invalid move ${unit.move}`); } if (!isInBounds(unit, scenario.map)) { errors.push(`${unitContext}: position ${unit.x},${unit.y} is outside ${scenario.map.width}x${scenario.map.height}`); } else { const tileKey = `${unit.x},${unit.y}`; const existingUnitId = occupiedTiles.get(tileKey); if (existingUnitId) { errors.push(`${context}: units "${existingUnitId}" and "${unit.id}" share tile ${tileKey}`); } occupiedTiles.set(tileKey, unit.id); } equipmentSlots.forEach((slot) => { const equipment = unit.equipment?.[slot]; if (!equipment) { errors.push(`${unitContext}: missing ${slot} equipment`); return; } if (!itemKeys.has(equipment.itemId)) { errors.push(`${unitContext}: unknown ${slot} item "${equipment.itemId}"`); } if (!Number.isInteger(equipment.level) || equipment.level < 1 || equipment.level > 9) { errors.push(`${unitContext}: invalid ${slot} level ${equipment.level}`); } if (!Number.isInteger(equipment.exp) || equipment.exp < 0) { errors.push(`${unitContext}: invalid ${slot} exp ${equipment.exp}`); } }); }); if (!allies.length) { errors.push(`${context}: scenario has no allied units`); } if (!enemies.length) { errors.push(`${context}: scenario has no enemy units`); } } function validateObjectives(errors, scenario, context) { const unitIds = new Set(scenario.units.map((unit) => unit.id)); const objectiveIds = new Set(); if (!scenario.leaderUnitId || !unitIds.has(scenario.leaderUnitId)) { errors.push(`${context}: leaderUnitId "${scenario.leaderUnitId}" is missing from units`); } scenario.defeatConditions.forEach((condition) => { if (!unitIds.has(condition.unitId)) { errors.push(`${context}: defeat condition references missing unit "${condition.unitId}"`); } }); scenario.objectives.forEach((objective) => { if (!objective.id || objectiveIds.has(objective.id)) { errors.push(`${context}: duplicate or missing objective id "${objective.id}"`); } objectiveIds.add(objective.id); assertNonEmptyString(errors, objective.label, `${context}/${objective.id}: objective label`); if (!Number.isFinite(objective.rewardGold) || objective.rewardGold < 0) { errors.push(`${context}/${objective.id}: invalid rewardGold ${objective.rewardGold}`); } 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)) { errors.push(`${context}/${objective.id}: target tile ${objective.targetTile.x},${objective.targetTile.y} is outside map`); } }); } function validateSortie(errors, scenario, classKeys, context) { const sortie = scenario.sortie; if (!sortie) { return; } const unitIds = new Set(scenario.units.map((unit) => unit.id)); if (!Number.isInteger(sortie.sortieLimit) || sortie.sortieLimit < 1) { errors.push(`${context}: invalid sortie limit ${sortie.sortieLimit}`); } if ((sortie.requiredUnits?.length ?? 0) > sortie.sortieLimit) { errors.push(`${context}: requiredUnits count exceeds sortieLimit`); } [...(sortie.requiredUnits ?? []), ...(sortie.excludedUnits ?? [])].forEach((unitId) => { if (!unitIds.has(unitId)) { errors.push(`${context}: sortie references missing unit "${unitId}"`); } }); sortie.recommendedUnits.forEach((recommendation) => { if (!unitIds.has(recommendation.unitId)) { errors.push(`${context}: recommended unit "${recommendation.unitId}" is missing from units`); } 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`); recommendation.classKeys.forEach((classKey) => { if (!classKeys.has(classKey)) { errors.push(`${context}: recommended class "${recommendation.label}" references unknown classKey "${classKey}"`); } }); }); if (sortie.deploymentSlots.length < sortie.sortieLimit) { errors.push(`${context}: deploymentSlots count is lower than sortieLimit`); } const deploymentTiles = new Set(); sortie.deploymentSlots.forEach((slot) => { if (!isInBounds(slot, scenario.map)) { errors.push(`${context}: deployment slot ${slot.x},${slot.y} is outside map`); return; } const tileKey = `${slot.x},${slot.y}`; if (deploymentTiles.has(tileKey)) { errors.push(`${context}: duplicate deployment slot ${tileKey}`); } deploymentTiles.add(tileKey); if (slot.unitId && !unitIds.has(slot.unitId)) { errors.push(`${context}: deployment slot references missing unit "${slot.unitId}"`); } }); } function validateRewards(errors, scenario, scenarioIds, context) { scenario.itemRewards.forEach((reward, index) => { assertNonEmptyString(errors, reward, `${context}: item reward ${index}`); }); const unlockBattleId = scenario.campaignReward?.unlockBattleId; if (unlockBattleId && !scenarioIds.has(unlockBattleId)) { errors.push(`${context}: campaignReward unlockBattleId "${unlockBattleId}" is not a known battle`); } } function isInBounds(point, map) { return ( Number.isInteger(point.x) && Number.isInteger(point.y) && point.x >= 0 && point.y >= 0 && point.x < map.width && point.y < map.height ); } function assertNonEmptyString(errors, value, context) { if (typeof value !== 'string' || value.trim().length === 0) { errors.push(`${context} must be a non-empty string`); } }