const FPS = 60; const FRAME_TIME = 1000 / FPS; let lastFrameTime = 0; const state = { canvas: null, ctx: null }; function init() { state.canvas = document.getElementById('gameCanvas'); state.ctx = state.canvas.getContext('2d'); player.init(); function resize() { state.canvas.width = window.innerWidth; state.canvas.height = window.innerHeight; Camera.centerOn(player.position); } window.addEventListener('resize', resize); resize(); state.canvas.addEventListener('click', handleClick); requestAnimationFrame(gameLoop); FogOfWar.init(); Items.init(); } function drawHex(ctx, x, y, hex) { const screenX = x - Camera.x; const screenY = y - Camera.y; // Only draw if hex is visible on screen (with some padding) if (screenX < -HexGrid.WIDTH || screenX > state.canvas.width + HexGrid.WIDTH || screenY < -HexGrid.HEIGHT || screenY > state.canvas.height + HexGrid.HEIGHT) { return; } // Only draw hex if it's been revealed if (FogOfWar.isRevealed(hex)) { ctx.beginPath(); for (let i = 0; i < 6; i++) { const angle = 2 * Math.PI / 6 * i; const xPos = screenX + HexGrid.SIZE * Math.cos(angle); const yPos = screenY + HexGrid.SIZE * Math.sin(angle); if (i === 0) { ctx.moveTo(xPos, yPos); } else { ctx.lineTo(xPos, yPos); } } ctx.closePath(); ctx.fillStyle = Config.colors.HEX_FILL; ctx.fill(); // Draw border with appropriate color based on visibility if (!FogOfWar.isVisible(hex)) { ctx.strokeStyle = 'rgba(0, 0, 0, 0.25)'; } else { ctx.strokeStyle = HexGrid.COLOR; // Normal grid color } ctx.lineWidth = 1; ctx.stroke(); } } function gameLoop(currentTime) { requestAnimationFrame(gameLoop); // Request next frame first if (currentTime - lastFrameTime < Config.game.FRAME_TIME) { return; // Skip frame if too soon } // Ensure consistent time step const deltaTime = Math.min(currentTime - lastFrameTime, Config.game.FRAME_TIME * 2); lastFrameTime = currentTime; // Clear with background state.ctx.fillStyle = Config.colors.BACKGROUND; state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height); // Round camera position to prevent sub-pixel rendering Camera.x = Math.round(Camera.x); Camera.y = Math.round(Camera.y); player.update(); Camera.smoothFollow(player.getCurrentPosition()); if (player.hasMoved) { FogOfWar.updateVisibility(player.position); player.hasMoved = false; } // Draw everything in one pass to prevent flicker HexGrid.getViewportHexes().forEach(hex => { const pixel = HexGrid.toPixel(hex); drawHex(state.ctx, Math.round(pixel.x), Math.round(pixel.y), hex); }); Items.draw(state.ctx, HexGrid.toPixel.bind(HexGrid), Camera, HexGrid.SIZE); player.draw(state.ctx, HexGrid.toPixel.bind(HexGrid), Camera, HexGrid.SIZE); FogOfWar.draw(state.ctx); InventoryUI.draw(state.ctx); } function handleClick(event) { if (InventoryUI.isOpen) { InventoryUI.toggleInventory(); return; } const rect = state.canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const hexCoord = HexGrid.fromPixel(x + Camera.x, y + Camera.y); // Check if clicking on player's position if (hexCoord.q === player.position.q && hexCoord.r === player.position.r) { InventoryUI.toggleInventory(); } else { player.moveTo(hexCoord); } } init();