import React, { useEffect, useRef, useState } from "react"; import { CameraStateListener, GameRenderer } from "@/shared/render"; import { useGameAPI } from "@/shared/api"; import { useThrottledCallback } from "@/shared/hooks"; interface GameCanvasProps { className?: string; initialState?: any | null; onRendererReady?: (renderer: GameRenderer | null) => void; onNationHover?: (nationId: number | null) => void; } export const GameCanvas: React.FC = ({ className, initialState, onRendererReady, onNationHover }) => { const canvasRef = useRef(null); const rendererRef = useRef(null); const [isInitialized, setIsInitialized] = useState(false); const lastHoveredNationRef = useRef(null); const lastHoveredTileRef = useRef(null); const api = useGameAPI(); // Apply initial state when provided (for reload recovery) useEffect(() => { if (!initialState || !rendererRef.current || !isInitialized) return; console.log("Applying initial game state to renderer:", initialState); const renderer = rendererRef.current; try { // Apply RenderInit data: terrain palette, terrain, player palette, territories renderer.setTerrainPalette(initialState.terrain_palette); renderer.initTerrain(initialState.terrain); renderer.initPalette(initialState.palette); renderer.applyTerritorySnapshot(initialState.initial_territories.turn, initialState.initial_territories.territories); console.log("Initial state applied successfully"); } catch (err) { console.error("Failed to apply initial state:", err); } }, [initialState, isInitialized]); // Initialize renderer once on mount useEffect(() => { if (!canvasRef.current || rendererRef.current) return; let cancelled = false; const renderer = new GameRenderer(); rendererRef.current = renderer; // Initialize renderer renderer .init(canvasRef.current) .then(async () => { if (cancelled) { renderer.destroy(); return; } setIsInitialized(true); // Set up camera listener const cameraListener: CameraStateListener = { onCameraUpdate: (x, y, zoom) => { if (api && typeof api.sendCameraUpdate === "function") { api.sendCameraUpdate({ x, y, zoom }); } }, }; renderer.setCameraListener(cameraListener); // Register renderer with API for receiving updates if (api && typeof api.setRenderer === "function") { await api.setRenderer(renderer); } // Notify parent that renderer is ready onRendererReady?.(renderer); }) .catch((err: unknown) => { if (!cancelled) { console.error("Failed to initialize GameRenderer:", err); } }); // Cleanup return () => { cancelled = true; if (rendererRef.current) { // Unregister renderer from API before destroying if (api && typeof api.setRenderer === "function") { api.setRenderer(null); } rendererRef.current.destroy(); rendererRef.current = null; onRendererReady?.(null); } setIsInitialized(false); }; }, [api, onRendererReady]); // Handle canvas clicks using native event listener (after renderer initialization) // React onClick doesn't work reliably when native listeners are attached to canvas useEffect(() => { const canvas = canvasRef.current; if (!canvas || !rendererRef.current || !api) return; const handleCanvasClick = (e: MouseEvent) => { if (!rendererRef.current || !api) return; // Ignore clicks that were camera drags if (rendererRef.current.hadCameraInteraction()) { return; } const rect = canvas.getBoundingClientRect(); const screenX = e.clientX - rect.left; const screenY = e.clientY - rect.top; const tileIndex = rendererRef.current.screenToTile(screenX, screenY); if (typeof api.sendMapClick === "function") { const worldPos = rendererRef.current.tileToWorld(tileIndex || 0); api.sendMapClick({ tile_index: tileIndex, world_x: worldPos.x, world_y: worldPos.y, button: e.button, }); } }; canvas.addEventListener("click", handleCanvasClick); return () => canvas.removeEventListener("click", handleCanvasClick); }, [api, isInitialized]); // Handle mouse move (for hover) - throttled to reduce IPC overhead const handleMouseMove = useThrottledCallback((e: React.MouseEvent) => { if (!rendererRef.current) return; const rect = canvasRef.current!.getBoundingClientRect(); const screenX = e.clientX - rect.left; const screenY = e.clientY - rect.top; const tileIndex = rendererRef.current.screenToTile(screenX, screenY); if (tileIndex === null) return; // Handle nation hover detection if (onNationHover) { const nationId = rendererRef.current.getNationAtTile(tileIndex); if (nationId !== lastHoveredNationRef.current) { lastHoveredNationRef.current = nationId; onNationHover(nationId); } } // Only send map hover if tile changed if (tileIndex !== lastHoveredTileRef.current) { lastHoveredTileRef.current = tileIndex; if (api && typeof api.sendMapHover === "function") { const worldPos = rendererRef.current.tileToWorld(tileIndex); // Only send if we have valid coordinates if (isFinite(worldPos.x) && isFinite(worldPos.y)) { api.sendMapHover({ tile_index: tileIndex, world_x: worldPos.x, world_y: worldPos.y, }); } } } }, 10); // Handle keyboard input useEffect(() => { if (!api) return; const handleKeyDown = (e: KeyboardEvent) => { if (typeof api.sendKeyPress === "function") { api.sendKeyPress({ key: e.code, pressed: true, }); } }; const handleKeyUp = (e: KeyboardEvent) => { if (typeof api.sendKeyPress === "function") { api.sendKeyPress({ key: e.code, pressed: false, }); } }; window.addEventListener("keydown", handleKeyDown); window.addEventListener("keyup", handleKeyUp); return () => { window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); }; }, [api]); return (
e.preventDefault()} style={{ display: "block", width: "100%", height: "100%", cursor: isInitialized ? undefined : "wait", }} /> {!isInitialized && (
Initializing renderer...
)}
); };