From 5d601be0a71f96bffeb4015871482c88eb07fa3a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 15:45:09 +0000 Subject: [PATCH] fix: battle simulator combat - units not fighting and air units not moving Three bugs caused the battle simulator to break: 1. SpatialGrid returns entity INDICES but callers used World.getEntity() which validates generational IDs. After entity recycling (Clear All), generation > 0 means index != full ID, causing all spatial queries to silently return no results. Fixed all 28 call sites across 9 files to use getEntityByIndex() which correctly handles index-based lookups. 2. BattleSimulatorPanel used ATTACK command type which only routes to CombatSystem (sets attackmove state but never requests pathfinding). Changed to ATTACK_MOVE which routes to MovementOrchestrator, handling both pathfinding and combat targeting. 3. AITacticsManager.executeAttackingPhase returned early when no enemy buildings existed (battle simulator has no buildings). The unit-hunt code that targets enemy unit clusters was unreachable. Now checks for enemy units before the early return. https://claude.ai/code/session_01BfPtHo2wTxYxsQX6beAFkZ --- src/components/game/BattleSimulatorPanel.tsx | 14 ++- src/engine/ai/UnitBehaviors.ts | 14 ++- src/engine/combat/TargetAcquisition.ts | 20 ++-- src/engine/systems/AIMicroSystem.ts | 6 +- src/engine/systems/CombatSystem.ts | 9 +- src/engine/systems/ProjectileSystem.ts | 6 +- src/engine/systems/UnitMechanicsSystem.ts | 91 ++++++++++++++----- src/engine/systems/VisionSystem.ts | 3 +- src/engine/systems/WallSystem.ts | 6 +- src/engine/systems/ai/AITacticsManager.ts | 19 +++- .../systems/movement/PathfindingMovement.ts | 7 +- tests/engine/ai/unitBehaviors.test.ts | 5 +- 12 files changed, 143 insertions(+), 57 deletions(-) diff --git a/src/components/game/BattleSimulatorPanel.tsx b/src/components/game/BattleSimulatorPanel.tsx index b703f82e..02848efd 100644 --- a/src/components/game/BattleSimulatorPanel.tsx +++ b/src/components/game/BattleSimulatorPanel.tsx @@ -104,8 +104,12 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { // Collect units by team and compute team center positions const player1Units: number[] = []; const player2Units: number[] = []; - let p1SumX = 0, p1SumY = 0, p1Count = 0; - let p2SumX = 0, p2SumY = 0, p2Count = 0; + let p1SumX = 0, + p1SumY = 0, + p1Count = 0; + let p2SumX = 0, + p2SumY = 0, + p2Count = 0; const entities = worldAdapter.getEntitiesWith('Unit', 'Selectable', 'Transform', 'Health'); @@ -136,12 +140,12 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { const player2Center = p2Count > 0 ? { x: p2SumX / p2Count, y: p2SumY / p2Count } : null; // Issue attack-move commands to each team toward the enemy center - // Attack-move triggers assault mode: units continuously seek and engage enemies + // ATTACK_MOVE routes to MovementOrchestrator which handles both pathfinding and combat if (player1Units.length > 0 && player2Center) { const command: GameCommand = { tick: currentTick, playerId: 'player1', - type: 'ATTACK', + type: 'ATTACK_MOVE', entityIds: player1Units, targetPosition: player2Center, }; @@ -152,7 +156,7 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { const command: GameCommand = { tick: currentTick, playerId: 'player2', - type: 'ATTACK', + type: 'ATTACK_MOVE', entityIds: player2Units, targetPosition: player1Center, }; diff --git a/src/engine/ai/UnitBehaviors.ts b/src/engine/ai/UnitBehaviors.ts index cd477918..cf4fa6de 100644 --- a/src/engine/ai/UnitBehaviors.ts +++ b/src/engine/ai/UnitBehaviors.ts @@ -82,7 +82,8 @@ function getNearestEnemy( for (const id of nearbyIds) { if (id === ctx.entityId) continue; - const enemy = ctx.world.getEntity(id); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const enemy = ctx.world.getEntityByIndex(id); if (!enemy) continue; const enemySelectable = enemy.get('Selectable'); @@ -99,7 +100,7 @@ function getNearestEnemy( if (dist < nearestDist) { nearestDist = dist; nearest = { - entityId: id, + entityId: enemy.id, distance: dist, transform: enemyTransform, unit: enemyUnit, @@ -127,7 +128,8 @@ function calculateThreatScore(ctx: BehaviorContext): number { for (const id of nearbyIds) { if (id === ctx.entityId) continue; - const enemy = ctx.world.getEntity(id); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const enemy = ctx.world.getEntityByIndex(id); if (!enemy) continue; const enemySelectable = enemy.get('Selectable'); @@ -263,7 +265,8 @@ export function hasMeleeThreatClose(ctx: BehaviorContext): boolean { for (const id of nearbyIds) { if (id === ctx.entityId) continue; - const enemy = ctx.world.getEntity(id); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const enemy = ctx.world.getEntityByIndex(id); if (!enemy) continue; const enemySelectable = enemy.get('Selectable'); @@ -383,7 +386,8 @@ export const kiteFromMelee = action('KiteFromMelee', (ctx) => { for (const id of nearbyIds) { if (id === ctx.entityId) continue; - const enemy = ctx.world.getEntity(id); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const enemy = ctx.world.getEntityByIndex(id); if (!enemy) continue; const enemySelectable = enemy.get('Selectable'); diff --git a/src/engine/combat/TargetAcquisition.ts b/src/engine/combat/TargetAcquisition.ts index 8e70cfb3..3eae96b9 100644 --- a/src/engine/combat/TargetAcquisition.ts +++ b/src/engine/combat/TargetAcquisition.ts @@ -145,7 +145,8 @@ export function findBestTarget(world: World, options: TargetQueryOptions): Targe continue; } - const entity = world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = world.getEntityByIndex(entityId); if (!entity) continue; const transform = entity.get('Transform'); @@ -190,7 +191,7 @@ export function findBestTarget(world: World, options: TargetQueryOptions): Targe ); if (!bestTarget || score > bestTarget.score) { - bestTarget = { entityId, score, distance, isBuilding: false }; + bestTarget = { entityId: entity.id, score, distance, isBuilding: false }; } } @@ -203,7 +204,8 @@ export function findBestTarget(world: World, options: TargetQueryOptions): Targe continue; } - const entity = world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = world.getEntityByIndex(entityId); if (!entity) continue; const transform = entity.get('Transform'); @@ -241,7 +243,7 @@ export function findBestTarget(world: World, options: TargetQueryOptions): Targe ); if (!bestTarget || score > bestTarget.score) { - bestTarget = { entityId, score, distance, isBuilding: true }; + bestTarget = { entityId: entity.id, score, distance, isBuilding: true }; } } } @@ -280,7 +282,8 @@ export function findAllTargets( continue; } - const entity = world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = world.getEntityByIndex(entityId); if (!entity) continue; const transform = entity.get('Transform'); @@ -322,7 +325,7 @@ export function findAllTargets( config ); - targets.push({ entityId, score, distance, isBuilding: false }); + targets.push({ entityId: entity.id, score, distance, isBuilding: false }); } // Query buildings @@ -334,7 +337,8 @@ export function findAllTargets( continue; } - const entity = world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = world.getEntityByIndex(entityId); if (!entity) continue; const transform = entity.get('Transform'); @@ -376,7 +380,7 @@ export function findAllTargets( config ); - targets.push({ entityId, score, distance, isBuilding: true }); + targets.push({ entityId: entity.id, score, distance, isBuilding: true }); } } diff --git a/src/engine/systems/AIMicroSystem.ts b/src/engine/systems/AIMicroSystem.ts index de3168d4..c16e2dbb 100644 --- a/src/engine/systems/AIMicroSystem.ts +++ b/src/engine/systems/AIMicroSystem.ts @@ -668,7 +668,8 @@ export class AIMicroSystem extends System { for (const nearbyId of nearbyUnits) { if (nearbyId === entityId) continue; - const nearbyEntity = this.world.getEntity(nearbyId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const nearbyEntity = this.world.getEntityByIndex(nearbyId); if (!nearbyEntity) continue; const nearbyUnit = nearbyEntity.get('Unit'); @@ -823,7 +824,8 @@ export class AIMicroSystem extends System { for (const nearbyId of nearbyUnits) { if (nearbyId === entityId) continue; - const nearbyEntity = this.world.getEntity(nearbyId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const nearbyEntity = this.world.getEntityByIndex(nearbyId); if (!nearbyEntity) continue; const nearbyUnit = nearbyEntity.get('Unit'); diff --git a/src/engine/systems/CombatSystem.ts b/src/engine/systems/CombatSystem.ts index ac46a9d2..a456098c 100644 --- a/src/engine/systems/CombatSystem.ts +++ b/src/engine/systems/CombatSystem.ts @@ -1163,7 +1163,8 @@ export class CombatSystem extends System { ); for (const entityId of nearbyBuildingIds) { - const entity = this.world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(entityId); if (!validateEntityAlive(entity, entityId, 'CombatSystem:checkCombatZone:buildings')) continue; @@ -1552,7 +1553,8 @@ export class CombatSystem extends System { for (const entityId of nearbyUnitIds) { if (entityId === attackerId) continue; - const entity = this.world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(entityId); if (!validateEntityAlive(entity, entityId, 'CombatSystem:applySplashDamage:units')) continue; const transform = entity.get('Transform'); @@ -1610,7 +1612,8 @@ export class CombatSystem extends System { ); for (const entityId of nearbyBuildingIds) { - const entity = this.world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(entityId); if (!validateEntityAlive(entity, entityId, 'CombatSystem:applySplashDamage:buildings')) continue; diff --git a/src/engine/systems/ProjectileSystem.ts b/src/engine/systems/ProjectileSystem.ts index 5fcad305..f3144527 100644 --- a/src/engine/systems/ProjectileSystem.ts +++ b/src/engine/systems/ProjectileSystem.ts @@ -372,7 +372,8 @@ export class ProjectileSystem extends System { // Skip primary target (already damaged) if (entityId === projectile.targetEntityId) continue; - const entity = this.world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(entityId); if (!validateEntityAlive(entity, entityId, 'ProjectileSystem:applySplashDamage:units')) continue; @@ -449,7 +450,8 @@ export class ProjectileSystem extends System { for (const entityId of nearbyBuildingIds) { if (entityId === projectile.targetEntityId) continue; - const entity = this.world.getEntity(entityId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(entityId); if (!validateEntityAlive(entity, entityId, 'ProjectileSystem:applySplashDamage:buildings')) continue; diff --git a/src/engine/systems/UnitMechanicsSystem.ts b/src/engine/systems/UnitMechanicsSystem.ts index 7571436e..98e23b28 100644 --- a/src/engine/systems/UnitMechanicsSystem.ts +++ b/src/engine/systems/UnitMechanicsSystem.ts @@ -106,7 +106,8 @@ export class UnitMechanicsSystem extends System { playerId?: string; }): void { const entity = this.world.getEntity(data.entityId); - if (!validateEntityAlive(entity, data.entityId, 'UnitMechanicsSystem:handleSetAutocast')) return; + if (!validateEntityAlive(entity, data.entityId, 'UnitMechanicsSystem:handleSetAutocast')) + return; const unit = entity.get('Unit'); if (!unit) return; @@ -130,7 +131,8 @@ export class UnitMechanicsSystem extends System { private handleToggleAutocastRepair(data: { entityIds: number[] }): void { for (const entityId of data.entityIds) { const entity = this.world.getEntity(entityId); - if (!validateEntityAlive(entity, entityId, 'UnitMechanicsSystem:handleToggleAutocastRepair')) continue; + if (!validateEntityAlive(entity, entityId, 'UnitMechanicsSystem:handleToggleAutocastRepair')) + continue; const unit = entity.get('Unit'); if (!unit || !unit.canRepair) continue; @@ -150,7 +152,8 @@ export class UnitMechanicsSystem extends System { private handleTransformCommand(command: TransformCommand): void { for (const entityId of command.entityIds) { const entity = this.world.getEntity(entityId); - if (!validateEntityAlive(entity, entityId, 'UnitMechanicsSystem:handleTransformCommand')) continue; + if (!validateEntityAlive(entity, entityId, 'UnitMechanicsSystem:handleTransformCommand')) + continue; const unit = entity.get('Unit'); if (!unit || !unit.canTransform) continue; @@ -170,7 +173,8 @@ export class UnitMechanicsSystem extends System { private handleCloakCommand(command: CloakCommand): void { for (const entityId of command.entityIds) { const entity = this.world.getEntity(entityId); - if (!validateEntityAlive(entity, entityId, 'UnitMechanicsSystem:handleCloakCommand')) continue; + if (!validateEntityAlive(entity, entityId, 'UnitMechanicsSystem:handleCloakCommand')) + continue; const unit = entity.get('Unit'); if (!unit || !unit.canCloak) continue; @@ -199,7 +203,14 @@ export class UnitMechanicsSystem extends System { private handleLoadCommand(command: LoadCommand): void { const transport = this.world.getEntity(command.transportId); - if (!validateEntityAlive(transport, command.transportId, 'UnitMechanicsSystem:handleLoadCommand:transport')) return; + if ( + !validateEntityAlive( + transport, + command.transportId, + 'UnitMechanicsSystem:handleLoadCommand:transport' + ) + ) + return; const transportUnit = transport.get('Unit'); const transportTransform = transport.get('Transform'); @@ -210,7 +221,8 @@ export class UnitMechanicsSystem extends System { for (const unitId of command.unitIds) { const entity = this.world.getEntity(unitId); - if (!validateEntityAlive(entity, unitId, 'UnitMechanicsSystem:handleLoadCommand:unit')) continue; + if (!validateEntityAlive(entity, unitId, 'UnitMechanicsSystem:handleLoadCommand:unit')) + continue; const unit = entity.get('Unit'); const transform = entity.get('Transform'); @@ -244,7 +256,14 @@ export class UnitMechanicsSystem extends System { private handleUnloadCommand(command: UnloadCommand): void { const transport = this.world.getEntity(command.transportId); - if (!validateEntityAlive(transport, command.transportId, 'UnitMechanicsSystem:handleUnloadCommand:transport')) return; + if ( + !validateEntityAlive( + transport, + command.transportId, + 'UnitMechanicsSystem:handleUnloadCommand:transport' + ) + ) + return; const transportUnit = transport.get('Unit'); const transportTransform = transport.get('Transform'); @@ -261,7 +280,8 @@ export class UnitMechanicsSystem extends System { if (!transportUnit.unloadUnit(unitId)) continue; const entity = this.world.getEntity(unitId); - if (!validateEntityAlive(entity, unitId, 'UnitMechanicsSystem:handleUnloadCommand:unit')) continue; + if (!validateEntityAlive(entity, unitId, 'UnitMechanicsSystem:handleUnloadCommand:unit')) + continue; const unit = entity.get('Unit'); const transform = entity.get('Transform'); @@ -289,7 +309,14 @@ export class UnitMechanicsSystem extends System { private handleLoadBunkerCommand(command: LoadIntoBunkerCommand): void { const bunker = this.world.getEntity(command.bunkerId); - if (!validateEntityAlive(bunker, command.bunkerId, 'UnitMechanicsSystem:handleLoadBunkerCommand:bunker')) return; + if ( + !validateEntityAlive( + bunker, + command.bunkerId, + 'UnitMechanicsSystem:handleLoadBunkerCommand:bunker' + ) + ) + return; const building = bunker.get('Building'); const bunkerTransform = bunker.get('Transform'); @@ -309,7 +336,8 @@ export class UnitMechanicsSystem extends System { if (data.loadedUnits.length >= data.maxCapacity) break; const entity = this.world.getEntity(unitId); - if (!validateEntityAlive(entity, unitId, 'UnitMechanicsSystem:handleLoadBunkerCommand:unit')) continue; + if (!validateEntityAlive(entity, unitId, 'UnitMechanicsSystem:handleLoadBunkerCommand:unit')) + continue; const unit = entity.get('Unit'); const transform = entity.get('Transform'); @@ -339,7 +367,14 @@ export class UnitMechanicsSystem extends System { private handleUnloadBunkerCommand(command: UnloadFromBunkerCommand): void { const bunker = this.world.getEntity(command.bunkerId); - if (!validateEntityAlive(bunker, command.bunkerId, 'UnitMechanicsSystem:handleUnloadBunkerCommand:bunker')) return; + if ( + !validateEntityAlive( + bunker, + command.bunkerId, + 'UnitMechanicsSystem:handleUnloadBunkerCommand:bunker' + ) + ) + return; const bunkerTransform = bunker.get('Transform'); if (!bunkerTransform) return; @@ -359,7 +394,10 @@ export class UnitMechanicsSystem extends System { data.loadedUnits.splice(index, 1); const entity = this.world.getEntity(unitId); - if (!validateEntityAlive(entity, unitId, 'UnitMechanicsSystem:handleUnloadBunkerCommand:unit')) continue; + if ( + !validateEntityAlive(entity, unitId, 'UnitMechanicsSystem:handleUnloadBunkerCommand:unit') + ) + continue; const unit = entity.get('Unit'); const transform = entity.get('Transform'); @@ -385,7 +423,10 @@ export class UnitMechanicsSystem extends System { private handleSalvageBunkerCommand(data: { bunkerId: number; playerId: string }): void { const bunker = this.world.getEntity(data.bunkerId); - if (!validateEntityAlive(bunker, data.bunkerId, 'UnitMechanicsSystem:handleSalvageBunkerCommand')) return; + if ( + !validateEntityAlive(bunker, data.bunkerId, 'UnitMechanicsSystem:handleSalvageBunkerCommand') + ) + return; const building = bunker.get('Building'); const selectable = bunker.get('Selectable'); @@ -421,7 +462,8 @@ export class UnitMechanicsSystem extends System { private handleHealCommand(command: HealCommand): void { const healer = this.world.getEntity(command.healerId); - if (!validateEntityAlive(healer, command.healerId, 'UnitMechanicsSystem:handleHealCommand')) return; + if (!validateEntityAlive(healer, command.healerId, 'UnitMechanicsSystem:handleHealCommand')) + return; const healerUnit = healer.get('Unit'); if (!healerUnit || !healerUnit.canHeal) return; @@ -431,7 +473,10 @@ export class UnitMechanicsSystem extends System { private handleRepairCommand(command: RepairCommand): void { const repairer = this.world.getEntity(command.repairerId); - if (!validateEntityAlive(repairer, command.repairerId, 'UnitMechanicsSystem:handleRepairCommand')) return; + if ( + !validateEntityAlive(repairer, command.repairerId, 'UnitMechanicsSystem:handleRepairCommand') + ) + return; const repairerUnit = repairer.get('Unit'); if (!repairerUnit || !repairerUnit.canRepair) return; @@ -566,7 +611,8 @@ export class UnitMechanicsSystem extends System { ); for (const buildingId of nearbyBuildingIds) { - const buildingEntity = this.world.getEntity(buildingId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const buildingEntity = this.world.getEntityByIndex(buildingId); if (!buildingEntity) continue; const buildingSelectable = buildingEntity.get('Selectable'); @@ -605,7 +651,7 @@ export class UnitMechanicsSystem extends System { if (dist <= autocastRange) { if (!closestTarget || dist < closestTarget.distance) { - closestTarget = { id: buildingId, distance: dist }; + closestTarget = { id: buildingEntity.id, distance: dist }; } } } @@ -620,7 +666,8 @@ export class UnitMechanicsSystem extends System { for (const unitId of nearbyUnitIds) { if (unitId === entity.id) continue; // Skip self - const unitEntity = this.world.getEntity(unitId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const unitEntity = this.world.getEntityByIndex(unitId); if (!unitEntity) continue; const targetUnit = unitEntity.get('Unit'); @@ -650,7 +697,7 @@ export class UnitMechanicsSystem extends System { if (dist <= autocastRange) { if (!closestTarget || dist < closestTarget.distance) { - closestTarget = { id: unitId, distance: dist }; + closestTarget = { id: unitEntity.id, distance: dist }; } } } @@ -922,7 +969,8 @@ export class UnitMechanicsSystem extends System { // Check units for (const unitId of nearbyUnitIds) { - const entity = this.world.getEntity(unitId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(unitId); if (!entity) continue; const selectable = entity.get('Selectable'); @@ -942,7 +990,8 @@ export class UnitMechanicsSystem extends System { // Check buildings for (const buildingId of nearbyBuildingIds) { - const entity = this.world.getEntity(buildingId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(buildingId); if (!entity) continue; const selectable = entity.get('Selectable'); diff --git a/src/engine/systems/VisionSystem.ts b/src/engine/systems/VisionSystem.ts index a3449ae5..bc83c729 100644 --- a/src/engine/systems/VisionSystem.ts +++ b/src/engine/systems/VisionSystem.ts @@ -1049,7 +1049,8 @@ export class VisionSystem extends System { ); for (const unitId of nearbyUnitIds) { - const entity = this.world.getEntity(unitId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(unitId); if (!entity) continue; const transform = entity.get('Transform'); diff --git a/src/engine/systems/WallSystem.ts b/src/engine/systems/WallSystem.ts index 8f7afc71..b925e3d4 100644 --- a/src/engine/systems/WallSystem.ts +++ b/src/engine/systems/WallSystem.ts @@ -133,7 +133,8 @@ export class WallSystem extends System { ); for (const unitId of nearbyUnitIds) { - const unitEntity = this.world.getEntity(unitId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const unitEntity = this.world.getEntityByIndex(unitId); if (!unitEntity) continue; const unitSelectable = unitEntity.get('Selectable'); @@ -164,7 +165,8 @@ export class WallSystem extends System { for (const buildingId of nearbyBuildingIds) { if (buildingId === droneEntity.id) continue; - const wallEntity = this.world.getEntity(buildingId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const wallEntity = this.world.getEntityByIndex(buildingId); if (!wallEntity) continue; // Check if this is a wall belonging to same player diff --git a/src/engine/systems/ai/AITacticsManager.ts b/src/engine/systems/ai/AITacticsManager.ts index f70cbd9d..0efa39bc 100644 --- a/src/engine/systems/ai/AITacticsManager.ts +++ b/src/engine/systems/ai/AITacticsManager.ts @@ -1120,11 +1120,20 @@ export class AITacticsManager { if (!attackTarget) { attackTarget = this.findAnyEnemyBuilding(ai); if (!attackTarget) { - // No valid target - end operation - ai.state = 'building'; - ai.activeAttackOperation = null; - this.isEngaged.set(ai.playerId, false); - return; + // No buildings found - check for enemy units (e.g., battle simulator) + if (this.hasRemainingEnemyUnits(ai)) { + const enemyCluster = this.findEnemyUnitCluster(ai); + if (enemyCluster) { + attackTarget = enemyCluster; + } + } + if (!attackTarget) { + // No valid target at all - end operation + ai.state = 'building'; + ai.activeAttackOperation = null; + this.isEngaged.set(ai.playerId, false); + return; + } } } } diff --git a/src/engine/systems/movement/PathfindingMovement.ts b/src/engine/systems/movement/PathfindingMovement.ts index d929fe55..d1d56082 100644 --- a/src/engine/systems/movement/PathfindingMovement.ts +++ b/src/engine/systems/movement/PathfindingMovement.ts @@ -45,6 +45,7 @@ const DROP_OFF_BUILDINGS = Object.freeze([ */ export interface PathfindingWorld { getEntity(id: number): Entity | undefined; + getEntityByIndex(index: number): Entity | undefined; buildingGrid: { queryRadius(x: number, y: number, radius: number): number[]; }; @@ -444,7 +445,8 @@ export class PathfindingMovement { if (selfUnit.constructingBuildingId === buildingId) continue; if (gatheringExtractorId === buildingId) continue; - const entity = this.world.getEntity(buildingId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(buildingId); if (!entity) continue; const buildingTransform = entity.get('Transform'); @@ -608,7 +610,8 @@ export class PathfindingMovement { for (const buildingId of nearbyBuildingIds) { if (unit.constructingBuildingId === buildingId) continue; - const entity = this.world.getEntity(buildingId); + // SpatialGrid returns entity indices, use getEntityByIndex for lookup + const entity = this.world.getEntityByIndex(buildingId); if (!entity) continue; const buildingTransform = entity.get('Transform'); diff --git a/tests/engine/ai/unitBehaviors.test.ts b/tests/engine/ai/unitBehaviors.test.ts index 7e7bc6af..44ef514a 100644 --- a/tests/engine/ai/unitBehaviors.test.ts +++ b/tests/engine/ai/unitBehaviors.test.ts @@ -114,8 +114,11 @@ function createMockContext(overrides: Partial = {}): BehaviorCo const mockUnitGrid = new SpatialGrid(100, 100, 8); const mockBuildingGrid = new SpatialGrid(100, 100, 8); + const getEntityMock = vi.fn(); const mockWorld = { - getEntity: vi.fn(), + getEntity: getEntityMock, + // SpatialGrid returns indices; getEntityByIndex delegates to same mock for tests + getEntityByIndex: (...args: unknown[]) => getEntityMock(...args), getEntitiesWith: vi.fn().mockReturnValue([]), unitGrid: mockUnitGrid, buildingGrid: mockBuildingGrid,