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 4492cae5..db23b4f1 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,29 @@ 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; + } + + case 'registerAI': { + if (!game) return; + const enhancedAI = game.world.getSystem(EnhancedAISystem); + if (enhancedAI) { + enhancedAI.registerAI( + message.playerId, + message.faction, + message.difficulty ?? 'medium' + ); + } break; } } 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 {