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 { campaignRecruitUnits } = await server.ssrLoadModule('/src/game/data/scenario.ts'); const { unitClasses } = await server.ssrLoadModule('/src/game/data/battleRules.ts'); const { itemCatalog, equipmentSlots } = await server.ssrLoadModule('/src/game/data/battleItems.ts'); const errors = []; const recruitTemplates = Array.isArray(campaignRecruitUnits) ? campaignRecruitUnits : []; const recruitTemplateIds = new Set(recruitTemplates.map((unit) => unit.id)); const classKeys = new Set(Object.keys(unitClasses)); const itemKeys = new Set(Object.keys(itemCatalog)); const recruitRewardRefs = []; validateRecruitTemplates(errors, recruitTemplates, classKeys, itemKeys, itemCatalog, equipmentSlots); const scenarioEntries = Object.entries(battleScenarios); scenarioEntries.forEach(([battleId, scenario]) => { const sameBattleAllies = new Set(scenario.units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id)); const rewardRecruits = scenario.campaignReward?.recruits ?? []; rewardRecruits.forEach((unitId, index) => { recruitRewardRefs.push({ battleId, unitId }); if (typeof unitId !== 'string' || unitId.trim().length === 0) { errors.push(`${battleId}: campaignReward.recruits[${index}] must be a non-empty string`); return; } if (!sameBattleAllies.has(unitId) && !recruitTemplateIds.has(unitId)) { errors.push( `${battleId}: recruit "${unitId}" is not an ally in this battle and has no campaignRecruitUnits template` ); } }); }); validateReachability(errors, recruitTemplates, recruitRewardRefs); validateSortieRecruitAvailability(errors, scenarioEntries); if (errors.length) { console.error(`Campaign recruit data verification failed with ${errors.length} issue(s):`); errors.slice(0, 100).forEach((error) => console.error(`- ${error}`)); if (errors.length > 100) { console.error(`- ...and ${errors.length - 100} more`); } process.exitCode = 1; } else { console.log( `Verified ${recruitTemplates.length} campaign recruit unit templates and ${recruitRewardRefs.length} recruit rewards.` ); } } finally { await server.close(); } function validateRecruitTemplates(errors, recruitTemplates, classKeys, itemKeys, itemCatalog, equipmentSlots) { const ids = new Set(); recruitTemplates.forEach((unit, index) => { const context = unit?.id ? `campaignRecruitUnits/${unit.id}` : `campaignRecruitUnits[${index}]`; if (!unit || typeof unit !== 'object') { errors.push(`${context}: must be a unit object`); return; } assertNonEmptyString(errors, unit.id, `${context}: id`); assertNonEmptyString(errors, unit.name, `${context}: name`); assertNonEmptyString(errors, unit.className, `${context}: className`); if (ids.has(unit.id)) { errors.push(`${context}: duplicate recruit id "${unit.id}"`); } ids.add(unit.id); if (unit.faction !== 'ally') { errors.push(`${context}: campaign recruit faction must be ally, got "${unit.faction}"`); } if (!classKeys.has(unit.classKey)) { errors.push(`${context}: unknown classKey "${unit.classKey}"`); } validateCharacterProgress(errors, unit, context); if (!Number.isInteger(unit.hp) || !Number.isInteger(unit.maxHp) || unit.hp < 1 || unit.maxHp < 1 || unit.hp > unit.maxHp) { errors.push(`${context}: invalid hp ${unit.hp}/${unit.maxHp}`); } if (!Number.isInteger(unit.attack) || unit.attack < 0) { errors.push(`${context}: invalid attack ${unit.attack}`); } if (!Number.isInteger(unit.move) || unit.move < 1) { errors.push(`${context}: invalid move ${unit.move}`); } if (!Number.isInteger(unit.x) || !Number.isInteger(unit.y)) { errors.push(`${context}: invalid fallback position ${unit.x},${unit.y}`); } validateStats(errors, unit.stats, context); validateEquipment(errors, unit.equipment, itemKeys, itemCatalog, equipmentSlots, context); }); } function validateStats(errors, stats, context) { const requiredStats = ['might', 'intelligence', 'leadership', 'agility', 'luck']; if (!stats || typeof stats !== 'object') { errors.push(`${context}: missing stats`); return; } requiredStats.forEach((statKey) => { const value = stats[statKey]; if (!Number.isInteger(value) || value < 1 || value > 120) { errors.push(`${context}: invalid stats.${statKey} ${value}`); } }); } function validateCharacterProgress(errors, unit, context) { if (!Number.isInteger(unit.level) || unit.level < 1) { errors.push(`${context}: invalid level ${unit.level}`); } if (!Number.isInteger(unit.exp) || unit.exp < 0) { errors.push(`${context}: invalid exp ${unit.exp}`); } else if (Number.isInteger(unit.level) && unit.level < 99 && unit.exp >= 100) { errors.push(`${context}: exp ${unit.exp} should be below 100 before max level`); } else if (Number.isInteger(unit.level) && unit.level >= 99 && unit.exp > 100) { errors.push(`${context}: max level exp ${unit.exp} should be at most 100`); } } function validateEquipment(errors, equipment, itemKeys, itemCatalog, equipmentSlots, context) { if (!equipment || typeof equipment !== 'object') { errors.push(`${context}: missing equipment`); return; } equipmentSlots.forEach((slot) => { const state = 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 validateReachability(errors, recruitTemplates, recruitRewardRefs) { const rewardedRecruitIds = new Set(recruitRewardRefs.map(({ unitId }) => unitId)); recruitTemplates.forEach((unit) => { if (!rewardedRecruitIds.has(unit.id)) { errors.push(`campaignRecruitUnits/${unit.id}: recruit template is never awarded by campaignReward.recruits`); } }); } function validateSortieRecruitAvailability(errors, scenarioEntries) { const firstBattleAllies = scenarioEntries[0]?.[1]?.units ?.filter((unit) => unit.faction === 'ally') .map((unit) => unit.id) ?? []; const availableUnitIds = new Set(firstBattleAllies); scenarioEntries.forEach(([battleId, scenario]) => { const unavailableRefs = new Set(); const sortie = scenario.sortie; (sortie?.requiredUnits ?? []).forEach((unitId) => { if (!availableUnitIds.has(unitId)) { unavailableRefs.add(`requiredUnits:${unitId}`); } }); (sortie?.recommendedUnits ?? []).forEach((recommendation) => { if (!availableUnitIds.has(recommendation.unitId)) { unavailableRefs.add(`recommendedUnits:${recommendation.unitId}`); } }); (sortie?.deploymentSlots ?? []).forEach((slot) => { if (slot.unitId && !availableUnitIds.has(slot.unitId)) { unavailableRefs.add(`deploymentSlots:${slot.unitId}`); } }); if (unavailableRefs.size > 0) { errors.push(`${battleId}: sortie references unit(s) before recruit availability: ${Array.from(unavailableRefs).join(', ')}`); } (scenario.campaignReward?.recruits ?? []).forEach((unitId) => availableUnitIds.add(unitId)); }); } function assertNonEmptyString(errors, value, context) { if (typeof value !== 'string' || value.trim().length === 0) { errors.push(`${context} must be a non-empty string`); } }