about summary refs log blame commit diff stats
path: root/html/mountain/game.js
blob: 39fff3ba595d0e53a6bb6686e42a791055628c9f (plain) (tree)



























                                    
                                                

                                    



                                
 

                       
                                               



                       

                      

  
                       

                      
                     
 







                                                   




                                     












                                    
 




                                      


                                          
































                                                            
 




















                                         



                                             











                                               















                                                                                                                    
                                                 

                     







                                                                

                                          
                                                                                 
                                                                     


                                                                                      
                                                 

                                                       
                                                              

                                                           
                                                                           

                                                 
                                                

                                        











































































                                                                                                 









                                                                                         
                           




                                             
                                                                                    
                                                          
                                                                                        

                                              
                                                                    

              


                                                                                          



                                                                     

                                                                                                




                                                                           
                                                                
                              
                                                                                    







                                                                            
                                                                                                   








                                                                                          

                  
                                                                             







                                                                                 

                                                                   












                                                      




                                                                                           
 
                        

 

                          
                 
    
                    


                                                



                                  
                    


                                                  


                                  
 

                                 
                                                                
                                                                                        



















                                                                    












                                                                                         

                                

 












                                                                  
                                








                                                 
 











                                                                                  

                                             
                                                        




                          



                                      




                                                                                        



                                                               

                                                             
                             


                                                   

                                                           



                                                                                            
        
                                       


                                                                                 


                                         


                                 







                                                     





                                                                                               








                                                               
                                 
                        




                                                                                         







                                             
                                           
                                                        
                         

                                                                                
                     

     




                                                                                     



                                                 

     




                                          
                        





                                                                      
     
 








                                                
 



                                       
 
                            
                                                                          



                                          













                                                                                    
    
                                         
                                                                             
    






                                                                                                
    
                                     


                                                                                          


                                                                              
                                

                                                       
                         
                                                                                                     


                                                                   
                                
                            
                           

                                            
                                             






                                                                                    
                                     


                                                                  
                          






                                                                                          
                                                 

                                                        
                                              
                                 



                                                                       
                                                                                         
        
                               
     
 
                                                                           
                                                          
                                    



                                                                                              
                               

     






                                    


                                                  

                             

                                       


                               
                          


                                  
                      


                                    



































                                                                                             







































                                                                         
                                                                                         




                                                   



                                                
                
                                
/* ================================

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);