// Player state and controls const player = { position: { q: 0, r: 0 }, // Current hex position target: null, // Target hex to move to path: [], // Array of hex coordinates to follow movementProgress: 0, // Progress of current movement (0 to 1) moveSpeed: Config.player.MOVE_SPEED, // Movement speed (0 to 1 per frame) // Initialize player init() { this.position = { q: 0, r: 0 }; this.target = null; this.path = []; return this; }, // Check if a hex coordinate is within grid bounds isValidHex(hex) { const halfGrid = Math.floor(HexGrid.GRID_SIZE / 2); return Math.abs(hex.q) <= halfGrid && Math.abs(hex.r) <= halfGrid; }, // Get neighbors that share an edge with the given hex getEdgeNeighbors(hex) { const directions = [ {q: 1, r: 0}, // East {q: 0, r: 1}, // Southeast {q: -1, r: 1}, // Southwest {q: -1, r: 0}, // West {q: 0, r: -1}, // Northwest {q: 1, r: -1} // Northeast ]; // Only return neighbors that are within grid bounds return directions .map(dir => ({ q: hex.q + dir.q, r: hex.r + dir.r })) .filter(hex => this.isValidHex(hex)); }, // Find path from current position to target findPath(targetHex) { const start = this.position; const goal = targetHex; // Simple breadth-first search const queue = [[start]]; const visited = new Set(); const key = hex => `${hex.q},${hex.r}`; visited.add(key(start)); while (queue.length > 0) { const path = queue.shift(); const current = path[path.length - 1]; if (current.q === goal.q && current.r === goal.r) { return path; } const neighbors = this.getEdgeNeighbors(current); for (const neighbor of neighbors) { const neighborKey = key(neighbor); if (!visited.has(neighborKey)) { visited.add(neighborKey); queue.push([...path, neighbor]); } } } return null; // No path found }, // Start moving to a target hex moveTo(targetHex) { // Only start new movement if we're not already moving and target is valid if (!this.target) { // Check if target is within grid bounds if (!this.isValidHex(targetHex)) { return; // Ignore movement request if target is out of bounds } const path = this.findPath(targetHex); if (path) { // Filter out any path points that would go out of bounds this.path = path.slice(1).filter(hex => this.isValidHex(hex)); if (this.path.length > 0) { this.target = this.path.shift(); this.movementProgress = 0; } } } }, // Update player position update() { if (this.target) { this.movementProgress += this.moveSpeed; if (this.movementProgress >= 1) { this.position = this.target; this.target = null; this.movementProgress = 0; this.hasMoved = true; if (this.path.length > 0) { this.target = this.path.shift(); this.movementProgress = 0; } } } }, // Get current interpolated position getCurrentPosition() { if (!this.target) { return this.position; } // Interpolate between current position and target return { q: this.position.q + (this.target.q - this.position.q) * this.movementProgress, r: this.position.r + (this.target.r - this.position.r) * this.movementProgress }; }, // Draw the player draw(ctx, hexToPixel, camera, HEX_SIZE) { const currentPos = this.getCurrentPosition(); const pixelPos = hexToPixel(currentPos); const screenX = pixelPos.x - camera.x; const screenY = pixelPos.y - camera.y; ctx.fillStyle = Config.colors.PLAYER; ctx.beginPath(); ctx.arc(screenX, screenY, HEX_SIZE * Config.player.SIZE_RATIO, 0, Math.PI * 2); ctx.fill(); // Optionally, draw the remaining path if (this.path.length > 0) { ctx.strokeStyle = Config.colors.PLAYER + '4D'; // 30% opacity version of player color ctx.beginPath(); let lastPos = this.target || this.position; this.path.forEach(point => { const from = hexToPixel(lastPos); const to = hexToPixel(point); ctx.moveTo(from.x - camera.x, from.y - camera.y); ctx.lineTo(to.x - camera.x, to.y - camera.y); lastPos = point; }); ctx.stroke(); } } };