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; // 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 this.ctx.beginPath(); this.ctx.fillStyle = '#ff4444'; this.ctx.strokeStyle = '#aa0000'; const bodyHeight = this.player.size * 2; const bodyWidth = this.player.size * 0.8; this.ctx.save(); this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY - jumpOffset); this.ctx.scale(1, 0.5); 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 this.ctx.beginPath(); this.ctx.ellipse( iso.x + this.offsetX, iso.y + this.offsetY - this.player.size - jumpOffset, this.player.size, this.player.size * 0.5, 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(); };