From 888df0d9a1d4a3196d9b60c7698d092dadc263fa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Feb 2026 17:09:03 +0000 Subject: [PATCH 1/2] fix: send render state after spawn/destroy when game is paused When the battle simulator pauses the game, the game loop stops and render states are no longer sent. This meant spawned/destroyed entities weren't visible to the main thread's RenderStateWorldAdapter, causing the Fight and Clear buttons to operate on stale data. Added forceRenderStateUpdate() method to WorkerGame and call it after spawnUnit and destroyEntity to ensure the main thread always has current entity data. https://claude.ai/code/session_01RByrKsfTJ9jf3kCjztLrY5 --- src/engine/workers/GameWorker.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/engine/workers/GameWorker.ts b/src/engine/workers/GameWorker.ts index 4492cae5..8a0b32fa 100644 --- a/src/engine/workers/GameWorker.ts +++ b/src/engine/workers/GameWorker.ts @@ -655,6 +655,14 @@ export class WorkerGame extends GameCore { return this.gameTime; } + /** + * Force send a render state update to the main thread. + * Used when entities are spawned/destroyed while the game is paused. + */ + public forceRenderStateUpdate(): void { + this.sendRenderState(); + } + // ============================================================================ // RENDER STATE COLLECTION // ============================================================================ @@ -1417,12 +1425,16 @@ if (typeof self !== 'undefined') { y: message.y, playerId: message.playerId, }); + // Send render state so main thread sees the new entity (even when paused) + game.forceRenderStateUpdate(); break; } case 'destroyEntity': { if (!game) return; game.eventBus.emit('entity:destroy', { entityId: message.entityId }); + // Send render state so main thread sees the entity removal (even when paused) + game.forceRenderStateUpdate(); break; } } From 39b1c3a42dd3f481f010d25018770ddd773cc291 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Feb 2026 17:16:46 +0000 Subject: [PATCH 2/2] fix: enable AI and add registerAI for battle simulator fight button The Fight button wasn't working because: 1. AI was disabled for battle simulator mode (aiEnabled: false) 2. There was no way to register players as AI-controlled via the worker Changes: - Enable AI systems for battle simulator mode in useWorkerBridge - Add registerAI message type to worker communication protocol - Add registerAI handler in GameWorker to call EnhancedAISystem.registerAI - Add registerAI method to WorkerBridge - Update handleFight to register both players as AI before resuming When Fight is clicked, both players are registered as AI-controlled, allowing the EnhancedAISystem and AIMicroSystem to take over and make units automatically attack enemies. https://claude.ai/code/session_01RByrKsfTJ9jf3kCjztLrY5 --- src/components/game/BattleSimulatorPanel.tsx | 8 +++- src/components/game/hooks/useWorkerBridge.ts | 9 +++- src/engine/workers/GameWorker.ts | 13 +++++ src/engine/workers/WorkerBridge.ts | 16 +++++++ src/engine/workers/types.ts | 50 +++++++++++++++++--- 5 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/components/game/BattleSimulatorPanel.tsx b/src/components/game/BattleSimulatorPanel.tsx index 7eafd139..2b000e8e 100644 --- a/src/components/game/BattleSimulatorPanel.tsx +++ b/src/components/game/BattleSimulatorPanel.tsx @@ -223,6 +223,12 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { return; } + // 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'; + bridge.registerAI('player1', player1Faction, 'medium'); + bridge.registerAI('player2', player2Faction, 'medium'); + const currentTick = bridge.currentTick; // Get all units from render state adapter @@ -278,7 +284,7 @@ export const BattleSimulatorPanel = memo(function BattleSimulatorPanel() { bridge.resume(); setIsPaused(false); setSelectedUnit(null); - }, []); + }, [playerSlots]); const handlePauseToggle = useCallback(() => { const bridge = getWorkerBridge(); diff --git a/src/components/game/hooks/useWorkerBridge.ts b/src/components/game/hooks/useWorkerBridge.ts index f2fc9206..7d2bf4ab 100644 --- a/src/components/game/hooks/useWorkerBridge.ts +++ b/src/components/game/hooks/useWorkerBridge.ts @@ -20,7 +20,12 @@ import { type RenderState, type GameEvent, } from '@/engine/workers'; -import { useGameSetupStore, getLocalPlayerId, isBattleSimulatorMode, type GameSetupState } from '@/store/gameSetupStore'; +import { + useGameSetupStore, + getLocalPlayerId, + isBattleSimulatorMode, + type GameSetupState, +} from '@/store/gameSetupStore'; import { useUIStore } from '@/store/uiStore'; import { useGameStore } from '@/store/gameStore'; import { isMultiplayerMode } from '@/store/multiplayerStore'; @@ -158,7 +163,7 @@ export function useWorkerBridge({ map, onGameOver }: UseWorkerBridgeProps): UseW tickRate: 20, isMultiplayer, playerId: localPlayerId ?? 'spectator', - aiEnabled: !isBattleSimulatorMode() && !isMultiplayer, + aiEnabled: !isMultiplayer, aiDifficulty: 'medium', fogOfWar, }, diff --git a/src/engine/workers/GameWorker.ts b/src/engine/workers/GameWorker.ts index 8a0b32fa..db23b4f1 100644 --- a/src/engine/workers/GameWorker.ts +++ b/src/engine/workers/GameWorker.ts @@ -1437,6 +1437,19 @@ if (typeof self !== 'undefined') { game.forceRenderStateUpdate(); break; } + + case 'registerAI': { + if (!game) return; + const enhancedAI = game.world.getSystem(EnhancedAISystem); + if (enhancedAI) { + enhancedAI.registerAI( + message.playerId, + message.faction, + message.difficulty ?? 'medium' + ); + } + break; + } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/src/engine/workers/WorkerBridge.ts b/src/engine/workers/WorkerBridge.ts index fe2acdb6..0e6d4331 100644 --- a/src/engine/workers/WorkerBridge.ts +++ b/src/engine/workers/WorkerBridge.ts @@ -561,6 +561,22 @@ export class WorkerBridge { } satisfies MainToWorkerMessage); } + /** + * Register a player as AI-controlled (for battle simulator) + */ + public registerAI( + playerId: string, + faction: string, + difficulty: 'easy' | 'medium' | 'hard' | 'insane' = 'medium' + ): void { + this.worker?.postMessage({ + type: 'registerAI', + playerId, + faction, + difficulty, + } satisfies MainToWorkerMessage); + } + public setControlGroup(groupNumber: number, entityIds: number[]): void { this.worker?.postMessage({ type: 'setControlGroup', diff --git a/src/engine/workers/types.ts b/src/engine/workers/types.ts index bf76e90f..5789e7ea 100644 --- a/src/engine/workers/types.ts +++ b/src/engine/workers/types.ts @@ -358,7 +358,12 @@ export interface SelectionChangedEvent { export interface AlertEvent { type: 'alert'; - alertType: 'under_attack' | 'unit_ready' | 'research_complete' | 'resources_low' | 'base_destroyed'; + alertType: + | 'under_attack' + | 'unit_ready' + | 'research_complete' + | 'resources_low' + | 'base_destroyed'; position?: { x: number; y: number }; playerId: string; details?: string; @@ -418,7 +423,13 @@ export type MainToWorkerMessage = | { type: 'spawnEntities'; mapData: SpawnMapData } | { type: 'setPerformanceCollection'; enabled: boolean } | { type: 'spawnUnit'; unitType: string; x: number; y: number; playerId: string } - | { type: 'destroyEntity'; entityId: number }; + | { type: 'destroyEntity'; entityId: number } + | { + type: 'registerAI'; + playerId: string; + faction: string; + difficulty?: 'easy' | 'medium' | 'hard' | 'insane'; + }; /** * Player slot info for spawning @@ -503,7 +514,21 @@ export type { GameCommand } from '../core/GameCommand'; */ export const TRANSFORM_FLOATS_PER_ENTITY = 11; -export function packTransforms(entities: Array<{ x: number; y: number; z: number; rotation: number; scaleX: number; scaleY: number; scaleZ: number; prevX: number; prevY: number; prevZ: number; prevRotation: number }>): Float32Array { +export function packTransforms( + entities: Array<{ + x: number; + y: number; + z: number; + rotation: number; + scaleX: number; + scaleY: number; + scaleZ: number; + prevX: number; + prevY: number; + prevZ: number; + prevRotation: number; + }> +): Float32Array { const buffer = new Float32Array(entities.length * TRANSFORM_FLOATS_PER_ENTITY); let offset = 0; for (const e of entities) { @@ -522,10 +547,21 @@ export function packTransforms(entities: Array<{ x: number; y: number; z: number return buffer; } -export function unpackTransform(buffer: Float32Array, index: number): { - x: number; y: number; z: number; rotation: number; - scaleX: number; scaleY: number; scaleZ: number; - prevX: number; prevY: number; prevZ: number; prevRotation: number; +export function unpackTransform( + buffer: Float32Array, + index: number +): { + x: number; + y: number; + z: number; + rotation: number; + scaleX: number; + scaleY: number; + scaleZ: number; + prevX: number; + prevY: number; + prevZ: number; + prevRotation: number; } { const offset = index * TRANSFORM_FLOATS_PER_ENTITY; return {