// 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) inventory: [], // Animation properties animation: null, sprites: [], // Initialize player async init() { this.position = { q: 0, r: 0 }; this.target = null; this.path = []; this.inventory = []; // Load sprites try { const [sprite1, sprite2] = await Promise.all([ Animation.loadImage('assets/home/goblin-01.png'), Animation.loadImage('assets/home/goblin-02.png') ]); this.sprites = [sprite1, sprite2]; this.animation = Animation.createAnimation(this.sprites, 500); // 500ms per frame } catch (error) { console.error('Failed to load player sprites:', error); } 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; } } } }, // Add item to inventory addToInventory(item) { this.inventory.push(item); }, // Check for and collect items checkForItems() { const item = Items.getItem(this.position.q, this.position.r); if (item) { Items.removeItem(this.position.q, this.position.r); this.addToInventory(item); } }, // 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; // Check for items when reaching new position this.checkForItems(); 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; if (this.animation && this.sprites.length > 0) { // Get current sprite from animation const currentSprite = this.animation.update(performance.now()); // Scale sprite to fit within hex // Use slightly smaller than hex size to ensure it fits visually const hexInnerSize = HEX_SIZE * 0.8; // 80% of hex size const { width, height, scale } = Animation.scaleToFit( currentSprite, hexInnerSize * 2, // width hexInnerSize * Math.sqrt(3) // height (hex height) ); // Calculate position to center the sprite in the hex const spriteX = screenX - width / 2; const spriteY = screenY - height / 2; // Save context state ctx.save(); // Optional: add a small bounce effect when moving if (this.target) { const bounce = Math.sin(performance.now() / 100) * 2; ctx.translate(spriteX, spriteY + bounce); } else { ctx.translate(spriteX, spriteY); } // Draw the sprite ctx.drawImage( currentSprite, 0, 0, width, height ); // Restore context state ctx.restore(); // Debug: draw hex bounds if debug is enabled if (Debug.isEnabled) { ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; ctx.beginPath(); HexGrid.drawHexPath(ctx, screenX, screenY, HEX_SIZE * 0.8); ctx.stroke(); } } else { // Fallback to circle if sprites aren't loaded ctx.fillStyle = Config.colors.PLAYER; ctx.beginPath(); ctx.arc(screenX, screenY, HEX_SIZE * Config.player.SIZE_RATIO, 0, Math.PI * 2); ctx.fill(); } // Draw path if needed if (this.path.length > 0) { ctx.strokeStyle = Config.colors.PLAYER + '4D'; 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(); } } };