diff --git a/scripts/qa-representative-battles.mjs b/scripts/qa-representative-battles.mjs
index 1641d64..80c4c31 100644
--- a/scripts/qa-representative-battles.mjs
+++ b/scripts/qa-representative-battles.mjs
@@ -6,6 +6,76 @@ const headless = process.env.QA_HEADLESS !== '0';
const maxRounds = Number(process.env.QA_MAX_ROUNDS ?? 80);
const pnpmCommand = process.env.PNPM_CMD ?? 'pnpm';
+const coreBrothers = ['liu-bei', 'guan-yu', 'zhang-fei'];
+const xuzhouFullSortie = ['liu-bei', 'guan-yu', 'zhang-fei', 'jian-yong', 'mi-zhu'];
+
+const earlyCampaignBattles = [
+ { no: 1, id: 'first-battle-zhuo-commandery', selected: [], targets: ['rebel-leader'], protected: ['liu-bei'] },
+ {
+ no: 2,
+ id: 'second-battle-yellow-turban-pursuit',
+ selected: coreBrothers,
+ targets: ['pursuit-leader-han-seok'],
+ protected: ['liu-bei']
+ },
+ {
+ no: 3,
+ id: 'third-battle-guangzong-road',
+ selected: coreBrothers,
+ targets: ['guangzong-leader-ma-yuan'],
+ protected: ['liu-bei']
+ },
+ {
+ no: 4,
+ id: 'fourth-battle-guangzong-camp',
+ selected: coreBrothers,
+ targets: ['guangzong-main-leader-zhang-jue'],
+ protected: ['liu-bei']
+ },
+ {
+ no: 5,
+ id: 'fifth-battle-sishui-vanguard',
+ selected: coreBrothers,
+ targets: ['sishui-leader-hu-zhen'],
+ protected: ['liu-bei']
+ },
+ {
+ no: 6,
+ id: 'sixth-battle-jieqiao-relief',
+ selected: coreBrothers,
+ targets: ['jieqiao-leader-qu-yi'],
+ protected: ['liu-bei']
+ },
+ {
+ no: 7,
+ id: 'seventh-battle-xuzhou-rescue',
+ selected: coreBrothers,
+ targets: ['xuzhou-leader-xiahou-dun'],
+ protected: ['liu-bei']
+ },
+ {
+ no: 8,
+ id: 'eighth-battle-xiaopei-supply-road',
+ selected: xuzhouFullSortie,
+ targets: ['xiaopei-leader-gao-shun'],
+ protected: ['liu-bei']
+ },
+ {
+ no: 9,
+ id: 'ninth-battle-xuzhou-gate-night-raid',
+ selected: xuzhouFullSortie,
+ targets: ['xuzhou-leader-cao-bao'],
+ protected: ['liu-bei']
+ },
+ {
+ no: 10,
+ id: 'tenth-battle-xuzhou-breakout',
+ selected: xuzhouFullSortie,
+ targets: ['xuzhou-escape-leader-song-xian'],
+ protected: ['liu-bei']
+ }
+];
+
const allRepresentativeBattles = [
{ no: 1, id: 'first-battle-zhuo-commandery', selected: [], targets: ['rebel-leader'], protected: ['liu-bei'] },
{
@@ -66,6 +136,16 @@ const allRepresentativeBattles = [
}
];
+const qaBattleSets = {
+ representative: allRepresentativeBattles,
+ early: earlyCampaignBattles
+};
+const qaSetName = process.env.QA_SET ?? 'representative';
+const qaBattleSet = qaBattleSets[qaSetName];
+if (!qaBattleSet) {
+ throw new Error(`Unknown QA_SET "${qaSetName}". Use one of: ${Object.keys(qaBattleSets).join(', ')}`);
+}
+
const requestedBattles = new Set(
(process.env.QA_BATTLES ?? '')
.split(',')
@@ -74,8 +154,8 @@ const requestedBattles = new Set(
);
const representativeBattles =
requestedBattles.size > 0
- ? allRepresentativeBattles.filter((battle) => requestedBattles.has(String(battle.no)) || requestedBattles.has(battle.id))
- : allRepresentativeBattles;
+ ? qaBattleSet.filter((battle) => requestedBattles.has(String(battle.no)) || requestedBattles.has(battle.id))
+ : qaBattleSet;
let serverProcess;
@@ -242,11 +322,28 @@ async function playBattleWithoutDebugVictory(page, battle) {
}
function focusTarget(unit) {
+ const enemies = liveUnits('enemy');
+ const canProgressTo = (enemy) => {
+ if (scene.canAttack(unit, enemy)) {
+ return true;
+ }
+ const approachTile = scene.chooseApproachTile?.(unit, enemy);
+ return Boolean(approachTile && (approachTile.x !== unit.x || approachTile.y !== unit.y));
+ };
+ const progressEnemies = enemies.filter(canProgressTo);
+ const nearestEnemy = progressEnemies.sort((a, b) => distance(unit, a) - distance(unit, b) || priorityForTarget(b) - priorityForTarget(a))[0];
const aliveTargets = targets.map((targetId) => unitRef(targetId)).filter((target) => target && target.hp > 0);
if (aliveTargets.length > 0) {
- return aliveTargets.sort((a, b) => distance(unit, a) - distance(unit, b))[0];
+ const reachableTargets = aliveTargets.filter(canProgressTo);
+ const nearestTarget = (reachableTargets.length > 0 ? reachableTargets : aliveTargets).sort((a, b) => distance(unit, a) - distance(unit, b))[0];
+ if (nearestEnemy && nearestEnemy.id !== nearestTarget.id && distance(unit, nearestEnemy) + 5 < distance(unit, nearestTarget)) {
+ return nearestEnemy;
+ }
+ return nearestTarget;
}
- return liveUnits('enemy').sort((a, b) => priorityForTarget(b) - priorityForTarget(a) || distance(unit, a) - distance(unit, b))[0];
+ return (progressEnemies.length > 0 ? progressEnemies : enemies).sort(
+ (a, b) => priorityForTarget(b) - priorityForTarget(a) || distance(unit, a) - distance(unit, b)
+ )[0];
}
function attackChoice(unit) {
@@ -300,13 +397,12 @@ async function playBattleWithoutDebugVictory(page, battle) {
}
const focus = focusTarget(unit);
const enemies = liveUnits('enemy');
- const vanguardInPosition = liveUnits('ally').some(
- (ally) => ally.id !== unit.id && !protectedIds.has(ally.id) && (!focus || distance(ally, focus) <= 10)
- );
- return (
- !vanguardInPosition ||
- (focus && targetIds.has(focus.id) && focus.hp <= Math.ceil(focus.maxHp * 0.3))
- );
+ const vanguards = liveUnits('ally').filter((ally) => ally.id !== unit.id && !protectedIds.has(ally.id));
+ const vanguardInPosition = vanguards.some((ally) => !focus || distance(ally, focus) <= 10);
+ if (vanguards.length === 0) {
+ return unit.hp >= Math.ceil(unit.maxHp * 0.8);
+ }
+ return Boolean(vanguardInPosition && focus && targetIds.has(focus.id) && focus.hp <= Math.ceil(focus.maxHp * 0.3));
}
function canTakeAttack(unit, attack, threat = protectedIds.has(unit.id) ? incomingThreat(unit, unit) : 0) {
@@ -367,6 +463,14 @@ async function playBattleWithoutDebugVictory(page, battle) {
const focus = focusTarget(unit);
if (focus) {
const currentDistance = distance(unit, focus);
+ const approachTile = scene.chooseApproachTile?.(unit, focus);
+ if (
+ approachTile &&
+ (approachTile.x !== unit.x || approachTile.y !== unit.y) &&
+ (!pressProtected || canPressProtectedTile(unit, approachTile, focus, currentDistance))
+ ) {
+ return approachTile;
+ }
const pressureTile = tiles
.filter((tile) => distance(tile, focus) < currentDistance)
.filter((tile) => !pressProtected || canPressProtectedTile(unit, tile, focus, currentDistance))
diff --git a/src/assets/images/battle/second-battle-map.svg b/src/assets/images/battle/second-battle-map.svg
index 5e9399b..5b551c9 100644
--- a/src/assets/images/battle/second-battle-map.svg
+++ b/src/assets/images/battle/second-battle-map.svg
@@ -46,6 +46,9 @@
+
+
+
diff --git a/src/game/data/battles.ts b/src/game/data/battles.ts
index f5bb87f..31dd40f 100644
--- a/src/game/data/battles.ts
+++ b/src/game/data/battles.ts
@@ -382,7 +382,7 @@ export const firstBattleScenario: BattleScenarioDefinition = {
openingObjectiveLines: [
'두령 한석을 격파하면 황건적의 전열이 무너집니다.',
'유비가 퇴각하면 의용군은 전투를 지속할 수 없습니다.',
- '마을을 확보하고 8턴 안에 승리하면 추가 보상이 붙습니다.'
+ '마을 지형을 밟아 보급을 확보하고, 다친 장수는 콩과 탁주로 회복하십시오.'
],
map: firstBattleMap,
units: firstBattleUnits,
@@ -434,15 +434,15 @@ export const secondBattleScenario: BattleScenarioDefinition = {
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'황건 잔당이 북쪽 마을로 달아났습니다. 두령 한석을 격파하면 적의 퇴로가 무너집니다.',
- '마을 주변에 적 위협이 남아 있으면 보상이 줄어듭니다. 길목의 습격조를 먼저 정리하십시오.',
- '12턴 이내에 승리하면 의용군의 추격전 명성이 오릅니다.'
+ '중앙 나루를 건너기 전에 길목의 습격조를 먼저 정리하면 북쪽 마을을 안전하게 확보할 수 있습니다.',
+ '세 형제를 모두 출전시키고 14턴 이내에 승리하면 추격전 명성과 추가 보상이 붙습니다.'
],
map: secondBattleMap,
units: secondBattleUnits,
bonds: secondBattleBonds,
mapTextureKey: 'battle-map-second',
leaderUnitId: 'pursuit-leader-han-seok',
- quickVictoryTurnLimit: 12,
+ quickVictoryTurnLimit: 14,
baseVictoryGold: 420,
objectives: [
{
@@ -469,13 +469,13 @@ export const secondBattleScenario: BattleScenarioDefinition = {
{
id: 'quick',
kind: 'quick-victory',
- label: '12턴 이내 승리',
+ label: '14턴 이내 승리',
rewardGold: 150,
- maxTurn: 12
+ maxTurn: 14
}
],
defeatConditions: [{ kind: 'unit-defeated', unitId: 'liu-bei' }],
- itemRewards: ['콩 2', '탁주 1', '의용군 명성 +2'],
+ itemRewards: ['콩 2', '상처약 1', '탁주 1', '의용군 명성 +2'],
victoryPages: secondBattleVictoryPages,
nextCampScene: 'CampScene'
};
@@ -646,7 +646,7 @@ export const sixthBattleScenario: BattleScenarioDefinition = {
defeatConditionLabel: '유비 퇴각',
openingObjectiveLines: [
'공손찬의 보급로를 원소군 교위 곡의가 끊으려 합니다. 곡의를 격파하면 계교로 향하는 길을 다시 열 수 있습니다.',
- '강가의 궁병과 측면 기병이 동시에 압박합니다. 숲길 척후를 먼저 밀어내고 중앙 길을 확보하십시오.',
+ '강가의 궁병과 측면 기병이 동시에 압박합니다. 유비는 뒤에 두고 관우와 장비로 중앙 길을 확보하십시오.',
'15턴 이내에 승리하면 공손찬 진영에서 세 형제의 신뢰가 커집니다.'
],
map: sixthBattleMap,
diff --git a/src/game/data/campaignFlow.ts b/src/game/data/campaignFlow.ts
index 3d1a903..9d9c9f3 100644
--- a/src/game/data/campaignFlow.ts
+++ b/src/game/data/campaignFlow.ts
@@ -223,7 +223,7 @@ const sortieFlows: Record = {
afterBattleId: defaultBattleScenario.id,
eyebrow: '다음 전장',
title: secondBattleScenario.title,
- description: '탁현을 물러난 황건 잔당이 인근 마을을 위협하고 있습니다. 의용군은 첫 승리의 기세를 몰아 추격에 나섭니다.',
+ description: '탁현을 물러난 황건 잔당이 인근 마을을 위협하고 있습니다. 의용군은 중앙 나루를 확보하며 첫 승리의 기세를 몰아 추격에 나섭니다.',
rewardHint: '예상 보상: 군자금, 소모품, 의용군 명성',
nextBattleId: secondBattleScenario.id,
campaignStep: 'second-battle',
diff --git a/src/game/data/scenario.ts b/src/game/data/scenario.ts
index b8ce9c1..d8e63d3 100644
--- a/src/game/data/scenario.ts
+++ b/src/game/data/scenario.ts
@@ -4244,8 +4244,8 @@ export const secondBattleMap: BattleMap = {
['plain', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'hill', 'plain', 'plain', 'plain', 'river', 'river', 'road', 'plain', 'plain', 'hill', 'plain', 'village', 'plain', 'forest', 'forest', 'plain'],
['plain', 'plain', 'forest', 'plain', 'road', 'plain', 'plain', 'hill', 'hill', 'plain', 'plain', 'plain', 'river', 'river', 'road', 'road', 'plain', 'plain', 'plain', 'village', 'plain', 'plain', 'forest', 'plain'],
['plain', 'forest', 'forest', 'plain', 'road', 'plain', 'plain', 'plain', 'hill', 'plain', 'road', 'road', 'plain', 'river', 'river', 'road', 'plain', 'plain', 'plain', 'plain', 'plain', 'plain', 'hill', 'plain'],
- ['plain', 'forest', 'plain', 'road', 'road', 'plain', 'hill', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'hill', 'plain'],
- ['plain', 'plain', 'plain', 'road', 'plain', 'plain', 'hill', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain', 'hill', 'plain'],
+ ['plain', 'forest', 'plain', 'road', 'road', 'plain', 'hill', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'hill', 'hill', 'plain'],
+ ['plain', 'plain', 'plain', 'road', 'plain', 'plain', 'hill', 'plain', 'road', 'road', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain', 'hill', 'plain'],
['hill', 'plain', 'plain', 'road', 'plain', 'plain', 'plain', 'road', 'road', 'plain', 'plain', 'village', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain', 'plain'],
['hill', 'hill', 'plain', 'road', 'plain', 'forest', 'plain', 'road', 'plain', 'plain', 'village', 'village', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'forest', 'forest', 'plain', 'plain', 'plain', 'plain'],
['cliff', 'hill', 'plain', 'road', 'road', 'plain', 'plain', 'road', 'plain', 'plain', 'plain', 'village', 'plain', 'plain', 'plain', 'river', 'river', 'plain', 'plain', 'forest', 'plain', 'plain', 'plain', 'plain'],
@@ -6119,9 +6119,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'archer',
level: 6,
exp: 38,
- hp: 29,
- maxHp: 29,
- attack: 12,
+ hp: 27,
+ maxHp: 27,
+ attack: 10,
move: 3,
stats: { might: 66, intelligence: 54, leadership: 54, agility: 66, luck: 50 },
equipment: {
@@ -6140,9 +6140,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'cavalry',
level: 7,
exp: 40,
- hp: 39,
- maxHp: 39,
- attack: 13,
+ hp: 37,
+ maxHp: 37,
+ attack: 12,
move: 5,
stats: { might: 78, intelligence: 42, leadership: 60, agility: 78, luck: 52 },
equipment: {
@@ -6163,7 +6163,7 @@ export const sixthBattleUnits: UnitData[] = [
exp: 38,
hp: 33,
maxHp: 33,
- attack: 12,
+ attack: 10,
move: 4,
stats: { might: 74, intelligence: 44, leadership: 51, agility: 69, luck: 51 },
equipment: {
@@ -6182,9 +6182,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'archer',
level: 7,
exp: 40,
- hp: 30,
- maxHp: 30,
- attack: 13,
+ hp: 27,
+ maxHp: 27,
+ attack: 9,
move: 3,
stats: { might: 68, intelligence: 56, leadership: 56, agility: 67, luck: 51 },
equipment: {
@@ -6203,9 +6203,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'cavalry',
level: 7,
exp: 42,
- hp: 40,
- maxHp: 40,
- attack: 13,
+ hp: 38,
+ maxHp: 38,
+ attack: 12,
move: 5,
stats: { might: 79, intelligence: 42, leadership: 61, agility: 79, luck: 52 },
equipment: {
@@ -6224,9 +6224,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'yellowTurban',
level: 7,
exp: 42,
- hp: 38,
- maxHp: 38,
- attack: 12,
+ hp: 26,
+ maxHp: 26,
+ attack: 8,
move: 3,
stats: { might: 76, intelligence: 45, leadership: 62, agility: 58, luck: 51 },
equipment: {
@@ -6245,9 +6245,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'cavalry',
level: 7,
exp: 44,
- hp: 41,
- maxHp: 41,
- attack: 13,
+ hp: 34,
+ maxHp: 34,
+ attack: 10,
move: 5,
stats: { might: 80, intelligence: 43, leadership: 62, agility: 80, luck: 53 },
equipment: {
@@ -6266,9 +6266,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'yellowTurban',
level: 7,
exp: 44,
- hp: 39,
- maxHp: 39,
- attack: 12,
+ hp: 26,
+ maxHp: 26,
+ attack: 8,
move: 3,
stats: { might: 77, intelligence: 45, leadership: 63, agility: 58, luck: 52 },
equipment: {
@@ -6287,9 +6287,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'archer',
level: 7,
exp: 44,
- hp: 31,
- maxHp: 31,
- attack: 13,
+ hp: 20,
+ maxHp: 20,
+ attack: 7,
move: 3,
stats: { might: 69, intelligence: 57, leadership: 57, agility: 68, luck: 52 },
equipment: {
@@ -6308,9 +6308,9 @@ export const sixthBattleUnits: UnitData[] = [
classKey: 'rebelLeader',
level: 8,
exp: 48,
- hp: 54,
- maxHp: 54,
- attack: 15,
+ hp: 38,
+ maxHp: 38,
+ attack: 10,
move: 3,
stats: { might: 84, intelligence: 62, leadership: 78, agility: 62, luck: 56 },
equipment: {
diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts
index b935ebe..fcf6685 100644
--- a/src/game/scenes/BattleScene.ts
+++ b/src/game/scenes/BattleScene.ts
@@ -2853,6 +2853,7 @@ export class BattleScene extends Phaser.Scene {
private cameraTileY = 0;
private edgeScrollElapsed = 0;
private suppressNextLeftClick = false;
+ private approachPathCostCache = new Map>();
constructor() {
super('BattleScene');
@@ -2886,6 +2887,7 @@ export class BattleScene extends Phaser.Scene {
this.battleLog = [];
this.actedUnitIds.clear();
this.attackIntents = [];
+ this.approachPathCostCache.clear();
this.selectedUnit = undefined;
this.pendingMove = undefined;
this.targetingAction = undefined;
@@ -6714,17 +6716,107 @@ export class BattleScene extends Phaser.Scene {
private chooseApproachTile(enemy: UnitData, target: UnitData) {
const candidates = [{ x: enemy.x, y: enemy.y, cost: 0 }, ...this.reachableTiles(enemy)];
+ const pathCosts = this.cachedPathCostsToAttackRange(enemy, target);
return candidates
.filter((tile) => !this.isOccupied(tile.x, tile.y, enemy.id))
.sort((a, b) => {
- const aDistance = Math.abs(a.x - target.x) + Math.abs(a.y - target.y);
- const bDistance = Math.abs(b.x - target.x) + Math.abs(b.y - target.y);
+ const aPathCost = pathCosts.get(this.tileKey(a.x, a.y)) ?? Number.POSITIVE_INFINITY;
+ const bPathCost = pathCosts.get(this.tileKey(b.x, b.y)) ?? Number.POSITIVE_INFINITY;
+ const aHasPath = Number.isFinite(aPathCost);
+ const bHasPath = Number.isFinite(bPathCost);
+ if (aHasPath !== bHasPath) {
+ return aHasPath ? -1 : 1;
+ }
+ const aDistance = aHasPath ? aPathCost : Math.abs(a.x - target.x) + Math.abs(a.y - target.y);
+ const bDistance = bHasPath ? bPathCost : Math.abs(b.x - target.x) + Math.abs(b.y - target.y);
const aTerrain = getTerrainRule(battleMap.terrain[a.y][a.x]).defenseBonus;
const bTerrain = getTerrainRule(battleMap.terrain[b.y][b.x]).defenseBonus;
return aDistance - bDistance || bTerrain - aTerrain || a.cost - b.cost;
})[0];
}
+ private cachedPathCostsToAttackRange(unit: UnitData, target: UnitData) {
+ const key = this.approachPathCacheKey(unit, target);
+ const cached = this.approachPathCostCache.get(key);
+ if (cached) {
+ return cached;
+ }
+
+ if (this.approachPathCostCache.size > 180) {
+ this.approachPathCostCache.clear();
+ }
+ const costs = this.pathCostsToAttackRange(unit, target);
+ this.approachPathCostCache.set(key, costs);
+ return costs;
+ }
+
+ private approachPathCacheKey(unit: UnitData, target: UnitData) {
+ const blockers = battleUnits
+ .filter((candidate) => candidate.id !== unit.id && candidate.hp > 0)
+ .map((candidate) => `${candidate.id}:${candidate.x},${candidate.y}`)
+ .join('|');
+ return `${unit.id}:${unit.classKey}:${unit.faction}:${target.id}:${target.x},${target.y}:${blockers}`;
+ }
+
+ private pathCostsToAttackRange(unit: UnitData, target: UnitData) {
+ const range = this.attackRange(unit);
+ const directions = [
+ { x: 1, y: 0 },
+ { x: -1, y: 0 },
+ { x: 0, y: 1 },
+ { x: 0, y: -1 }
+ ];
+ const costs = new Map();
+ const queue: Array<{ x: number; y: number; cost: number }> = [];
+
+ this.tilesWithinDistance(target.x, target.y, range).forEach((tile) => {
+ if (this.isMovementBlocked(unit, tile.x, tile.y) || this.isOccupied(tile.x, tile.y, unit.id)) {
+ return;
+ }
+
+ const stepCost = this.movementCost(unit, battleMap.terrain[tile.y][tile.x]);
+ if (!Number.isFinite(stepCost)) {
+ return;
+ }
+
+ const key = this.tileKey(tile.x, tile.y);
+ costs.set(key, 0);
+ queue.push({ ...tile, cost: 0 });
+ });
+
+ while (queue.length > 0) {
+ queue.sort((a, b) => a.cost - b.cost);
+ const current = queue.shift();
+ if (!current) {
+ break;
+ }
+
+ for (const direction of directions) {
+ const nextX = current.x + direction.x;
+ const nextY = current.y + direction.y;
+ if (!this.isInBounds(nextX, nextY) || this.isMovementBlocked(unit, nextX, nextY)) {
+ continue;
+ }
+
+ const stepCost = this.movementCost(unit, battleMap.terrain[current.y][current.x]);
+ if (!Number.isFinite(stepCost)) {
+ continue;
+ }
+
+ const nextCost = current.cost + stepCost;
+ const key = this.tileKey(nextX, nextY);
+ if (nextCost >= (costs.get(key) ?? Number.POSITIVE_INFINITY)) {
+ continue;
+ }
+
+ costs.set(key, nextCost);
+ queue.push({ x: nextX, y: nextY, cost: nextCost });
+ }
+ }
+
+ return costs;
+ }
+
private nearestAliveUnit(unit: UnitData, faction: UnitData['faction']) {
return battleUnits
.filter((candidate) => candidate.faction === faction && candidate.hp > 0)
diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts
index 9d96fa8..3c7367d 100644
--- a/src/game/scenes/CampScene.ts
+++ b/src/game/scenes/CampScene.ts
@@ -559,7 +559,7 @@ const sortieRulesByBattleId: Partial