diff options
author | elioat <elioat@tilde.institute> | 2024-12-06 21:20:33 -0500 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-12-06 21:20:33 -0500 |
commit | 734a8083d8c313d5a30d0247fe34fc6646e09612 (patch) | |
tree | c5cf949033535aa039d21a9d3d92704414750764 | |
parent | 67203663c65c1862a213ce7b03fb7f825d46a59d (diff) | |
download | tour-734a8083d8c313d5a30d0247fe34fc6646e09612.tar.gz |
*
-rw-r--r-- | html/mountain/game.js | 400 |
1 files changed, 254 insertions, 146 deletions
diff --git a/html/mountain/game.js b/html/mountain/game.js index be75810..8243e9f 100644 --- a/html/mountain/game.js +++ b/html/mountain/game.js @@ -1,25 +1,11 @@ -// Set up canvas const canvas = document.createElement('canvas'); +const ctx = canvas.getContext('2d'); +document.body.appendChild(canvas); canvas.style.display = 'block'; canvas.style.position = 'fixed'; canvas.style.top = '0'; canvas.style.left = '0'; -document.body.appendChild(canvas); -const ctx = canvas.getContext('2d'); - -// Add CSS to body -document.body.style.margin = '0'; -document.body.style.overflow = 'hidden'; -// Make canvas fullscreen -function resizeCanvas() { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; -} -resizeCanvas(); -window.addEventListener('resize', resizeCanvas); - -// Add these at the top of the file, after the canvas setup but before the game constants const GAME_STATE = { PLAYING: 'playing', GAME_OVER: 'game_over' @@ -30,28 +16,73 @@ const PLATFORM_TYPE = { DEADLY: 'deadly' }; -// Game constants const PLAYER_SIZE = 20; -const PLATFORM_HEIGHT = 20; -const MIN_PLATFORM_WIDTH = 100; -const MAX_PLATFORM_WIDTH = 300; const GRAVITY = 0.5; const JUMP_FORCE = 12; const MOVE_SPEED = 7; -// Add these constants for level generation -const MIN_PARTITION_SIZE = MAX_PLATFORM_WIDTH + 20; // Reduced from 50 to allow more splits -const PARTITION_RATIO = 0.3; // Reduced from 0.4 to allow more uneven splits +const PLATFORM_HEIGHT = 20; +const MIN_PLATFORM_WIDTH = 100; +const MAX_PLATFORM_WIDTH = 300; +const MIN_PARTITION_SIZE = MAX_PLATFORM_WIDTH + 20; +const PARTITION_RATIO = 0.3; +const MIN_PLATFORM_SPACING = 100; +const DEADLY_BORDER_HEIGHT = 7; + +const FPS = 60; +const FRAME_TIME = 1000 / FPS; + +const PARTICLE_COUNT = 20; +const PARTICLE_SPEED = 4; +const PARTICLE_SIZE = 4; +const PARTICLE_LIFETIME = 25; + +const DEATH_PARTICLE_COUNT = 60; +const DEATH_PARTICLE_SPEED = 10; +const DEATH_PARTICLE_SIZE = 4; +const DEATH_PARTICLE_LIFETIME = 50; +const DEATH_ANIMATION_DURATION = 55; -// Add this constant for minimum platform spacing -const MIN_PLATFORM_SPACING = 100; // Minimum distance between platforms +const ENEMY_SPEED = 2; + +let gameState = GAME_STATE.PLAYING; +let level = 1; +let platforms = []; +let enemies = []; +let particles = []; +let deathParticles = []; +let deathAnimationTimer = 0; +let frameCount = 0; +let lastFpsUpdate = 0; +let currentFps = 0; +let lastFrameTime = 0; +let accumulator = 0; + +let player = { + x: PLAYER_SIZE, + y: window.innerHeight - PLAYER_SIZE * 2, + velocityX: 0, + velocityY: 0, + isJumping: false, + jumpsLeft: 2, + gravityMultiplier: 1, + isDead: false +}; + +let exit = { + x: window.innerWidth - PLAYER_SIZE * 2, + y: PLAYER_SIZE * 2, + size: PLAYER_SIZE +}; + +const keys = {}; +window.addEventListener('keydown', e => keys[e.key] = true); +window.addEventListener('keyup', e => keys[e.key] = false); -// Add these helper functions for the RSP tree function randomRange(min, max) { return min + Math.random() * (max - min); } -// Core partition data structure (plain object) function createPartition(x, y, width, height) { return { x, @@ -64,7 +95,6 @@ function createPartition(x, y, width, height) { }; } -// Split function that returns new partitions instead of mutating function splitPartition(partition, vertical) { const splitPoint = randomRange( vertical ? partition.width * PARTITION_RATIO : partition.height * PARTITION_RATIO, @@ -81,12 +111,9 @@ function splitPartition(partition, vertical) { }; } -// Replace the platformsOverlap function with this new version function platformsOverlap(platform1, platform2) { - // Add a small buffer space around platforms (5 pixels) const buffer = 5; - // Check if the rectangles (with buffer) overlap return !( platform1.x + platform1.width + buffer < platform2.x || platform1.x > platform2.x + platform2.width + buffer || @@ -95,7 +122,91 @@ function platformsOverlap(platform1, platform2) { ); } -// Modify the generatePlatformsForPartition function +function createParticle(x, y, velocityY) { + return { + x: x + PLAYER_SIZE / 2 + (Math.random() - 0.5) * PLAYER_SIZE, + y: y + (velocityY > 0 ? 0 : PLAYER_SIZE), + velocityX: (Math.random() - 0.5) * PARTICLE_SPEED * 2, + velocityY: (Math.random() * PARTICLE_SPEED) * Math.sign(velocityY), + size: PARTICLE_SIZE + Math.random() * 2, + life: PARTICLE_LIFETIME, + initialOpacity: 0.3 + Math.random() * 0.7 + }; +} + +function createDeathParticles(x, y) { + deathParticles = []; + for (let i = 0; i < DEATH_PARTICLE_COUNT; i++) { + const angle = (Math.PI * 2 * i) / DEATH_PARTICLE_COUNT; + deathParticles.push({ + x: x + PLAYER_SIZE / 2, + y: y + PLAYER_SIZE / 2, + velocityX: Math.cos(angle) * DEATH_PARTICLE_SPEED * (0.5 + Math.random()), + velocityY: Math.sin(angle) * DEATH_PARTICLE_SPEED * (0.5 + Math.random()), + size: DEATH_PARTICLE_SIZE + Math.random() * 2, + life: DEATH_PARTICLE_LIFETIME, + initialOpacity: 0.6 + Math.random() * 0.4 + }); + } + deathAnimationTimer = DEATH_ANIMATION_DURATION; +} + +function updateParticles() { + particles = particles.filter(particle => { + particle.x += particle.velocityX; + particle.y += particle.velocityY; + particle.life--; + return particle.life > 0; + }); +} + +function updateDeathParticles() { + if (deathAnimationTimer > 0) { + deathAnimationTimer--; + } + + deathParticles = deathParticles.filter(particle => { + particle.x += particle.velocityX; + particle.y += particle.velocityY; + particle.velocityY += GRAVITY * 0.5; // GRAVITY! Is working against me... + particle.life--; + return particle.life > 0; + }); +} + +function createEnemy(platform) { + const startsOnTop = Math.random() < 0.5; // 50% chance to start on top + return { + x: platform.x, + y: startsOnTop ? + platform.y - PLAYER_SIZE : + platform.y + platform.height, + width: PLAYER_SIZE, + height: PLAYER_SIZE, + platform: platform, + direction: 1, + moveRight: true, + isOnTop: startsOnTop // Track where things started + }; +} + +function updateEnemies() { + enemies.forEach(enemy => { + enemy.x += ENEMY_SPEED * (enemy.moveRight ? 1 : -1); + + if (enemy.moveRight && enemy.x + enemy.width > enemy.platform.x + enemy.platform.width) { + enemy.moveRight = false; + } else if (!enemy.moveRight && enemy.x < enemy.platform.x) { + enemy.moveRight = true; + } + + enemy.y = (player.gravityMultiplier > 0) === enemy.isOnTop ? + enemy.platform.y - enemy.height : + enemy.platform.y + enemy.platform.height; + }); +} + +// 9. Level Generation function generatePlatformsForPartition(partition) { if (partition.width < MIN_PLATFORM_WIDTH || partition.height < PLATFORM_HEIGHT * 2) { return []; @@ -106,7 +217,7 @@ function generatePlatformsForPartition(partition) { : 1; const newPlatforms = []; - const maxAttempts = 20; // Increased max attempts for better placement + const maxAttempts = 20; for (let i = 0; i < platformCount; i++) { let validPlatform = null; @@ -118,7 +229,6 @@ function generatePlatformsForPartition(partition) { Math.min(MAX_PLATFORM_WIDTH, partition.width * 0.8) ); - // Calculate valid range for platform placement const minX = Math.max( partition.x, partition.x + (partition.width * (i / platformCount)) @@ -128,7 +238,6 @@ function generatePlatformsForPartition(partition) { partition.x + (partition.width * ((i + 1) / platformCount)) ); - // Only proceed if we have valid space if (maxX > minX) { const candidatePlatform = { x: randomRange(minX, maxX), @@ -138,13 +247,11 @@ function generatePlatformsForPartition(partition) { ), width: platformWidth, height: PLATFORM_HEIGHT, - // 20% chance for a platform to be deadly after level 1 type: (level > 1 && Math.random() < 0.2) ? PLATFORM_TYPE.DEADLY : PLATFORM_TYPE.NORMAL }; - // Check overlap with all existing platforms let overlapping = false; for (const existingPlatform of [...platforms, ...newPlatforms]) { if (platformsOverlap(candidatePlatform, existingPlatform)) { @@ -166,49 +273,19 @@ function generatePlatformsForPartition(partition) { } } - return newPlatforms; -} - -// Game state -let gameState = GAME_STATE.PLAYING; -let level = 1; -let platforms = []; -let player = { - x: PLAYER_SIZE, - y: window.innerHeight - PLAYER_SIZE * 2, - velocityX: 0, - velocityY: 0, - isJumping: false, - jumpsLeft: 2, - gravityMultiplier: 1, - isDead: false -}; -let exit = { - x: window.innerWidth - PLAYER_SIZE * 2, - y: PLAYER_SIZE * 2, - size: PLAYER_SIZE -}; + newPlatforms.forEach(platform => { + if (platform.type === PLATFORM_TYPE.NORMAL && Math.random() < 0.2) { // 20% chance + enemies.push(createEnemy(platform)); + } + }); -// Add reset player function -function resetPlayer() { - player.x = PLAYER_SIZE; - player.y = window.innerHeight - PLAYER_SIZE * 2; - player.velocityX = 0; - player.velocityY = 0; - player.isJumping = false; - player.jumpsLeft = 2; - player.gravityMultiplier = 1; - player.isDead = false; - gameState = GAME_STATE.PLAYING; - level = 1; - generateLevel(); + return newPlatforms; } -// Restore the original generateLevel function function generateLevel() { platforms = []; + enemies = []; - // Add starting platform platforms.push({ x: 0, y: window.innerHeight - PLATFORM_HEIGHT, @@ -217,7 +294,6 @@ function generateLevel() { type: PLATFORM_TYPE.NORMAL }); - // Add end platform platforms.push({ x: window.innerWidth - MIN_PLATFORM_WIDTH, y: PLAYER_SIZE * 3, @@ -250,7 +326,6 @@ function generateLevel() { return generatePlatformsForPartition(node); } - // Create sections and generate platforms for (let i = 0; i < horizontalSections; i++) { for (let j = 0; j < verticalSections; j++) { const root = createPartition( @@ -266,47 +341,64 @@ function generateLevel() { } } -// Handle player input -const keys = {}; -window.addEventListener('keydown', e => keys[e.key] = true); -window.addEventListener('keyup', e => keys[e.key] = false); +function resetPlayer() { + player.x = PLAYER_SIZE; + player.y = window.innerHeight - PLATFORM_HEIGHT - PLAYER_SIZE; + player.velocityX = 0; + player.velocityY = 0; + player.isJumping = false; + player.jumpsLeft = 2; + player.gravityMultiplier = 1; + player.isDead = false; + gameState = GAME_STATE.PLAYING; + level = 1; + generateLevel(); + enemies = []; +} + +function killPlayer() { + if (!player.isDead) { + createDeathParticles(player.x, player.y); + player.isDead = true; + gameState = GAME_STATE.GAME_OVER; + } +} function updatePlayer() { if (gameState === GAME_STATE.GAME_OVER) { - if (keys[' ']) { // Space key to restart when game over + if (deathAnimationTimer <= 0 && keys['Enter']) { resetPlayer(); } return; } - // Horizontal movement if (keys['ArrowLeft']) player.velocityX = -MOVE_SPEED; else if (keys['ArrowRight']) player.velocityX = MOVE_SPEED; else player.velocityX = 0; - // Toggle gravity when 'g' or spacebar is pressed if ((keys['g'] || keys[' ']) && !player.lastGravityKey) { player.gravityMultiplier *= -1; - player.velocityY = 0; // Reset vertical velocity when flipping gravity + player.velocityY = 0; } player.lastGravityKey = keys['g'] || keys[' ']; - // Apply gravity (now with direction) player.velocityY += GRAVITY * player.gravityMultiplier; - // Jump (in the direction opposite to gravity) if (keys['ArrowUp'] && keys['ArrowUp'] !== player.lastJumpKey && player.jumpsLeft > 0) { player.velocityY = -JUMP_FORCE * player.gravityMultiplier; player.jumpsLeft--; player.isJumping = true; + + // Add particles whenever you jump + for (let i = 0; i < PARTICLE_COUNT; i++) { + particles.push(createParticle(player.x, player.y, player.velocityY)); + } } player.lastJumpKey = keys['ArrowUp']; - // Update position player.x += player.velocityX; player.y += player.velocityY; - // Check platform collisions (modified for deadly platforms) player.isJumping = true; for (let platform of platforms) { if (player.x + PLAYER_SIZE > platform.x && @@ -315,14 +407,12 @@ function updatePlayer() { let collision = false; if (player.gravityMultiplier > 0) { - // Normal gravity collision if (player.y + PLAYER_SIZE > platform.y && player.y + PLAYER_SIZE < platform.y + platform.height + player.velocityY) { collision = true; player.y = platform.y - PLAYER_SIZE; } } else { - // Reversed gravity collision if (player.y < platform.y + platform.height && player.y > platform.y + player.velocityY) { collision = true; @@ -332,8 +422,7 @@ function updatePlayer() { if (collision) { if (platform.type === PLATFORM_TYPE.DEADLY) { - player.isDead = true; - gameState = GAME_STATE.GAME_OVER; + killPlayer(); } else { player.velocityY = 0; player.isJumping = false; @@ -343,19 +432,14 @@ function updatePlayer() { } } - // Check for deadly border collisions (modified for gap) if (player.y <= DEADLY_BORDER_HEIGHT) { - // Only die if touching the actual drawn deadly border if (player.x < canvas.width - exit.size - 300) { - player.isDead = true; - gameState = GAME_STATE.GAME_OVER; + killPlayer(); } } else if (player.y + PLAYER_SIZE >= canvas.height - DEADLY_BORDER_HEIGHT) { - player.isDead = true; - gameState = GAME_STATE.GAME_OVER; + killPlayer(); } - // Keep player in bounds (modified to account for deadly borders) if (player.x < 0) player.x = 0; if (player.x + PLAYER_SIZE > canvas.width) player.x = canvas.width - PLAYER_SIZE; if (player.y < 0) { @@ -367,66 +451,90 @@ function updatePlayer() { player.velocityY = 0; } - // Check if player reached exit if (player.x + PLAYER_SIZE > exit.x && player.x < exit.x + exit.size && player.y + PLAYER_SIZE > exit.y && player.y < exit.y + exit.size) { level++; - player.x = PLAYER_SIZE; - player.y = window.innerHeight - PLAYER_SIZE; generateLevel(); + player.x = PLAYER_SIZE; + player.y = window.innerHeight - PLATFORM_HEIGHT - PLAYER_SIZE; + player.velocityY = 0; + player.velocityX = 0; + player.jumpsLeft = 2; + player.isJumping = false; } -} -// Add these variables at the top with other game state -let frameCount = 0; -let lastFpsUpdate = 0; -let currentFps = 0; + enemies.forEach(enemy => { + if (player.x < enemy.x + enemy.width && + player.x + PLAYER_SIZE > enemy.x && + player.y < enemy.y + enemy.height && + player.y + PLAYER_SIZE > enemy.y) { + killPlayer(); + } + }); +} -// Add this constant at the top with other constants -const DEADLY_BORDER_HEIGHT = 7; +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} -// Update the draw function to include the deadly borders function draw(currentTime) { - // Clear canvas with light grey ctx.fillStyle = '#E0E0E0'; ctx.fillRect(0, 0, canvas.width, canvas.height); - // Draw deadly borders with gap for exit - ctx.fillStyle = '#FF0000'; - // Top border (with gap) - ctx.fillRect(0, 0, canvas.width - exit.size - 300, DEADLY_BORDER_HEIGHT); // Left section stops before exit + ctx.fillStyle = 'tomato'; + ctx.fillRect(0, 0, canvas.width - exit.size - 300, DEADLY_BORDER_HEIGHT); - // Bottom border (full width) - ctx.fillRect(0, canvas.height - DEADLY_BORDER_HEIGHT, canvas.width, DEADLY_BORDER_HEIGHT); + ctx.fillRect(0, canvas.height - DEADLY_BORDER_HEIGHT, platforms[0].x, DEADLY_BORDER_HEIGHT); + ctx.fillRect( + platforms[0].x + platforms[0].width, + canvas.height - DEADLY_BORDER_HEIGHT, + canvas.width - (platforms[0].x + platforms[0].width), + DEADLY_BORDER_HEIGHT + ); - // Draw platforms (almost black for normal platforms, red for deadly) for (let platform of platforms) { ctx.fillStyle = platform.type === PLATFORM_TYPE.DEADLY ? - '#FF0000' : // Red for deadly platforms - '#1A1A1A'; // Almost black for normal platforms + 'tomato' : + '#1A1A1A'; ctx.fillRect(platform.x, platform.y, platform.width, platform.height); } - // Draw exit (keeping green for now) - ctx.fillStyle = '#32CD32'; + ctx.fillStyle = 'teal'; ctx.fillRect(exit.x, exit.y, exit.size, exit.size); - // Draw player in almost black if (!player.isDead) { - ctx.fillStyle = '#1A1A1A'; + ctx.fillStyle = player.gravityMultiplier > 0 ? '#1A1A1A' : '#4A90E2'; ctx.fillRect(player.x, player.y, PLAYER_SIZE, PLAYER_SIZE); } - // Draw level number only ctx.fillStyle = '#000000'; ctx.font = '20px Arial'; ctx.textAlign = 'left'; ctx.fillText(`Level: ${level}`, 10, 30); - // Draw game over screen - if (gameState === GAME_STATE.GAME_OVER) { + // Draw particles with different opacities + particles.forEach(particle => { + const alpha = (particle.life / PARTICLE_LIFETIME) * particle.initialOpacity; + ctx.fillStyle = `rgba(26, 26, 26, ${alpha})`; + ctx.fillRect(particle.x, particle.y, particle.size, particle.size); + }); + + enemies.forEach(enemy => { + ctx.fillStyle = 'tomato'; + ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height); + }); + + // Death particles! + deathParticles.forEach(particle => { + const alpha = (particle.life / DEATH_PARTICLE_LIFETIME) * particle.initialOpacity; + ctx.fillStyle = `rgba(26, 26, 26, ${alpha})`; + ctx.fillRect(particle.x, particle.y, particle.size, particle.size); + }); + + if (gameState === GAME_STATE.GAME_OVER && deathAnimationTimer <= 0) { ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(0, 0, canvas.width, canvas.height); @@ -436,19 +544,21 @@ function draw(currentTime) { ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2); ctx.font = '24px Arial'; - ctx.fillText('Press SPACE to restart', canvas.width / 2, canvas.height / 2 + 40); + ctx.fillText('Press ENTER to restart', canvas.width / 2, canvas.height / 2 + 40); - ctx.textAlign = 'left'; // Reset text align + ctx.textAlign = 'left'; } -} -// Add these constants at the top with other game constants -const FPS = 60; -const FRAME_TIME = 1000 / FPS; - -// Replace the simple game loop with this new version -let lastFrameTime = 0; -let accumulator = 0; + // Show help text on the first level + if (level === 1 && gameState === GAME_STATE.PLAYING) { + ctx.fillStyle = '#1A1A1A'; + ctx.font = '24px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('Arrow keys move the player.', canvas.width / 2, canvas.height / 2 - 20); + ctx.fillText('Space bar reverses gravity.', canvas.width / 2, canvas.height / 2 + 20); + ctx.textAlign = 'left'; // Reset text alignment for other text + } +} function gameLoop(currentTime) { if (lastFrameTime === 0) { @@ -456,28 +566,26 @@ function gameLoop(currentTime) { lastFpsUpdate = currentTime; } - // Calculate delta time const deltaTime = currentTime - lastFrameTime; lastFrameTime = currentTime; - // Accumulate time to process accumulator += deltaTime; - // Update physics at a fixed time step while (accumulator >= FRAME_TIME) { updatePlayer(); + updateEnemies(); + updateParticles(); + updateDeathParticles(); accumulator -= FRAME_TIME; } - // Render at whatever frame rate the browser can handle draw(currentTime); requestAnimationFrame(gameLoop); } -// Initialize the first level before starting the game loop +document.body.style.margin = '0'; +document.body.style.overflow = 'hidden'; +resizeCanvas(); +window.addEventListener('resize', resizeCanvas); generateLevel(); - -// Start the game loop -lastFrameTime = 0; -accumulator = 0; requestAnimationFrame(gameLoop); |