diff --git a/scripts/verify-battle-scenario-data.mjs b/scripts/verify-battle-scenario-data.mjs index 8368643..79bcb44 100644 --- a/scripts/verify-battle-scenario-data.mjs +++ b/scripts/verify-battle-scenario-data.mjs @@ -38,7 +38,7 @@ try { validateMap(errors, scenario, terrainKeys, context); validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, context); validateObjectives(errors, scenario, context); - validateSortie(errors, scenario, classKeys, formationRoles, context); + validateSortie(errors, scenario, classKeys, formationRoles, terrainRules, context); validateRewards(errors, scenario, scenarioIds, itemNames, knownRewardUnitIds, context); }); @@ -192,13 +192,15 @@ function validateObjectives(errors, scenario, context) { }); } -function validateSortie(errors, scenario, classKeys, formationRoles, context) { +function validateSortie(errors, scenario, classKeys, formationRoles, terrainRules, context) { const sortie = scenario.sortie; if (!sortie) { return; } const unitIds = new Set(scenario.units.map((unit) => unit.id)); + const allyUnitIds = new Set(scenario.units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id)); + const enemyTiles = new Set(scenario.units.filter((unit) => unit.faction === 'enemy').map((unit) => `${unit.x},${unit.y}`)); if (!Number.isInteger(sortie.sortieLimit) || sortie.sortieLimit < 1) { errors.push(`${context}: invalid sortie limit ${sortie.sortieLimit}`); } @@ -230,6 +232,8 @@ function validateSortie(errors, scenario, classKeys, formationRoles, context) { [...requiredUnits, ...excludedUnits].forEach((unitId) => { if (!unitIds.has(unitId)) { errors.push(`${context}: sortie references missing unit "${unitId}"`); + } else if (!allyUnitIds.has(unitId)) { + errors.push(`${context}: sortie references non-allied unit "${unitId}"`); } }); const recommendedUnitIds = new Set(); @@ -240,6 +244,8 @@ function validateSortie(errors, scenario, classKeys, formationRoles, context) { recommendedUnitIds.add(recommendation.unitId); if (!unitIds.has(recommendation.unitId)) { errors.push(`${context}: recommended unit "${recommendation.unitId}" is missing from units`); + } else if (!allyUnitIds.has(recommendation.unitId)) { + errors.push(`${context}: recommended unit "${recommendation.unitId}" is not allied`); } if (excludedUnitIds.has(recommendation.unitId)) { errors.push(`${context}: excluded unit "${recommendation.unitId}" cannot also be recommended`); @@ -276,12 +282,24 @@ function validateSortie(errors, scenario, classKeys, formationRoles, context) { errors.push(`${context}: deployment slot ${slot.x},${slot.y} has invalid role "${slot.role}"`); } const tileKey = `${slot.x},${slot.y}`; + const terrain = scenario.map.terrain[slot.y][slot.x]; + if (terrainRules[terrain]?.passable === false) { + errors.push(`${context}: deployment slot ${tileKey} is on impassable terrain "${terrain}"`); + } + if (enemyTiles.has(tileKey)) { + errors.push(`${context}: deployment slot ${tileKey} overlaps an enemy unit`); + } 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}"`); + } else if (slot.unitId && !allyUnitIds.has(slot.unitId)) { + errors.push(`${context}: deployment slot references non-allied unit "${slot.unitId}"`); + } + if (slot.unitId && excludedUnitIds.has(slot.unitId)) { + errors.push(`${context}: excluded unit "${slot.unitId}" cannot have a deployment slot`); } if (slot.unitId) { if (deploymentUnitIds.has(slot.unitId)) { @@ -290,6 +308,11 @@ function validateSortie(errors, scenario, classKeys, formationRoles, context) { deploymentUnitIds.add(slot.unitId); } }); + requiredUnits.forEach((unitId) => { + if (!deploymentUnitIds.has(unitId)) { + errors.push(`${context}: required unit "${unitId}" must have an explicit deployment slot`); + } + }); } function validateRewards(errors, scenario, scenarioIds, itemNames, knownRewardUnitIds, context) { diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts index f3edb75..8d3d9d5 100644 --- a/src/game/data/battles.ts +++ b/src/game/data/battles.ts @@ -5841,7 +5841,7 @@ export const fortiethBattleScenario: BattleScenarioDefinition = { { x: 13, y: 56, role: 'support', unitId: 'ma-liang', label: '제방 민심' }, { x: 21, y: 57, role: 'support', unitId: 'huang-quan', label: '퇴로 판정' }, { x: 14, y: 54, role: 'flank', unitId: 'zhao-yun', label: '나루 보강' }, - { x: 22, y: 51, role: 'flank', unitId: 'ma-chao', label: '기병 봉쇄' } + { x: 22, y: 52, role: 'flank', unitId: 'ma-chao', label: '기병 봉쇄' } ], note: '한수 수공은 둑길과 수문을 잡아 우금 본대를 강과 진흙 사이에 묶는 전투입니다. 법정·마량·황권으로 물길을 읽고, 조운·마초로 방덕 기병의 돌파를 막으며 관우의 전열을 유지하십시오.' @@ -5979,7 +5979,7 @@ export const fortyFirstBattleScenario: BattleScenarioDefinition = { deploymentSlots: [ { x: 5, y: 58, role: 'reserve', unitId: 'liu-bei', label: '포위 명분' }, { x: 12, y: 53, role: 'front', unitId: 'guan-yu', label: '성문 압박' }, - { x: 18, y: 54, role: 'support', unitId: 'huang-zhong', label: '성벽 견제' }, + { x: 17, y: 54, role: 'support', unitId: 'huang-zhong', label: '성벽 견제' }, { x: 14, y: 58, role: 'support', unitId: 'ma-liang', label: '형주 후방' }, { x: 19, y: 58, role: 'support', unitId: 'fa-zheng', label: '수성전 판단' }, { x: 15, y: 56, role: 'flank', unitId: 'zhao-yun', label: '나루 보강' }, @@ -6123,7 +6123,7 @@ export const fortySecondBattleScenario: BattleScenarioDefinition = { { x: 13, y: 54, role: 'front', unitId: 'guan-yu', label: '나루 전열' }, { x: 15, y: 59, role: 'support', unitId: 'ma-liang', label: '민심 판정' }, { x: 9, y: 60, role: 'support', unitId: 'mi-zhu', label: '창고 장부' }, - { x: 16, y: 57, role: 'flank', unitId: 'zhao-yun', label: '나루 보강' }, + { x: 16, y: 58, role: 'flank', unitId: 'zhao-yun', label: '나루 보강' }, { x: 23, y: 60, role: 'support', unitId: 'huang-quan', label: '장부 판정' }, { x: 23, y: 52, role: 'flank', unitId: 'wang-ping', label: '숲길 경계' } ], @@ -6262,12 +6262,12 @@ export const fortyThirdBattleScenario: BattleScenarioDefinition = { ], deploymentSlots: [ { x: 8, y: 61, role: 'reserve', unitId: 'liu-bei', label: '형주 명분' }, - { x: 15, y: 56, role: 'front', unitId: 'guan-yu', label: '성문 전열' }, + { x: 14, y: 56, role: 'front', unitId: 'guan-yu', label: '성문 전열' }, { x: 17, y: 61, role: 'support', unitId: 'ma-liang', label: '민심 판정' }, { x: 11, y: 61, role: 'support', unitId: 'mi-zhu', label: '창고 장부' }, { x: 26, y: 62, role: 'support', unitId: 'huang-quan', label: '군령 판정' }, { x: 25, y: 52, role: 'flank', unitId: 'wang-ping', label: '숲길 경계' }, - { x: 18, y: 58, role: 'flank', unitId: 'zhao-yun', label: '나루 차단' } + { x: 18, y: 60, role: 'flank', unitId: 'zhao-yun', label: '나루 차단' } ], note: '공안 성문 변고는 여몽 본대를 막는 동시에 성문과 장부를 다시 붙드는 전투입니다. 마량·황권으로 군령의 진위를 읽고, 미축으로 창고 기록을 확인하며, 조운·왕평으로 강변 복병을 끊으십시오.' @@ -9062,7 +9062,7 @@ export const sixtyFirstBattleScenario: BattleScenarioDefinition = { { x: 34, y: 85, role: 'front', unitId: 'jiang-wei', label: '빗길 길눈' }, { x: 45, y: 90, role: 'front', unitId: 'wei-yan', label: '반격 선봉' }, { x: 31, y: 88, role: 'support', unitId: 'li-yan', label: '수레 장부' }, - { x: 12, y: 87, role: 'support', unitId: 'huang-quan', label: '군량 장부' }, + { x: 13, y: 87, role: 'support', unitId: 'huang-quan', label: '군량 장부' }, { x: 42, y: 86, role: 'front', unitId: 'wang-ping', label: '산길 표식' }, { x: 30, y: 92, role: 'front', unitId: 'zhao-yun', label: '기동 예비' }, { x: 24, y: 85, role: 'support', unitId: 'ma-liang', label: '마을 안심' } @@ -9236,7 +9236,7 @@ export const sixtySecondBattleScenario: BattleScenarioDefinition = { { x: 45, y: 91, role: 'front', unitId: 'wei-yan', label: '돌파 선봉' }, { x: 30, y: 93, role: 'front', unitId: 'zhao-yun', label: '회수선' }, { x: 42, y: 86, role: 'front', unitId: 'wang-ping', label: '산길 표식' }, - { x: 12, y: 88, role: 'support', unitId: 'huang-quan', label: '보급 장부' }, + { x: 13, y: 88, role: 'support', unitId: 'huang-quan', label: '보급 장부' }, { x: 24, y: 86, role: 'support', unitId: 'ma-liang', label: '마을 안심' }, { x: 59, y: 95, role: 'flank', unitId: 'ma-dai', label: '측면 견제' } ], @@ -9393,7 +9393,7 @@ export const sixtyThirdBattleScenario: BattleScenarioDefinition = { { x: 45, y: 93, role: 'front', unitId: 'wei-yan', label: '추격 선봉' }, { x: 31, y: 94, role: 'front', unitId: 'zhao-yun', label: '회수 예비' }, { x: 42, y: 88, role: 'front', unitId: 'wang-ping', label: '표식 유지' }, - { x: 12, y: 90, role: 'support', unitId: 'huang-quan', label: '수레 장부' }, + { x: 14, y: 90, role: 'support', unitId: 'huang-quan', label: '수레 장부' }, { x: 25, y: 88, role: 'support', unitId: 'ma-liang', label: '마을 고시' }, { x: 60, y: 97, role: 'flank', unitId: 'ma-dai', label: '봉화 차단' } ],