import { BuildableUnit, Cell, PlayerActions, PlayerBorderTiles, PlayerBuildableUnitType, PlayerID, PlayerProfile, } from "../game/Game"; import { TileRef } from "../game/GameMap"; import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates"; import { ClientID, GameStartInfo, Turn } from "../Schemas"; import { generateID } from "../Util"; import { WorkerMessage } from "./WorkerMessages"; export class WorkerClient { private worker: Worker; private isInitialized = false; private messageHandlers: Map void>; private gameUpdateCallback?: ( update: GameUpdateViewData | ErrorUpdate, ) => void; constructor( private gameStartInfo: GameStartInfo, private clientID: ClientID | undefined, ) { this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url), { type: "module", }); this.messageHandlers = new Map(); // Set up global message handler this.worker.addEventListener( "message", this.handleWorkerMessage.bind(this), ); } private handleWorkerMessage(event: MessageEvent) { const message = event.data; switch (message.type) { case "game_update": if (this.gameUpdateCallback && message.gameUpdate) { this.gameUpdateCallback(message.gameUpdate); } break; case "game_update_batch": if (this.gameUpdateCallback && message.gameUpdates) { for (const gu of message.gameUpdates) { this.gameUpdateCallback(gu); } } break; case "initialized": default: if (message.id && this.messageHandlers.has(message.id)) { const handler = this.messageHandlers.get(message.id)!; handler(message); this.messageHandlers.delete(message.id); } break; } } initialize(): Promise { return new Promise((resolve, reject) => { const messageId = generateID(); this.messageHandlers.set(messageId, (message) => { if (message.type === "initialized") { this.isInitialized = true; resolve(); } }); this.worker.postMessage({ type: "init", id: messageId, gameStartInfo: this.gameStartInfo, clientID: this.clientID, }); // Add timeout for initialization setTimeout(() => { if (!this.isInitialized) { this.messageHandlers.delete(messageId); reject(new Error("Worker initialization timeout")); } }, 20000); // 20 second timeout }); } start(gameUpdate: (gu: GameUpdateViewData | ErrorUpdate) => void) { if (!this.isInitialized) { throw new Error("Failed to initialize pathfinder"); } this.gameUpdateCallback = gameUpdate; } sendTurn(turn: Turn) { if (!this.isInitialized) { throw new Error("Worker not initialized"); } this.worker.postMessage({ type: "turn", turn, }); } playerProfile(playerID: number): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error("Worker not initialized")); return; } const messageId = generateID(); this.messageHandlers.set(messageId, (message) => { if ( message.type === "player_profile_result" && message.result !== undefined ) { resolve(message.result); } }); this.worker.postMessage({ type: "player_profile", id: messageId, playerID: playerID, }); }); } playerBorderTiles(playerID: PlayerID): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error("Worker not initialized")); return; } const messageId = generateID(); this.messageHandlers.set(messageId, (message) => { if ( message.type === "player_border_tiles_result" && message.result !== undefined ) { resolve(message.result); } }); this.worker.postMessage({ type: "player_border_tiles", id: messageId, playerID: playerID, }); }); } playerInteraction( playerID: PlayerID, x?: number, y?: number, units?: readonly PlayerBuildableUnitType[] | null, ): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error("Worker not initialized")); return; } const messageId = generateID(); this.messageHandlers.set(messageId, (message) => { if ( message.type === "player_actions_result" && message.result !== undefined ) { resolve(message.result); } }); this.worker.postMessage({ type: "player_actions", id: messageId, playerID, x, y, units, }); }); } playerBuildables( playerID: PlayerID, x?: number, y?: number, units?: readonly PlayerBuildableUnitType[], ): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error("Worker not initialized")); return; } const messageId = generateID(); this.messageHandlers.set(messageId, (message) => { if ( message.type === "player_buildables_result" && message.result !== undefined ) { resolve(message.result); } }); this.worker.postMessage({ type: "player_buildables", id: messageId, playerID, x, y, units, }); }); } attackClusteredPositions( playerID: number, attackID?: string, ): Promise<{ id: string; positions: Cell[] }[]> { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error("Worker not initialized")); return; } const messageId = generateID(); const timeout = setTimeout(() => { this.messageHandlers.delete(messageId); reject(new Error("attack_clustered_positions request timed out")); }, 5000); this.messageHandlers.set(messageId, (message) => { clearTimeout(timeout); if (message.type !== "attack_clustered_positions_result") { reject( new Error( `Unexpected message type for attackClusteredPositions: ${message.type}`, ), ); return; } resolve( message.attacks.map((a) => ({ id: a.id, positions: a.positions.map((c) => new Cell(c.x, c.y)), })), ); }); this.worker.postMessage({ type: "attack_clustered_positions", id: messageId, playerID, attackID, }); }); } transportShipSpawn( playerID: PlayerID, targetTile: TileRef, ): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error("Worker not initialized")); return; } const messageId = generateID(); this.messageHandlers.set(messageId, (message) => { if ( message.type === "transport_ship_spawn_result" && message.result !== undefined ) { resolve(message.result); } }); this.worker.postMessage({ type: "transport_ship_spawn", id: messageId, playerID: playerID, targetTile: targetTile, }); }); } cleanup() { this.worker.terminate(); this.messageHandlers.clear(); this.gameUpdateCallback = undefined; } }