// 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();
}
}
};