about summary refs log tree commit diff stats
path: root/html/mountain/game.js
diff options
context:
space:
mode:
Diffstat (limited to 'html/mountain/game.js')
-rw-r--r--html/mountain/game.js810
1 files changed, 810 insertions, 0 deletions
diff --git a/html/mountain/game.js b/html/mountain/game.js
new file mode 100644
index 0000000..39fff3b
--- /dev/null
+++ b/html/mountain/game.js
@@ -0,0 +1,810 @@
+/* ================================
+
+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;
+    }
+}
+
+// Try to compensate for varying viewport widths
+function calculateTimeLimit(isSuper) {
+    const baseLimit = isSuper ? SUPER_HARD_MODE_TIME_LIMIT : HARD_MODE_TIME_LIMIT;
+    
+    if (canvas.width <= 2000) return baseLimit;
+    
+    const extraWidth = canvas.width - 2000;
+    const extraSeconds = Math.floor(extraWidth / 1000) * (isSuper ? 0.5 : 1);
+    
+    return baseLimit + extraSeconds;
+}
+
+function updatePlayer() {
+    if (gameState === GAME_STATE.GAME_OVER) {
+        if (deathAnimationTimer <= 0 && keys['Enter']) {
+            resetPlayer();
+        }
+        return;
+    }
+
+    const timeLimit = superHardMode ? 
+        calculateTimeLimit(true) : 
+        calculateTimeLimit(false);
+        
+    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 ? 
+            calculateTimeLimit(true) : 
+            calculateTimeLimit(false);
+            
+        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);