diff options
author | elioat <elioat@tilde.institute> | 2024-12-29 06:19:41 -0500 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-12-29 06:19:41 -0500 |
commit | 326e29526606bf7a3d7623705e3baabf5302a071 (patch) | |
tree | bffedeef80abafa59fbbe4f43176cb65a80e90b6 | |
parent | 1efe21139c8f76541e37918de31e80fcb33e6e07 (diff) | |
download | tour-326e29526606bf7a3d7623705e3baabf5302a071.tar.gz |
*
-rw-r--r-- | html/voxels/index.html | 2 | ||||
-rw-r--r-- | html/voxels/js/game.js | 523 |
2 files changed, 233 insertions, 292 deletions
diff --git a/html/voxels/index.html b/html/voxels/index.html index fda7eba..570f247 100644 --- a/html/voxels/index.html +++ b/html/voxels/index.html @@ -1,7 +1,7 @@ <!DOCTYPE html> <html lang="en"> <head> - <title>Isometric Game</title> + <title>Bouncy Isometric Guy</title> <style> body { margin: 0; diff --git a/html/voxels/js/game.js b/html/voxels/js/game.js index 2ced6fd..fb6530d 100644 --- a/html/voxels/js/game.js +++ b/html/voxels/js/game.js @@ -1,362 +1,303 @@ -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 = { +function createGame() { + const state = { + canvas: document.getElementById('gameCanvas'), + ctx: null, + gridSize: 10, + tileWidth: 50, + tileHeight: 25, + offsetX: 0, + offsetY: 0, + particles: [], + player: { x: 0, y: 0, targetX: 0, targetY: 0, size: 20, - path: [], // Array to store waypoints + path: [], currentWaypoint: null, jumpHeight: 0, jumpProgress: 0, isJumping: false, startX: 0, startY: 0 + } + }; + + state.ctx = state.canvas.getContext('2d'); + + function toIsometric(x, y) { + return { + x: (x - y) * state.tileWidth / 2, + y: (x + y) * state.tileHeight / 2 }; - - // 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; + + function fromIsometric(screenX, screenY) { + const adjustedX = screenX - state.offsetX; + const adjustedY = screenY - state.offsetY; - // Recalculate grid offset to center it - this.offsetX = this.canvas.width / 2; - this.offsetY = this.canvas.height / 3; + const x = (adjustedX / state.tileWidth + adjustedY / state.tileHeight) / 1; + const y = (adjustedY / state.tileHeight - adjustedX / state.tileWidth) / 1; - // 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 - }; + return { x: Math.round(x), y: Math.round(y) }; } - - fromIsometric(screenX, screenY) { - // Convert screen coordinates back to grid coordinates - screenX -= this.offsetX; - screenY -= this.offsetY; + + function resizeCanvas() { + state.canvas.width = window.innerWidth; + state.canvas.height = window.innerHeight; - const x = (screenX / this.tileWidth + screenY / this.tileHeight) / 1; - const y = (screenY / this.tileHeight - screenX / this.tileWidth) / 1; + state.offsetX = state.canvas.width / 2; + state.offsetY = state.canvas.height / 3; - return { x: Math.round(x), y: Math.round(y) }; + 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; } - - 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(); - } + + 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 + }); } } - - 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); + + 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); } } - - // 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 + + function findPath(startX, startY, endX, endY) { 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 }); + path.push({ 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 }); + path.push({ x: endX, 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; - } + function updatePlayer() { + const jumpDuration = 0.1; + const maxJumpHeight = state.tileHeight; - // 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; - } - } + 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; } - } - - 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; + if (state.player.currentWaypoint && state.player.isJumping) { + state.player.jumpProgress += jumpDuration; + state.player.jumpProgress = Math.min(state.player.jumpProgress, 1); - // 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); + state.player.jumpHeight = Math.sin(state.player.jumpProgress * Math.PI) * maxJumpHeight; - // Random grey color - const greyValue = 220 + Math.floor(Math.random() * 35); - const color = `rgb(${greyValue}, ${greyValue}, ${greyValue})`; + 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; - 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 - }); + 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; + } } } - 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); + 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(); } } } - drawParticles() { - for (const particle of this.particles) { - const iso = this.toIsometric(particle.x, particle.y); + function drawParticles() { + state.particles.forEach(particle => { + const iso = toIsometric(particle.x, particle.y); - this.ctx.save(); - this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY); - this.ctx.rotate(particle.rotation); + state.ctx.save(); + state.ctx.translate(iso.x + state.offsetX, iso.y + state.offsetY); + state.ctx.rotate(particle.rotation); - // Draw a slightly irregular dust puff - this.ctx.beginPath(); + 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; - if (i === 0) this.ctx.moveTo(x, y); - else this.ctx.lineTo(x, y); + i === 0 ? state.ctx.moveTo(x, y) : state.ctx.lineTo(x, y); } - this.ctx.closePath(); + state.ctx.closePath(); - this.ctx.fillStyle = `rgba(${particle.color.slice(4, -1)}, ${particle.life * 0.5})`; - this.ctx.fill(); + state.ctx.fillStyle = `rgba(${particle.color.slice(4, -1)}, ${particle.life * 0.5})`; + state.ctx.fill(); - this.ctx.restore(); + state.ctx.restore(); + }); + } + + function drawPlayer() { + const iso = toIsometric(state.player.x, state.player.y); + const jumpOffset = state.player.jumpHeight || 0; + + 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() { + state.ctx.clearRect(0, 0, state.canvas.width, state.canvas.height); + + drawGrid(); + updateParticles(); + drawParticles(); + updatePlayer(); + drawPlayer(); + + 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); + + 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); + gameLoop(); + } + + return { init }; } -// Start the game when the page loads window.onload = () => { - new IsometricGame(); + const game = createGame(); + game.init(); }; \ No newline at end of file |