class IsometricGame {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
// Grid properties
this.gridSize = 10;
this.tileWidth = 50;
this.tileHeight = 25;
// Player properties
this.player = {
x: 0,
y: 0,
targetX: 0,
targetY: 0,
size: 20,
path: [], // Array to store waypoints
currentWaypoint: null,
jumpHeight: 0,
jumpProgress: 0,
isJumping: false,
startX: 0,
startY: 0
};
// Add particle system
this.particles = [];
// Handle window resize
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
this.setupEventListeners();
this.gameLoop();
}
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
// Recalculate grid offset to center it
this.offsetX = this.canvas.width / 2;
this.offsetY = this.canvas.height / 3;
// Scale tile size based on screen size
const minDimension = Math.min(this.canvas.width, this.canvas.height);
const scaleFactor = minDimension / 800; // 800 is our reference size
this.tileWidth = 50 * scaleFactor;
this.tileHeight = 25 * scaleFactor;
this.player.size = 20 * scaleFactor;
}
toIsometric(x, y) {
return {
x: (x - y) * this.tileWidth / 2,
y: (x + y) * this.tileHeight / 2
};
}
fromIsometric(screenX, screenY) {
// Convert screen coordinates back to grid coordinates
screenX -= this.offsetX;
screenY -= this.offsetY;
const x = (screenX / this.tileWidth + screenY / this.tileHeight) / 1;
const y = (screenY / this.tileHeight - screenX / this.tileWidth) / 1;
return { x: Math.round(x), y: Math.round(y) };
}
drawGrid() {
for (let x = 0; x < this.gridSize; x++) {
for (let y = 0; y < this.gridSize; y++) {
const iso = this.toIsometric(x, y);
// Draw tile
this.ctx.beginPath();
this.ctx.moveTo(iso.x + this.offsetX, iso.y + this.offsetY - this.tileHeight/2);
this.ctx.lineTo(iso.x + this.offsetX + this.tileWidth/2, iso.y + this.offsetY);
this.ctx.lineTo(iso.x + this.offsetX, iso.y + this.offsetY + this.tileHeight/2);
this.ctx.lineTo(iso.x + this.offsetX - this.tileWidth/2, iso.y + this.offsetY);
this.ctx.closePath();
this.ctx.strokeStyle = '#666';
this.ctx.stroke();
this.ctx.fillStyle = '#fff';
this.ctx.fill();
}
}
}
drawPlayer() {
// Convert player grid position to isometric coordinates
const iso = this.toIsometric(this.player.x, this.player.y);
// Apply jump height offset
const jumpOffset = this.player.jumpHeight || 0;
// Calculate squash and stretch based on jump progress
let squashStretch = 1;
if (this.player.isJumping) {
// Stretch at the start and middle of jump, squash at landing
const jumpPhase = Math.sin(this.player.jumpProgress * Math.PI);
if (this.player.jumpProgress < 0.2) {
// Initial stretch when jumping
squashStretch = 1 + (0.3 * (1 - this.player.jumpProgress / 0.2));
} else if (this.player.jumpProgress > 0.8) {
// Squash when landing
squashStretch = 1 - (0.3 * ((this.player.jumpProgress - 0.8) / 0.2));
} else {
// Slight stretch at peak of jump
squashStretch = 1 + (0.1 * jumpPhase);
}
}
// Draw player shadow (gets smaller when jumping)
const shadowScale = Math.max(0.2, 1 - (jumpOffset / this.tileHeight));
this.ctx.beginPath();
this.ctx.ellipse(
iso.x + this.offsetX,
iso.y + this.offsetY + 2,
this.player.size * 0.8 * shadowScale,
this.player.size * 0.3 * shadowScale,
0,
0,
Math.PI * 2
);
this.ctx.fillStyle = `rgba(0,0,0,${0.2 * shadowScale})`;
this.ctx.fill();
// Draw player body with jump offset and squash/stretch
this.ctx.beginPath();
this.ctx.fillStyle = '#ff4444';
this.ctx.strokeStyle = '#aa0000';
const bodyHeight = this.player.size * 2 * squashStretch;
const bodyWidth = this.player.size * 0.8 * (1 / squashStretch); // Inverse stretch for width
this.ctx.save();
this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY - jumpOffset);
this.ctx.scale(1, 0.5); // Apply isometric perspective
this.ctx.fillRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight);
this.ctx.strokeRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight);
this.ctx.restore();
// Draw player head with jump offset and squash/stretch
this.ctx.beginPath();
this.ctx.ellipse(
iso.x + this.offsetX,
iso.y + this.offsetY - this.player.size * squashStretch - jumpOffset,
this.player.size * (1 / squashStretch),
this.player.size * 0.5 * squashStretch,
0,
0,
Math.PI * 2
);
this.ctx.fillStyle = '#ff4444';
this.ctx.fill();
this.ctx.strokeStyle = '#aa0000';
this.ctx.stroke();
}
findPath(startX, startY, endX, endY) {
// Simple pathfinding that follows grid edges
const path = [];
// First move along X axis
if (startX !== endX) {
const stepX = startX < endX ? 1 : -1;
for (let x = startX + stepX; stepX > 0 ? x <= endX : x >= endX; x += stepX) {
path.push({ x: x, y: startY });
}
}
// Then move along Y axis
if (startY !== endY) {
const stepY = startY < endY ? 1 : -1;
for (let y = startY + stepY; stepY > 0 ? y <= endY : y >= endY; y += stepY) {
path.push({ x: endX, y: y });
}
}
return path;
}
updatePlayer() {
const jumpDuration = 0.1; // Faster jump for snappier movement
const maxJumpHeight = this.tileHeight;
// If we don't have a current waypoint but have a path, get next waypoint
if (!this.player.currentWaypoint && this.player.path.length > 0) {
this.player.currentWaypoint = this.player.path.shift();
this.player.isJumping = true;
this.player.jumpProgress = 0;
// Store starting position for interpolation
this.player.startX = this.player.x;
this.player.startY = this.player.y;
}
// Move towards current waypoint
if (this.player.currentWaypoint) {
// Update jump animation
if (this.player.isJumping) {
this.player.jumpProgress += jumpDuration;
// Clamp progress to 1
if (this.player.jumpProgress > 1) this.player.jumpProgress = 1;
// Parabolic jump arc
this.player.jumpHeight = Math.sin(this.player.jumpProgress * Math.PI) * maxJumpHeight;
// Precise interpolation between points
this.player.x = this.player.startX + (this.player.currentWaypoint.x - this.player.startX) * this.player.jumpProgress;
this.player.y = this.player.startY + (this.player.currentWaypoint.y - this.player.startY) * this.player.jumpProgress;
// Landing
if (this.player.jumpProgress >= 1) {
this.player.isJumping = false;
this.player.jumpHeight = 0;
this.player.x = this.player.currentWaypoint.x;
this.player.y = this.player.currentWaypoint.y;
this.createDustParticles(this.player.x, this.player.y);
this.player.currentWaypoint = null;
}
}
}
}
setupEventListeners() {
this.canvas.addEventListener('click', (e) => {
const rect = this.canvas.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const gridPos = this.fromIsometric(clickX, clickY);
// Only move if within grid bounds
if (gridPos.x >= 0 && gridPos.x < this.gridSize &&
gridPos.y >= 0 && gridPos.y < this.gridSize) {
// Set target and calculate path
this.player.targetX = Math.round(gridPos.x);
this.player.targetY = Math.round(gridPos.y);
// Calculate new path
this.player.path = this.findPath(
Math.round(this.player.x),
Math.round(this.player.y),
this.player.targetX,
this.player.targetY
);
// Clear current waypoint to start new path
this.player.currentWaypoint = null;
}
});
}
gameLoop() {
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw game elements
this.drawGrid();
this.updateParticles();
this.drawParticles();
this.updatePlayer();
this.drawPlayer();
// Continue game loop
requestAnimationFrame(() => this.gameLoop());
}
// Add new particle system methods
createDustParticles(x, y) {
const particleCount = 12; // Increased for more particles
for (let i = 0; i < particleCount; i++) {
// Randomize the angle slightly
const baseAngle = (Math.PI * 2 * i) / particleCount;
const randomAngle = baseAngle + (Math.random() - 0.5) * 0.5;
// Random speed and size variations
const speed = 0.3 + Math.random() * 0.4;
const initialSize = (this.player.size * 0.15) + (Math.random() * this.player.size * 0.15);
// Random grey color
const greyValue = 220 + Math.floor(Math.random() * 35);
const color = `rgb(${greyValue}, ${greyValue}, ${greyValue})`;
this.particles.push({
x: x,
y: y,
dx: Math.cos(randomAngle) * speed,
dy: Math.sin(randomAngle) * speed,
life: 0.8 + Math.random() * 0.4, // Random initial life
size: initialSize,
color: color,
initialSize: initialSize,
rotationSpeed: (Math.random() - 0.5) * 0.2,
rotation: Math.random() * Math.PI * 2
});
}
}
updateParticles() {
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
// Update position with slight gravity effect
particle.x += particle.dx;
particle.y += particle.dy;
particle.dy += 0.01; // Slight upward drift
// Update rotation
particle.rotation += particle.rotationSpeed;
// Non-linear fade out
particle.life -= 0.03;
particle.size = particle.initialSize * (particle.life * 1.5); // Grow slightly as they fade
// Remove dead particles
if (particle.life <= 0) {
this.particles.splice(i, 1);
}
}
}
drawParticles() {
for (const particle of this.particles) {
const iso = this.toIsometric(particle.x, particle.y);
this.ctx.save();
this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY);
this.ctx.rotate(particle.rotation);
// Draw a slightly irregular dust puff
this.ctx.beginPath();
const points = 5;
for (let i = 0; i < points * 2; i++) {
const angle = (i * Math.PI) / points;
const radius = particle.size * (i % 2 ? 0.7 : 1);
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
if (i === 0) this.ctx.moveTo(x, y);
else this.ctx.lineTo(x, y);
}
this.ctx.closePath();
this.ctx.fillStyle = `rgba(${particle.color.slice(4, -1)}, ${particle.life * 0.5})`;
this.ctx.fill();
this.ctx.restore();
}
}
}
// Start the game when the page loads
window.onload = () => {
new IsometricGame();
};