function createGame() { const state = { canvas: document.getElementById('gameCanvas'), ctx: null, gridSize: 10, tileWidth: 50, tileHeight: 25, offsetX: 0, offsetY: 0, particles: [], lastFrameTime: 0, player: { x: 0, y: 0, targetX: 0, targetY: 0, size: 20, path: [], currentWaypoint: null, jumpHeight: 0, jumpProgress: 0, isJumping: false, startX: 0, startY: 0 }, isHopping: false, hopProgress: 0 }; state.ctx = state.canvas.getContext('2d'); function toIsometric(x, y) { return { x: (x - y) * state.tileWidth / 2, y: (x + y) * state.tileHeight / 2 }; } function fromIsometric(screenX, screenY) { const adjustedX = screenX - state.offsetX; const adjustedY = screenY - state.offsetY; const x = (adjustedX / state.tileWidth + adjustedY / state.tileHeight) / 1; const y = (adjustedY / state.tileHeight - adjustedX / state.tileWidth) / 1; return { x: Math.round(x), y: Math.round(y) }; } function resizeCanvas() { state.canvas.width = window.innerWidth; state.canvas.height = window.innerHeight; state.offsetX = state.canvas.width / 2; state.offsetY = state.canvas.height / 3; const minDimension = Math.min(state.canvas.width, state.canvas.height); const scaleFactor = minDimension / 800; state.tileWidth = 50 * scaleFactor; state.tileHeight = 25 * scaleFactor; state.player.size = 20 * scaleFactor; } function dustyParticles(x, y) { const particleCount = 12; for (let i = 0; i < particleCount; i++) { const baseAngle = (Math.PI * 2 * i) / particleCount; const randomAngle = baseAngle + (Math.random() - 0.5) * 0.5; const speed = 0.3 + Math.random() * 0.4; const initialSize = (state.player.size * 0.15) + (Math.random() * state.player.size * 0.15); const greyValue = 220 + Math.floor(Math.random() * 35); state.particles.push({ x, y, dx: Math.cos(randomAngle) * speed, dy: Math.sin(randomAngle) * speed, life: 0.8 + Math.random() * 0.4, size: initialSize, color: `rgb(${greyValue}, ${greyValue}, ${greyValue})`, initialSize, rotationSpeed: (Math.random() - 0.5) * 0.2, rotation: Math.random() * Math.PI * 2 }); } } function updateParticles() { for (let i = state.particles.length - 1; i >= 0; i--) { const particle = state.particles[i]; particle.x += particle.dx; particle.y += particle.dy; particle.dy += 0.01; particle.rotation += particle.rotationSpeed; particle.life -= 0.03; particle.size = particle.initialSize * (particle.life * 1.5); if (particle.life <= 0) { state.particles.splice(i, 1); } } } function findPath(startX, startY, endX, endY) { const path = []; 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, y: startY }); } } 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 }); } } return path; } function updatePlayer() { const jumpDuration = 0.1; const maxJumpHeight = state.tileHeight; if (state.isHopping) { state.hopProgress += jumpDuration; state.hopProgress = Math.min(state.hopProgress, 1); state.player.jumpHeight = Math.sin(state.hopProgress * Math.PI) * maxJumpHeight; if (state.hopProgress >= 1) { state.isHopping = false; state.player.jumpHeight = 0; } } else { if (!state.player.currentWaypoint && state.player.path.length > 0) { state.player.currentWaypoint = state.player.path.shift(); state.player.isJumping = true; state.player.jumpProgress = 0; state.player.startX = state.player.x; state.player.startY = state.player.y; } if (state.player.currentWaypoint && state.player.isJumping) { state.player.jumpProgress += jumpDuration; state.player.jumpProgress = Math.min(state.player.jumpProgress, 1); state.player.jumpHeight = Math.sin(state.player.jumpProgress * Math.PI) * maxJumpHeight; state.player.x = state.player.startX + (state.player.currentWaypoint.x - state.player.startX) * state.player.jumpProgress; state.player.y = state.player.startY + (state.player.currentWaypoint.y - state.player.startY) * state.player.jumpProgress; if (state.player.jumpProgress >= 1) { state.player.isJumping = false; state.player.jumpHeight = 0; state.player.x = state.player.currentWaypoint.x; state.player.y = state.player.currentWaypoint.y; dustyParticles(state.player.x, state.player.y); state.player.currentWaypoint = null; } } } } function drawGrid() { for (let x = 0; x < state.gridSize; x++) { for (let y = 0; y < state.gridSize; y++) { const iso = toIsometric(x, y); // Diamonds! state.ctx.beginPath(); // Start a new path state.ctx.moveTo(iso.x + state.offsetX, iso.y + state.offsetY - state.tileHeight/2); // Move to the top point of the diamond state.ctx.lineTo(iso.x + state.offsetX + state.tileWidth/2, iso.y + state.offsetY); // Draw line to the right point of the diamond state.ctx.lineTo(iso.x + state.offsetX, iso.y + state.offsetY + state.tileHeight/2); // Draw line to the bottom point of the diamond state.ctx.lineTo(iso.x + state.offsetX - state.tileWidth/2, iso.y + state.offsetY); // Draw line to the left point of the diamond state.ctx.closePath(); // Close the path to complete the diamond state.ctx.strokeStyle = '#666'; state.ctx.stroke(); state.ctx.fillStyle = '#fff'; state.ctx.fill(); } } } function drawParticles() { state.particles.forEach(particle => { const iso = toIsometric(particle.x, particle.y); state.ctx.save(); state.ctx.translate(iso.x + state.offsetX, iso.y + state.offsetY); state.ctx.rotate(particle.rotation); state.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; i === 0 ? state.ctx.moveTo(x, y) : state.ctx.lineTo(x, y); } state.ctx.closePath(); state.ctx.fillStyle = `rgba(${particle.color.slice(4, -1)}, ${particle.life * 0.5})`; state.ctx.fill(); state.ctx.restore(); }); } function drawPlayer() { const iso = toIsometric(state.player.x, state.player.y); const jumpOffset = state.player.jumpHeight || state.player.jumpHeight; let squashStretch = 1; if (state.player.isJumping) { const jumpPhase = Math.sin(state.player.jumpProgress * Math.PI); if (state.player.jumpProgress < 0.2) { squashStretch = 1 + (0.3 * (1 - state.player.jumpProgress / 0.2)); } else if (state.player.jumpProgress > 0.8) { squashStretch = 1 - (0.3 * ((state.player.jumpProgress - 0.8) / 0.2)); } else { squashStretch = 1 + (0.1 * jumpPhase); } } const shadowScale = Math.max(0.2, 1 - (jumpOffset / state.tileHeight)); state.ctx.beginPath(); state.ctx.ellipse( iso.x + state.offsetX, iso.y + state.offsetY + 2, state.player.size * 0.8 * shadowScale, state.player.size * 0.3 * shadowScale, 0, 0, Math.PI * 2 ); state.ctx.fillStyle = `rgba(0,0,0,${0.2 * shadowScale})`; state.ctx.fill(); const bodyHeight = state.player.size * 2 * squashStretch; const bodyWidth = state.player.size * 0.8 * (1 / squashStretch); state.ctx.save(); state.ctx.translate(iso.x + state.offsetX, iso.y + state.offsetY - jumpOffset); state.ctx.scale(1, 0.5); state.ctx.fillStyle = '#ff4444'; state.ctx.strokeStyle = '#aa0000'; state.ctx.fillRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight); state.ctx.strokeRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight); state.ctx.restore(); state.ctx.beginPath(); state.ctx.ellipse( iso.x + state.offsetX, iso.y + state.offsetY - state.player.size * squashStretch - jumpOffset, state.player.size * (1 / squashStretch), state.player.size * 0.5 * squashStretch, 0, 0, Math.PI * 2 ); state.ctx.fillStyle = '#ff4444'; state.ctx.fill(); state.ctx.strokeStyle = '#aa0000'; state.ctx.stroke(); } function gameLoop(timestamp) { const frameInterval = 1000 / 60; if (!state.lastFrameTime || timestamp - state.lastFrameTime >= frameInterval) { state.ctx.clearRect(0, 0, state.canvas.width, state.canvas.height); drawGrid(); updateParticles(); drawParticles(); updatePlayer(); drawPlayer(); state.lastFrameTime = timestamp; } requestAnimationFrame(gameLoop); } function handleClick(e) { const rect = state.canvas.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; const gridPos = fromIsometric(clickX, clickY); const iso = toIsometric(state.player.x, state.player.y); const playerRadius = state.player.size; const distanceToPlayer = Math.sqrt( Math.pow(clickX - (iso.x + state.offsetX), 2) + Math.pow(clickY - (iso.y + state.offsetY), 2) ); if (distanceToPlayer < playerRadius) { state.isHopping = true; state.hopProgress = 0; } else if (gridPos.x >= 0 && gridPos.x < state.gridSize && gridPos.y >= 0 && gridPos.y < state.gridSize) { state.player.targetX = Math.round(gridPos.x); state.player.targetY = Math.round(gridPos.y); state.player.path = findPath( Math.round(state.player.x), Math.round(state.player.y), state.player.targetX, state.player.targetY ); state.player.currentWaypoint = null; } } function init() { resizeCanvas(); window.addEventListener('resize', resizeCanvas); state.canvas.addEventListener('click', handleClick); state.lastFrameTime = 0; gameLoop(); } return { init }; } window.onload = () => { const game = createGame(); game.init(); };