/* ================================ 45 There's something quieter than sleep Within this inner room! It wears a sprig upon its breast— And will not tell its name. Some touch it, and some kiss it— Some chafe its idle hand— It has a simple gravity I do not understand! I would not weep if I were they— How rude in one to sob! Might scare the quiet fairy Back to her native wood! While simple-hearted neighbors Chat of the "Early dead"— We—prone to periphrasis Remark that Birds have fled! Emily Dickinson ================================*/ 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'; const GAME_STATE = { PLAYING: 'playing', GAME_OVER: 'game_over' // (ノಠ益ಠ)ノ }; const PLATFORM_TYPE = { NORMAL: 'normal', DEADLY: 'deadly', FALLING: 'falling' }; const PLAYER_SIZE = 20; const GRAVITY = 0.5; const JUMP_FORCE = 12; const MOVE_SPEED = 7; 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 ENEMY_SPEED = 2; const FALLING_PLATFORM_DELAY = 800; const FALLING_PLATFORM_GRAVITY = 0.5; 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; const PLATFORM_PARTICLE_COUNT = 30; const PLATFORM_PARTICLE_SPEED = 8; const PLATFORM_PARTICLE_SIZE = 4; const PLATFORM_PARTICLE_LIFETIME = 40; const HARD_MODE_TIME_LIMIT = 7; // seconds const SUPER_HARD_MODE_TIME_LIMIT = 5; 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); const COLORS = { BACKGROUND: '#E0E0E0', PLATFORM: { NORMAL: '#1A1A1A', DEADLY: 'tomato', FALLING: 'rgba(26, 26, 26, 0.5)' }, PLAYER: { NORMAL: '#1A1A1A', INVERTED: '#4A90E2' }, ENEMY: 'tomato', EXIT: 'teal', TEXT: '#1A1A1A', GAME_OVER: { OVERLAY: 'rgba(0, 0, 0, 0.7)', TEXT: 'rgba(255, 255, 255, 0.75)' }, DEADLY_BORDER: 'tomato' }; function randomRange(min, max) { return min + Math.random() * (max - min); } function createPartition(x, y, width, height) { return { x, y, width, height, platform: null, left: null, right: null }; } function splitPartition(partition, vertical) { const splitPoint = randomRange( vertical ? partition.width * PARTITION_RATIO : partition.height * PARTITION_RATIO, vertical ? partition.width * (1 - PARTITION_RATIO) : partition.height * (1 - PARTITION_RATIO) ); return { left: vertical ? createPartition(partition.x, partition.y, splitPoint, partition.height) : createPartition(partition.x, partition.y, partition.width, splitPoint), right: vertical ? createPartition(partition.x + splitPoint, partition.y, partition.width - splitPoint, partition.height) : createPartition(partition.x, partition.y + splitPoint, partition.width, partition.height - splitPoint) }; } function platformsOverlap(platform1, platform2) { const buffer = 5; return !( platform1.x + platform1.width + buffer < platform2.x || platform1.x > platform2.x + platform2.width + buffer || platform1.y + platform1.height + buffer < platform2.y || platform1.y > platform2.y + platform2.height + buffer ); } function createParticle(x, y, velocityY) { return { // Randomly places particles around the player, within the player's width x: x + PLAYER_SIZE / 2 + (Math.random() - 0.5) * PLAYER_SIZE, // Put particles to the top or bottom of the player depending on the direction // the player is moving y: y + (velocityY > 0 ? 0 : PLAYER_SIZE), // Zoot out to the left and right of the player velocityX: (Math.random() - 0.5) * PARTICLE_SPEED * 2, // Zoot up or down, matching the player's direction velocityY: (Math.random() * PARTICLE_SPEED) * Math.sign(velocityY), // Add some variety to the particle sizes 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; }); } function generatePlatformsForPartition(partition) { if (partition.width < MIN_PLATFORM_WIDTH || partition.height < PLATFORM_HEIGHT * 2) { return []; } const platformCount = partition.width > MAX_PLATFORM_WIDTH * 1.5 ? Math.floor(randomRange(1, 3)) : 1; const newPlatforms = []; const maxAttempts = 20; for (let i = 0; i < platformCount; i++) { let validPlatform = null; let attempts = 0; // Try to create a platform, but don't try too hard so that things get stuck while (!validPlatform && attempts < maxAttempts) { // Generate a random width for the platform, that is between a min and a max const platformWidth = randomRange( MIN_PLATFORM_WIDTH, Math.min(MAX_PLATFORM_WIDTH, partition.width * 0.8) ); // Generate the minimum x position, which is either the partition's start // or a position based on the platform's number being placed (i/platformCount) // This spreads the platforms out more evenly across the partition const minX = Math.max( partition.x, partition.x + (partition.width * (i / platformCount)) ); // Along the same lines, calculate a max x position that accounts for platform width const maxX = Math.min( partition.x + partition.width - platformWidth, partition.x + (partition.width * ((i + 1) / platformCount)) ); // Try to place a platform if there is a valid range if (maxX > minX) { // Create a candidate platform with a random position and properties const candidatePlatform = { x: randomRange(minX, maxX), y: randomRange( partition.y + PLATFORM_HEIGHT, partition.y + partition.height - PLATFORM_HEIGHT * 2 ), width: platformWidth, height: PLATFORM_HEIGHT, // After level 1, there is a chance that a platform will be deadly, or falling! type: (() => { if (level > 1 && Math.random() < 0.2) return PLATFORM_TYPE.DEADLY; if (level > 1 && Math.random() < 0.3) { return PLATFORM_TYPE.FALLING; } return PLATFORM_TYPE.NORMAL; })(), fallTimer: null, velocityY: 0 }; // Check if the platform overlaps with any existing platforms let overlapping = false; for (const existingPlatform of [...platforms, ...newPlatforms]) { if (platformsOverlap(candidatePlatform, existingPlatform)) { overlapping = true; break; } } // If there isn't an overlap the platform is valid! // Place it! if (!overlapping) { validPlatform = candidatePlatform; } } attempts++; } if (validPlatform) { newPlatforms.push(validPlatform); } } newPlatforms.forEach(platform => { if (platform.type === PLATFORM_TYPE.NORMAL && Math.random() < 0.2) { // 20% chance enemies.push(createEnemy(platform)); } }); return newPlatforms; } function generateLevel() { platforms = []; enemies = []; platforms.push({ x: 0, y: window.innerHeight - PLATFORM_HEIGHT, width: MIN_PLATFORM_WIDTH, height: PLATFORM_HEIGHT, type: PLATFORM_TYPE.NORMAL }); platforms.push({ x: window.innerWidth - MIN_PLATFORM_WIDTH, y: PLAYER_SIZE * 3, width: MIN_PLATFORM_WIDTH, height: PLATFORM_HEIGHT, type: PLATFORM_TYPE.NORMAL }); const horizontalSections = 3; const verticalSections = 2; const sectionWidth = window.innerWidth / horizontalSections; const sectionHeight = (window.innerHeight - PLATFORM_HEIGHT * 4) / verticalSections; function subdivide(node, depth) { if (depth === 0) { return generatePlatformsForPartition(node); } const vertical = Math.random() > 0.4; if ((vertical && node.width > MIN_PARTITION_SIZE * 1.5) || (!vertical && node.height > MIN_PARTITION_SIZE * 1.5)) { const { left, right } = splitPartition(node, vertical); return [ ...subdivide(left, depth - 1), ...subdivide(right, depth - 1) ]; } return generatePlatformsForPartition(node); } for (let i = 0; i < horizontalSections; i++) { for (let j = 0; j < verticalSections; j++) { const root = createPartition( i * sectionWidth, PLATFORM_HEIGHT * 2 + (j * sectionHeight), sectionWidth, sectionHeight ); const newPlatforms = subdivide(root, Math.min(3 + Math.floor(level / 2), 5)); platforms.push(...newPlatforms); } } levelStartTime = Date.now(); } 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 = []; levelStartTime = Date.now(); } 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 (deathAnimationTimer <= 0 && keys['Enter']) { resetPlayer(); } return; } const timeLimit = superHardMode ? SUPER_HARD_MODE_TIME_LIMIT : HARD_MODE_TIME_LIMIT; if ((hardMode || superHardMode) && Date.now() - levelStartTime > timeLimit * 1000) { killPlayer(); return; } if (keys['ArrowLeft']) player.velocityX = -MOVE_SPEED; else if (keys['ArrowRight']) player.velocityX = MOVE_SPEED; else player.velocityX = 0; if ((keys['g'] || keys[' ']) && !player.lastGravityKey) { player.gravityMultiplier *= -1; player.velocityY = 0; } player.lastGravityKey = keys['g'] || keys[' ']; player.velocityY += GRAVITY * player.gravityMultiplier; if (keys['ArrowUp'] && keys['ArrowUp'] !== player.lastJumpKey && player.jumpsLeft > 0) { player.velocityY = -JUMP_FORCE * player.gravityMultiplier; player.jumpsLeft--; player.isJumping = true; // Add particles for every jump for (let i = 0; i < PARTICLE_COUNT; i++) { particles.push(createParticle(player.x, player.y, player.velocityY)); } } player.lastJumpKey = keys['ArrowUp']; player.x += player.velocityX; player.y += player.velocityY; player.isJumping = true; for (let platform of platforms) { if (player.x + PLAYER_SIZE > platform.x && player.x < platform.x + platform.width) { let collision = false; if (player.gravityMultiplier > 0) { 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 { if (player.y < platform.y + platform.height && player.y > platform.y + player.velocityY) { collision = true; player.y = platform.y + platform.height; } } if (collision) { if (platform.type === PLATFORM_TYPE.DEADLY) { killPlayer(); } else { if (platform.type === PLATFORM_TYPE.FALLING && !platform.fallTimer) { platform.fallTimer = setTimeout(() => { platform.isFalling = true; }, FALLING_PLATFORM_DELAY); } player.velocityY = 0; player.isJumping = false; player.jumpsLeft = 2; } } } } if (player.y <= DEADLY_BORDER_HEIGHT) { if (player.x < canvas.width - exit.size - 300) { killPlayer(); } } else if (player.y + PLAYER_SIZE >= canvas.height - DEADLY_BORDER_HEIGHT) { killPlayer(); } if (player.x < 0) player.x = 0; if (player.x + PLAYER_SIZE > canvas.width) player.x = canvas.width - PLAYER_SIZE; if (player.y < 0) { player.y = 0; player.velocityY = 0; } if (player.y + PLAYER_SIZE > canvas.height) { player.y = canvas.height - PLAYER_SIZE; player.velocityY = 0; } 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++; 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; } 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(); } }); } function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } function draw(currentTime) { if ((hardMode || superHardMode) && gameState === GAME_STATE.PLAYING) { const timeLimit = superHardMode ? SUPER_HARD_MODE_TIME_LIMIT : HARD_MODE_TIME_LIMIT; const timeElapsed = Date.now() - levelStartTime; const timeRemaining = Math.max(0, timeLimit * 1000 - timeElapsed); const progressRatio = timeRemaining / (timeLimit * 1000); ctx.fillStyle = COLORS.BACKGROUND; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; const progressWidth = canvas.width * progressRatio; ctx.fillRect(progressWidth, 0, canvas.width - progressWidth, canvas.height); } else { ctx.fillStyle = COLORS.BACKGROUND; ctx.fillRect(0, 0, canvas.width, canvas.height); } ctx.fillStyle = COLORS.DEADLY_BORDER; ctx.fillRect(0, 0, canvas.width - exit.size - 300, 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 ); for (let platform of platforms) { ctx.fillStyle = platform.type === PLATFORM_TYPE.DEADLY ? COLORS.PLATFORM.DEADLY : platform.type === PLATFORM_TYPE.FALLING ? COLORS.PLATFORM.FALLING : COLORS.PLATFORM.NORMAL; ctx.fillRect(platform.x, platform.y, platform.width, platform.height); } ctx.fillStyle = COLORS.EXIT; ctx.fillRect(exit.x, exit.y, exit.size, exit.size); if (!player.isDead) { ctx.fillStyle = player.gravityMultiplier > 0 ? COLORS.PLAYER.NORMAL : COLORS.PLAYER.INVERTED; ctx.fillRect(player.x, player.y, PLAYER_SIZE, PLAYER_SIZE); } ctx.fillStyle = COLORS.TEXT; ctx.font = '20px Arial'; ctx.textAlign = 'left'; ctx.fillText(`Level: ${level}`, 10, 30); // Particles can have 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 = COLORS.ENEMY; ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height); }); // Death particles x_x 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 = COLORS.GAME_OVER.OVERLAY; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = COLORS.GAME_OVER.TEXT; ctx.font = '100px Arial'; ctx.textAlign = 'center'; ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2); ctx.font = '24px Arial'; ctx.fillText('Press ENTER to restart', canvas.width / 2, canvas.height / 2 + 40); ctx.textAlign = 'left'; } // Show some help text on the first level so that folks know what to do if (level === 1 && gameState === GAME_STATE.PLAYING) { ctx.fillStyle = COLORS.TEXT; 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'; } } function gameLoop(currentTime) { if (lastFrameTime === 0) { lastFrameTime = currentTime; lastFpsUpdate = currentTime; } const deltaTime = currentTime - lastFrameTime; lastFrameTime = currentTime; accumulator += deltaTime; while (accumulator >= FRAME_TIME) { updatePlayer(); updateEnemies(); updateParticles(); updateDeathParticles(); updatePlatforms(); accumulator -= FRAME_TIME; } draw(currentTime); requestAnimationFrame(gameLoop); } function createPlatformParticles(platform) { for (let i = 0; i < PLATFORM_PARTICLE_COUNT; i++) { const angle = (Math.PI * 2 * i) / PLATFORM_PARTICLE_COUNT; particles.push({ x: platform.x + platform.width / 2, y: platform.y + platform.height / 2, velocityX: Math.cos(angle) * PLATFORM_PARTICLE_SPEED * (0.5 + Math.random()), velocityY: Math.sin(angle) * PLATFORM_PARTICLE_SPEED * (0.5 + Math.random()), size: PLATFORM_PARTICLE_SIZE + Math.random() * 2, life: PLATFORM_PARTICLE_LIFETIME, initialOpacity: 0.6 + Math.random() * 0.4 }); } } function updatePlatforms() { platforms.forEach(platform => { if (platform.type === PLATFORM_TYPE.FALLING && platform.isFalling) { platform.velocityY += FALLING_PLATFORM_GRAVITY * player.gravityMultiplier; platform.y += platform.velocityY; // Create particles when platform goes off screen if ((player.gravityMultiplier > 0 && platform.y > canvas.height + 50) || (player.gravityMultiplier < 0 && platform.y < -50)) { createPlatformParticles(platform); } } }); // Remove platforms that have fallen off screen in either direction platforms = platforms.filter(platform => platform.type !== PLATFORM_TYPE.FALLING || (player.gravityMultiplier > 0 ? platform.y < canvas.height + 100 : platform.y > -100) ); } let hardMode = false; let superHardMode = false; let levelStartTime = 0; const hardModeButton = document.createElement('button'); hardModeButton.textContent = 'Hard Mode: OFF'; hardModeButton.style.position = 'fixed'; hardModeButton.style.left = '10px'; hardModeButton.style.top = '40px'; hardModeButton.style.padding = '5px 10px'; hardModeButton.style.backgroundColor = COLORS.PLATFORM.NORMAL; hardModeButton.style.color = 'white'; hardModeButton.style.border = 'none'; hardModeButton.style.cursor = 'pointer'; document.body.appendChild(hardModeButton); const superHardModeButton = document.createElement('button'); superHardModeButton.textContent = 'Super Hard Mode: OFF'; superHardModeButton.style.position = 'fixed'; superHardModeButton.style.left = '10px'; superHardModeButton.style.top = '70px'; superHardModeButton.style.padding = '5px 10px'; superHardModeButton.style.backgroundColor = COLORS.PLATFORM.NORMAL; superHardModeButton.style.color = 'white'; superHardModeButton.style.border = 'none'; superHardModeButton.style.cursor = 'pointer'; document.body.appendChild(superHardModeButton); hardModeButton.addEventListener('click', () => { hardMode = !hardMode; superHardMode = false; hardModeButton.textContent = `Hard Mode: ${hardMode ? 'ON' : 'OFF'}`; superHardModeButton.textContent = 'Super Hard Mode: OFF'; resetPlayer(); hardModeButton.blur(); }); superHardModeButton.addEventListener('click', () => { superHardMode = !superHardMode; hardMode = false; superHardModeButton.textContent = `Super Hard Mode: ${superHardMode ? 'ON' : 'OFF'}`; hardModeButton.textContent = 'Hard Mode: OFF'; resetPlayer(); superHardModeButton.blur(); }); document.body.style.margin = '0'; document.body.style.overflow = 'hidden'; resizeCanvas(); window.addEventListener('resize', resizeCanvas); generateLevel(); requestAnimationFrame(gameLoop);