From 2142e52839169ae88da63a428c2eb97b88a21267 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Feb 2026 18:13:26 +0000 Subject: [PATCH] fix: add targeting capability fields to UnitRenderState for battle simulator The battle simulator's Fight button wasn't working because the findValidTarget() function needed isNaval, canAttackGround, and canAttackAir fields which weren't being sent in the render state. - Add isNaval, canAttackGround, canAttackAir to UnitRenderState interface - Update GameWorker to send these fields in the render state - Update UnitAdapter to expose these fields to the main thread - Remove debug logging from BattleSimulatorPanel https://claude.ai/code/session_01RByrKsfTJ9jf3kCjztLrY5 --- src/components/game/BattleSimulatorPanel.tsx | 53 ++------------- src/engine/workers/GameWorker.ts | 4 ++ src/engine/workers/RenderStateAdapter.ts | 69 +++++++++++++------- src/engine/workers/types.ts | 4 ++ 4 files changed, 60 insertions(+), 70 deletions(-) diff --git a/src/components/game/BattleSimulatorPanel.tsx b/src/components/game/BattleSimulatorPanel.tsx index e608401f..4d2d387a 100644 --- a/src/components/game/BattleSimulatorPanel.tsx +++ b/src/components/game/BattleSimulatorPanel.tsx @@ -210,21 +210,13 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { }, [selectedUnit, selectedTeam, spawnQuantity]); const handleFight = useCallback(() => { - console.log('[BattleSimulator] handleFight called'); const bridge = getWorkerBridge(); const worldAdapter = RenderStateWorldAdapter.getInstance(); - if (!worldAdapter) { - console.warn('[BattleSimulator] No world adapter available'); + if (!worldAdapter || !bridge) { return; } - if (!bridge) { - console.warn('[BattleSimulator] No worker bridge available'); - return; - } - - console.log('[BattleSimulator] Registering AI for both players'); // Register both players as AI-controlled so the AI takes over and fights const player1Faction = playerSlots[0]?.faction ?? 'dominion'; const player2Faction = playerSlots[1]?.faction ?? 'dominion'; @@ -232,18 +224,9 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { bridge.registerAI('player2', player2Faction, 'medium'); const currentTick = bridge.currentTick; - console.log('[BattleSimulator] Current tick:', currentTick); // Get all units from render state adapter const entities = worldAdapter.getEntitiesWith('Unit', 'Selectable', 'Transform', 'Health'); - console.log('[BattleSimulator] Found entities:', entities.length); - - let skippedNoComponents = 0; - let skippedDead = 0; - let skippedWorker = 0; - let skippedWrongPlayer = 0; - let attackCommands = 0; - let moveCommands = 0; for (const entity of entities) { const selectable = entity.get<{ playerId: string }>('Selectable'); @@ -259,29 +242,16 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { const transform = entity.get<{ x: number; y: number }>('Transform'); const health = entity.get<{ isDead: () => boolean }>('Health'); - if (!selectable || !unit || !transform || !health) { - skippedNoComponents++; - continue; - } - if (health.isDead()) { - skippedDead++; - continue; - } - if (unit.isWorker) { - skippedWorker++; - continue; - } + if (!selectable || !unit || !transform || !health) continue; + if (health.isDead()) continue; + if (unit.isWorker) continue; const playerId = selectable.playerId; - if (playerId !== 'player1' && playerId !== 'player2') { - skippedWrongPlayer++; - continue; - } + if (playerId !== 'player1' && playerId !== 'player2') continue; const targetId = findValidTarget(worldAdapter, entity.id, unit, transform, playerId); if (targetId !== null) { - attackCommands++; const attackCommand: GameCommand = { tick: currentTick, playerId, @@ -293,7 +263,6 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { } else { const enemyCenter = findEnemyCenter(worldAdapter, unit, playerId); if (enemyCenter) { - moveCommands++; const moveCommand: GameCommand = { tick: currentTick, playerId, @@ -306,21 +275,9 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { } } - console.log('[BattleSimulator] Entity processing stats:', { - total: entities.length, - skippedNoComponents, - skippedDead, - skippedWorker, - skippedWrongPlayer, - attackCommands, - moveCommands, - }); - - console.log('[BattleSimulator] Calling bridge.resume()'); bridge.resume(); setIsPaused(false); setSelectedUnit(null); - console.log('[BattleSimulator] handleFight completed'); }, [playerSlots]); const handlePauseToggle = useCallback(() => { diff --git a/src/engine/workers/GameWorker.ts b/src/engine/workers/GameWorker.ts index db23b4f1..9954b63c 100644 --- a/src/engine/workers/GameWorker.ts +++ b/src/engine/workers/GameWorker.ts @@ -835,6 +835,10 @@ export class WorkerGame extends GameCore { // Combat stats for range overlays attackRange: unit.attackRange, sightRange: unit.sightRange, + // Targeting capabilities + isNaval: unit.isNaval, + canAttackGround: unit.canAttackGround, + canAttackAir: unit.canAttackAir, // Selection properties for hit detection selectionRadius: selectable.selectionRadius, selectionPriority: selectable.selectionPriority, diff --git a/src/engine/workers/RenderStateAdapter.ts b/src/engine/workers/RenderStateAdapter.ts index cca38f02..d1c87db4 100644 --- a/src/engine/workers/RenderStateAdapter.ts +++ b/src/engine/workers/RenderStateAdapter.ts @@ -128,10 +128,19 @@ class UnitAdapter { public targetY: number | null; public speed: number; // Command queue for shift-click visualization - public commandQueue: Array<{ type: string; targetX?: number; targetY?: number; targetEntityId?: number }>; + public commandQueue: Array<{ + type: string; + targetX?: number; + targetY?: number; + targetEntityId?: number; + }>; // Combat stats for range overlays public attackRange: number; public sightRange: number; + // Targeting capabilities + public isNaval: boolean; + public canAttackGround: boolean; + public canAttackAir: boolean; constructor(data: UnitRenderState) { this.unitId = data.unitId; @@ -153,6 +162,9 @@ class UnitAdapter { this.commandQueue = data.commandQueue; this.attackRange = data.attackRange; this.sightRange = data.sightRange; + this.isNaval = data.isNaval; + this.canAttackGround = data.canAttackGround; + this.canAttackAir = data.canAttackAir; } public update(data: UnitRenderState): void { @@ -175,6 +187,9 @@ class UnitAdapter { this.commandQueue = data.commandQueue; this.attackRange = data.attackRange; this.sightRange = data.sightRange; + this.isNaval = data.isNaval; + this.canAttackGround = data.canAttackGround; + this.canAttackAir = data.canAttackAir; } public isSelected(): boolean { @@ -371,9 +386,7 @@ class BuildingComponentAdapter { this.isFlying = data.isFlying; this.liftProgress = data.liftProgress; // Simulate productionQueue for renderer compatibility - this.productionQueue = data.hasProductionQueue - ? [{ progress: data.productionProgress }] - : []; + this.productionQueue = data.hasProductionQueue ? [{ progress: data.productionProgress }] : []; this.attackRange = data.attackRange; this.sightRange = data.sightRange; } @@ -389,9 +402,7 @@ class BuildingComponentAdapter { this.isFlying = data.isFlying; this.liftProgress = data.liftProgress; // Simulate productionQueue for renderer compatibility - this.productionQueue = data.hasProductionQueue - ? [{ progress: data.productionProgress }] - : []; + this.productionQueue = data.hasProductionQueue ? [{ progress: data.productionProgress }] : []; this.attackRange = data.attackRange; this.sightRange = data.sightRange; } @@ -576,7 +587,7 @@ class ResourceComponentAdapter { } public getDepletionPercent(): number { - return this.maxAmount > 0 ? 1 - (this.amount / this.maxAmount) : 0; + return this.maxAmount > 0 ? 1 - this.amount / this.maxAmount : 0; } public getCurrentGatherers(): number { @@ -622,7 +633,9 @@ export class RenderStateWorldAdapter implements IWorldProvider { if (!instance) { instance = new RenderStateWorldAdapter(); global[RENDER_STATE_ADAPTER_KEY] = instance; - debugInitialization.log('[RenderStateWorldAdapter] Created new singleton instance on globalThis'); + debugInitialization.log( + '[RenderStateWorldAdapter] Created new singleton instance on globalThis' + ); } return instance; } @@ -683,9 +696,11 @@ export class RenderStateWorldAdapter implements IWorldProvider { try { this._updateCount++; - // Debug: log first significant update - if (!this.hasLoggedFirstUpdate && (state.units.length > 0 || state.buildings.length > 0 || state.resources.length > 0)) { + if ( + !this.hasLoggedFirstUpdate && + (state.units.length > 0 || state.buildings.length > 0 || state.resources.length > 0) + ) { debugInitialization.log('[RenderStateWorldAdapter] First update with entities:', { tick: state.tick, units: state.units.length, @@ -753,7 +768,12 @@ export class RenderStateWorldAdapter implements IWorldProvider { } // Mark as ready once we have entities - if (!this._isReady && (this.unitEntities.size > 0 || this.buildingEntities.size > 0 || this.resourceEntities.size > 0)) { + if ( + !this._isReady && + (this.unitEntities.size > 0 || + this.buildingEntities.size > 0 || + this.resourceEntities.size > 0) + ) { this._isReady = true; debugInitialization.log('[RenderStateWorldAdapter] Adapter is now ready with entities:', { units: this.unitEntities.size, @@ -762,7 +782,10 @@ export class RenderStateWorldAdapter implements IWorldProvider { }); } } catch (error) { - debugInitialization.error('[RenderStateWorldAdapter] Error updating from render state:', error); + debugInitialization.error( + '[RenderStateWorldAdapter] Error updating from render state:', + error + ); } } @@ -786,13 +809,13 @@ export class RenderStateWorldAdapter implements IWorldProvider { // Units: return when querying for Unit, Selectable (without Building/Resource filter), // or Transform alone (legacy) - const includeUnits = hasUnit || - (hasSelectable && !hasBuilding && !hasResource) || - (hasTransform && componentTypes.length === 1); + const includeUnits = + hasUnit || + (hasSelectable && !hasBuilding && !hasResource) || + (hasTransform && componentTypes.length === 1); // Buildings: return when querying for Building, or Selectable (without Unit/Resource filter) - const includeBuildings = hasBuilding || - (hasSelectable && !hasUnit && !hasResource); + const includeBuildings = hasBuilding || (hasSelectable && !hasUnit && !hasResource); // Resources: return when querying for Resource const includeResources = hasResource; @@ -822,10 +845,12 @@ export class RenderStateWorldAdapter implements IWorldProvider { * Get entity by ID */ public getEntity(entityId: number): IEntity | null { - return this.unitEntities.get(entityId) - ?? this.buildingEntities.get(entityId) - ?? this.resourceEntities.get(entityId) - ?? null; + return ( + this.unitEntities.get(entityId) ?? + this.buildingEntities.get(entityId) ?? + this.resourceEntities.get(entityId) ?? + null + ); } /** diff --git a/src/engine/workers/types.ts b/src/engine/workers/types.ts index 5789e7ea..8a65d9e8 100644 --- a/src/engine/workers/types.ts +++ b/src/engine/workers/types.ts @@ -80,6 +80,10 @@ export interface UnitRenderState { // Combat stats for range overlays attackRange: number; sightRange: number; + // Targeting capabilities + isNaval: boolean; + canAttackGround: boolean; + canAttackAir: boolean; // Selection properties for hit detection selectionRadius: number; selectionPriority: number;