about summary refs log tree commit diff stats
path: root/html
diff options
context:
space:
mode:
Diffstat (limited to 'html')
-rw-r--r--html/rogue/index.html26
-rw-r--r--html/rogue/js/camera.js56
-rw-r--r--html/rogue/js/config.js32
-rw-r--r--html/rogue/js/input.js19
-rw-r--r--html/rogue/js/player.js64
-rw-r--r--html/rogue/js/renderer.js97
-rw-r--r--html/rogue/js/rogue.js144
-rw-r--r--html/rogue/js/world.js1054
8 files changed, 1492 insertions, 0 deletions
diff --git a/html/rogue/index.html b/html/rogue/index.html
new file mode 100644
index 0000000..ef37885
--- /dev/null
+++ b/html/rogue/index.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Rogue</title>
+    <style>
+        body {
+            margin: 0;
+            overflow: hidden;
+        }
+        canvas {
+            display: block;
+        }
+    </style>
+</head>
+<body>
+    <canvas id="gameCanvas"></canvas>
+    <!-- Load config first since other files depend on it -->
+    <script src="js/config.js"></script>
+    <script src="js/renderer.js"></script>
+    <script src="js/input.js"></script>
+    <script src="js/world.js"></script>
+    <script src="js/player.js"></script>
+    <script src="js/camera.js"></script>
+    <script src="js/rogue.js"></script>
+</body>
+</html>
diff --git a/html/rogue/js/camera.js b/html/rogue/js/camera.js
new file mode 100644
index 0000000..e5d5d14
--- /dev/null
+++ b/html/rogue/js/camera.js
@@ -0,0 +1,56 @@
+const createCamera = (x, y) => ({
+    x,
+    y,
+    width: window.innerWidth,
+    height: window.innerHeight,
+    // Define the dead zone (the area where camera won't move)
+    deadZone: {
+        x: window.innerWidth * 0.3, // 30% of screen width
+        y: window.innerHeight * 0.3, // 30% of screen height
+    }
+});
+
+const updateCamera = (camera, target) => {
+    // Calculate the center point of the screen
+    const screenCenterX = camera.x + camera.width / 2;
+    const screenCenterY = camera.y + camera.height / 2;
+
+    // Calculate the distance from the target to the screen center
+    const distanceX = target.x - screenCenterX;
+    const distanceY = target.y - screenCenterY;
+
+    // Calculate the dead zone boundaries
+    const deadZoneLeft = -camera.deadZone.x / 2;
+    const deadZoneRight = camera.deadZone.x / 2;
+    const deadZoneTop = -camera.deadZone.y / 2;
+    const deadZoneBottom = camera.deadZone.y / 2;
+
+    // Calculate new camera position with smooth following
+    let newX = camera.x;
+    let newY = camera.y;
+
+    // Horizontal camera movement
+    if (distanceX < deadZoneLeft) {
+        newX += distanceX - deadZoneLeft;
+    } else if (distanceX > deadZoneRight) {
+        newX += distanceX - deadZoneRight;
+    }
+
+    // Vertical camera movement
+    if (distanceY < deadZoneTop) {
+        newY += distanceY - deadZoneTop;
+    } else if (distanceY > deadZoneBottom) {
+        newY += distanceY - deadZoneBottom;
+    }
+
+    // Add subtle smoothing to camera movement
+    const smoothing = 0.1;
+    newX = camera.x + (newX - camera.x) * smoothing;
+    newY = camera.y + (newY - camera.y) * smoothing;
+
+    return {
+        ...camera,
+        x: newX,
+        y: newY
+    };
+};
diff --git a/html/rogue/js/config.js b/html/rogue/js/config.js
new file mode 100644
index 0000000..77100e1
--- /dev/null
+++ b/html/rogue/js/config.js
@@ -0,0 +1,32 @@
+const CONFIG = {
+    player: {
+        width: 32,
+        height: 48,
+        speed: 5,
+        jumpForce: 12,
+        gravity: 0.5,
+        color: '#ff0000'
+    },
+    world: {
+        groundHeight: 12,
+        wilderness: {
+            vegetation: {
+                grass: {
+                    colors: ['#3a5', '#294'],
+                    hatch: {
+                        angle: Math.PI / 4,
+                        variation: Math.PI / 6,
+                        spacing: 4,
+                        length: 8,
+                        margin: 2
+                    }
+                }
+            }
+        }
+    },
+    camera: {
+        width: window.innerWidth,
+        height: window.innerHeight,
+        followSpeed: 0.1
+    }
+}; 
\ No newline at end of file
diff --git a/html/rogue/js/input.js b/html/rogue/js/input.js
new file mode 100644
index 0000000..047321c
--- /dev/null
+++ b/html/rogue/js/input.js
@@ -0,0 +1,19 @@
+const setupInputHandlers = (canvas, gameState) => {
+    let mouseMoveThrottle;
+    
+    canvas.addEventListener('mousemove', (e) => {
+        if (!mouseMoveThrottle) {
+            mouseMoveThrottle = setTimeout(() => {
+                gameState.debug.mouseX = e.clientX + gameState.camera.x;
+                gameState.debug.mouseY = e.clientY + gameState.camera.y;
+                mouseMoveThrottle = null;
+            }, 16);
+        }
+    });
+
+    window.addEventListener('keydown', (e) => {
+        if (e.key === 'd') {
+            gameState.debug.enabled = !gameState.debug.enabled;
+        }
+    });
+}; 
\ No newline at end of file
diff --git a/html/rogue/js/player.js b/html/rogue/js/player.js
new file mode 100644
index 0000000..270b26f
--- /dev/null
+++ b/html/rogue/js/player.js
@@ -0,0 +1,64 @@
+const createPlayer = (x, y) => ({
+    x,
+    y,
+    width: 32,
+    height: 32,
+    velocityX: 0,
+    velocityY: 0,
+    speed: 300,
+    jumping: false
+});
+
+const updatePlayer = (player, deltaTime) => {
+    const keys = getKeys();
+    const seconds = deltaTime / 1000;
+
+    let velocityX = 0;
+    let velocityY = player.velocityY;
+
+    // Horizontal movement
+    if (keys.ArrowLeft) velocityX -= player.speed;
+    if (keys.ArrowRight) velocityX += player.speed;
+
+    // Simple jumping (can be improved)
+    if (keys.ArrowUp && !player.jumping) {
+        velocityY = -500;
+    }
+
+    // Apply gravity
+    velocityY += 980 * seconds; // 980 pixels/secondĀ²
+
+    // Update position
+    const x = player.x + velocityX * seconds;
+    const y = player.y + velocityY * seconds;
+
+    // Create updated player state
+    let updatedPlayer = {
+        ...player,
+        x,
+        y,
+        velocityX,
+        velocityY
+    };
+
+    // Handle collisions with the world
+    return handleWorldCollisions(updatedPlayer, window.gameState.world);
+};
+
+const renderPlayer = (ctx, player) => {
+    ctx.fillStyle = '#f00';
+    ctx.fillRect(player.x, player.y, player.width, player.height);
+};
+
+// Key handling
+const keys = {};
+
+window.addEventListener('keydown', (e) => {
+    keys[e.key] = true;
+});
+
+window.addEventListener('keyup', (e) => {
+    keys[e.key] = false;
+});
+
+const getKeys = () => ({...keys});
diff --git a/html/rogue/js/renderer.js b/html/rogue/js/renderer.js
new file mode 100644
index 0000000..617b4c7
--- /dev/null
+++ b/html/rogue/js/renderer.js
@@ -0,0 +1,97 @@
+// Rendering constants
+const RENDER_CONSTANTS = {
+    VIEWPORT_BUFFER: 100,
+    SKY_COLORS: {
+        TOP: '#1a1a2e',
+        UPPER_MID: '#2d1b3d',
+        LOWER_MID: '#462639',
+        BOTTOM: '#1f1f2f'
+    },
+    GROUND_COLOR: '#4a4',
+    DEBUG_FONT: '14px monospace',
+    DEBUG_COLOR: '#ffffff'
+};
+
+// Helper functions for rendering
+const createSkyGradient = (ctx, groundY) => {
+    const gradient = ctx.createLinearGradient(0, 0, 0, groundY);
+    gradient.addColorStop(0, RENDER_CONSTANTS.SKY_COLORS.TOP);
+    gradient.addColorStop(0.4, RENDER_CONSTANTS.SKY_COLORS.UPPER_MID);
+    gradient.addColorStop(0.7, RENDER_CONSTANTS.SKY_COLORS.LOWER_MID);
+    gradient.addColorStop(1, RENDER_CONSTANTS.SKY_COLORS.BOTTOM);
+    return gradient;
+};
+
+const getViewBounds = (camera) => ({
+    left: camera.x - RENDER_CONSTANTS.VIEWPORT_BUFFER,
+    right: camera.x + camera.width + RENDER_CONSTANTS.VIEWPORT_BUFFER,
+    top: camera.y - RENDER_CONSTANTS.VIEWPORT_BUFFER,
+    bottom: camera.y + camera.height + RENDER_CONSTANTS.VIEWPORT_BUFFER
+});
+
+const isInView = (x, viewBounds) => 
+    x > viewBounds.left && x < viewBounds.right;
+
+// Layer rendering functions
+const renderBackground = (ctx, state, groundY, viewBounds) => {
+    // Save the current transform
+    ctx.save();
+    
+    // Reset transform for viewport-fixed sky and initial black fill
+    ctx.setTransform(1, 0, 0, 1, 0, 0);
+    
+    // Sky (fixed to viewport)
+    ctx.fillStyle = state.cachedGradient;
+    ctx.fillRect(0, 0, ctx.canvas.width, groundY);
+
+    // Initial black fill for the bottom of the viewport
+    ctx.fillStyle = '#000000';
+    ctx.fillRect(0, groundY, ctx.canvas.width, ctx.canvas.height - groundY + 1);
+
+    // Restore transform for world-space rendering
+    ctx.restore();
+    
+    // Additional black fill that follows the camera (extends far to left and right)
+    ctx.fillStyle = '#000000';
+    ctx.fillRect(
+        viewBounds.left - 2000,
+        groundY,
+        viewBounds.right - viewBounds.left + 4000,
+        ctx.canvas.height
+    );
+};
+
+const renderBackgroundObjects = (ctx, state, groundY, viewBounds) => {
+    state.world.backgroundTrees
+        .filter(tree => isInView(tree.x, viewBounds))
+        .forEach(tree => renderTree(ctx, tree, groundY));
+    
+    state.world.backgroundRocks
+        .filter(rock => isInView(rock.x, viewBounds))
+        .forEach(rock => renderRock(ctx, rock, groundY));
+    
+    state.world.backgroundGrass
+        .filter(grass => isInView(grass.x, viewBounds))
+        .forEach(grass => renderGrass(ctx, grass, groundY, state.time));
+};
+
+const renderForegroundObjects = (ctx, state, groundY, viewBounds) => {
+    state.world.foregroundTrees
+        .filter(tree => isInView(tree.x, viewBounds))
+        .forEach(tree => renderTree(ctx, tree, groundY));
+    
+    state.world.foregroundRocks
+        .filter(rock => isInView(rock.x, viewBounds))
+        .forEach(rock => renderRock(ctx, rock, groundY));
+    
+    state.world.foregroundGrass
+        .filter(grass => isInView(grass.x, viewBounds))
+        .forEach(grass => renderGrass(ctx, grass, groundY, state.time));
+};
+
+const renderDebugInfo = (ctx, state) => {
+    ctx.fillStyle = RENDER_CONSTANTS.DEBUG_COLOR;
+    ctx.font = RENDER_CONSTANTS.DEBUG_FONT;
+    const text = `x: ${Math.round(state.debug.mouseX)}, y: ${Math.round(state.debug.mouseY)}`;
+    ctx.fillText(text, 10, ctx.canvas.height - 20);
+}; 
\ No newline at end of file
diff --git a/html/rogue/js/rogue.js b/html/rogue/js/rogue.js
new file mode 100644
index 0000000..64716a4
--- /dev/null
+++ b/html/rogue/js/rogue.js
@@ -0,0 +1,144 @@
+window.gameState = null;
+
+const initGame = () => {
+    const canvas = document.getElementById('gameCanvas');
+    const ctx = canvas.getContext('2d');
+    
+    // Target frame rate
+    const FPS = 60;
+    const FRAME_TIME = 1000 / FPS;
+    let lastFrameTime = 0;
+
+    // Set canvas to full viewport size
+    const resizeCanvas = () => {
+        canvas.width = window.innerWidth;
+        canvas.height = window.innerHeight;
+        // Clear any cached gradients when resizing
+        if (window.gameState) {
+            window.gameState.cachedGradient = null;
+        }
+    };
+
+    window.addEventListener('resize', resizeCanvas);
+    resizeCanvas();
+
+    // Calculate initial player position at ground level
+    const groundY = canvas.height - CONFIG.world.groundHeight;
+    const initialPlayerY = groundY - CONFIG.player.height;
+
+    // Game state
+    let gameState = {
+        time: 0,
+        player: createPlayer(100, initialPlayerY),
+        camera: createCamera(0, 0),
+        world: createWorld(),
+        debug: {
+            enabled: false,
+            mouseX: 0,
+            mouseY: 0
+        }
+    };
+    
+    // Make gameState globally accessible
+    window.gameState = gameState;
+
+    // Set up input handlers
+    setupInputHandlers(canvas, gameState);
+
+    // Game loop
+    const gameLoop = (timestamp) => {
+        // Check if enough time has passed since last frame
+        if (timestamp - lastFrameTime < FRAME_TIME) {
+            requestAnimationFrame(gameLoop);
+            return;
+        }
+
+        // Calculate delta time (capped at 1 second to prevent huge jumps)
+        const deltaTime = Math.min(timestamp - lastFrameTime, 1000);
+        lastFrameTime = timestamp;
+        gameState.time = timestamp;
+
+        // Clear the entire canvas
+        ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+        // Update
+        gameState = updateGame(gameState, deltaTime);
+
+        // Render
+        render(ctx, gameState);
+
+        // Schedule next frame
+        requestAnimationFrame(gameLoop);
+    };
+
+    // Start the game loop
+    lastFrameTime = performance.now();
+    requestAnimationFrame(gameLoop);
+};
+
+const updateGame = (state, deltaTime) => {
+    const updatedPlayer = updatePlayer(state.player, deltaTime);
+    const updatedCamera = updateCamera(state.camera, updatedPlayer);
+
+    return {
+        ...state,
+        player: updatedPlayer,
+        camera: updatedCamera
+    };
+};
+
+const render = (ctx, state) => {
+    const groundY = ctx.canvas.height - state.world.groundHeight;
+
+    // Cache sky gradient
+    if (!state.cachedGradient) {
+        state.cachedGradient = createSkyGradient(ctx, groundY);
+    }
+
+    const viewBounds = getViewBounds(state.camera);
+
+    // Apply camera transform
+    ctx.save();
+    ctx.translate(-state.camera.x, -state.camera.y);
+
+    // Clear the transformed canvas area
+    ctx.clearRect(
+        viewBounds.left,
+        0,
+        viewBounds.right - viewBounds.left,
+        ctx.canvas.height
+    );
+
+    // Render layers
+    renderBackground(ctx, state, groundY, viewBounds);
+    renderBackgroundObjects(ctx, state, groundY, viewBounds);
+    
+    // Render platforms
+    state.world.platforms.forEach(platform => {
+        ctx.fillStyle = platform.color;
+        ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
+    });
+
+    renderPlayer(ctx, state.player);
+    renderForegroundObjects(ctx, state, groundY, viewBounds);
+
+    // Render ground line
+    ctx.fillStyle = RENDER_CONSTANTS.GROUND_COLOR;
+    ctx.fillRect(
+        state.camera.x - 1000,
+        groundY,
+        ctx.canvas.width + 2000,
+        1
+    );
+
+    // Handle debug rendering
+    if (state.debug.enabled) {
+        ctx.restore();
+        renderDebugInfo(ctx, state);
+    } else {
+        ctx.restore();
+    }
+};
+
+// Initialize the game when the window loads
+window.addEventListener('load', initGame);
diff --git a/html/rogue/js/world.js b/html/rogue/js/world.js
new file mode 100644
index 0000000..2b2529a
--- /dev/null
+++ b/html/rogue/js/world.js
@@ -0,0 +1,1054 @@
+// Color palettes and configurations
+const NATURE_COLORS = {
+    GREENS: [
+        '#11aa33', // Darker forest green
+        '#22bb44', // Medium forest green
+        '#33cc55', // Bright forest green
+        '#118844', // Deep sea green
+        '#22aa66', // Sage green
+        '#11bb55', // Deep pine
+        '#229955', // Ocean green
+        '#33bb66', // Fresh spring green
+        '#117744', // Dark emerald
+    ],
+    TRUNK_COLORS: {
+        LIGHT: '#664422',
+        MEDIUM: '#553311',
+        DARK: '#442200'
+    }
+};
+
+const GRASS_CONFIG = {
+    MIN_BLADES: 4,
+    MAX_BLADES: 8,
+    SPREAD_FACTOR: 0.7,
+    RUSTLE_SPEED: 300,
+    WAVE_SPEED: 1000,
+    GLOW_SPEED: 500,
+    BASE_BLADE_WIDTH: 5
+};
+
+const FIR_CONFIG = {
+    ROW_COUNT: 40,
+    FEATHERS_PER_ROW: 24,
+    EDGE_FEATHER_COUNT: 40,
+    FEATHER_WIDTH: 2,
+    TAPER_POWER: 0.8,
+    CENTER_NEEDLES: 5,     // Number of needles in center column
+    NEEDLE_SPACING: 1.5,   // Spacing between center needles
+    UPWARD_TILT: 0.1,     // Amount of upward tilt (in radians)
+    ANGLE_VARIATION: 0.05, // Variation in needle angles
+    LENGTH_MULTIPLIER: 0.8 // Base length multiplier for needles
+};
+
+// Define world object types
+const WORLD_OBJECTS = {
+    PLATFORM: 'platform',
+    FIR_BACKGROUND: 'fir_background',
+    FIR_FOREGROUND: 'fir_foreground',
+    MAPLE_BACKGROUND: 'maple_background',
+    MAPLE_FOREGROUND: 'maple_foreground',
+    ROCK_BACKGROUND: 'rock_background',
+    ROCK_FOREGROUND: 'rock_foreground',
+    GRASS_BACKGROUND: 'grass_background',
+    GRASS_FOREGROUND: 'grass_foreground'
+};
+
+// Separate configurations for different tree types
+const FIR_TYPES = {
+    SMALL: {
+        type: 'fir',
+        width: 100,
+        height: 230,
+        canopyOffset: 45,
+        canopyColor: '#22aa44',
+        trunkColor: '#553311'
+    },
+    LARGE: {
+        type: 'fir',
+        width: 150,
+        height: 320,
+        canopyOffset: 65,
+        canopyColor: '#11aa44',
+        trunkColor: '#442200'
+    }
+};
+
+const MAPLE_TYPES = {
+    SMALL: {
+        type: 'maple',
+        width: 100,
+        height: 330,
+        trunkHeight: 60,
+        canopyRadius: 35,
+        leafClusters: 5,
+        canopyColor: '#33aa22', // Bright green
+        trunkColor: '#664422'
+    },
+    MEDIUM: {
+        type: 'maple',
+        width: 130,
+        height: 360,
+        trunkHeight: 70,
+        canopyRadius: 45,
+        leafClusters: 6,
+        canopyColor: '#228833', // Deep forest green
+        trunkColor: '#553311'
+    },
+    LARGE: {
+        type: 'maple',
+        width: 160,
+        height: 400,
+        trunkHeight: 85,
+        canopyRadius: 55,
+        leafClusters: 7,
+        canopyColor: '#115522', // Dark green
+        trunkColor: '#553311'
+    },
+    BRIGHT: {
+        type: 'maple',
+        width: 140,
+        height: 380,
+        trunkHeight: 75,
+        canopyRadius: 50,
+        leafClusters: 6,
+        canopyColor: '#44bb33', // Vibrant green
+        trunkColor: '#664422'
+    },
+    SAGE: {
+        type: 'maple',
+        width: 150,
+        height: 490,
+        trunkHeight: 80,
+        canopyRadius: 52,
+        leafClusters: 6,
+        canopyColor: '#225544', // Sage green
+        trunkColor: '#553311'
+    }
+};
+
+// Rock configurations
+const ROCK_TYPES = {
+    SMALL: {
+        width: 40,
+        height: 30,
+        color: '#666',
+        highlights: '#888'
+    },
+    MEDIUM: {
+        width: 70,
+        height: 45,
+        color: '#555',
+        highlights: '#777'
+    },
+    LARGE: {
+        width: 100,
+        height: 60,
+        color: '#444',
+        highlights: '#666'
+    }
+};
+
+// Add grass configurations
+const GRASS_TYPES = {
+    TALL: {
+        type: 'grass',
+        width: 30,
+        height: 40,
+        color: '#33aa55',
+        shadowColor: '#229944'
+    },
+    SHORT: {
+        type: 'grass',
+        width: 20,
+        height: 25,
+        color: '#33bb66',
+        shadowColor: '#22aa55'
+    },
+    GOLDEN_TALL: {
+        type: 'grass',
+        width: 30,
+        height: 40,
+        color: '#eebb33',
+        shadowColor: '#cc9922'
+    },
+    GOLDEN_SHORT: {
+        type: 'grass',
+        width: 20,
+        height: 25,
+        color: '#ffcc44',
+        shadowColor: '#ddaa33'
+    },
+    BLUE_TALL: {
+        type: 'grass',
+        width: 30,
+        height: 40,
+        color: '#44aaff',
+        shadowColor: '#2299ff',
+        glowing: true
+    },
+    BLUE_SHORT: {
+        type: 'grass',
+        width: 20,
+        height: 25,
+        color: '#55bbff',
+        shadowColor: '#33aaff',
+        glowing: true
+    }
+};
+
+// Utility functions
+const utils = {
+    getRandomColorFromPalette: (palette, seed1, seed2) => {
+        const colorIndex = Math.floor(seededRandom(seed1, seed2) * palette.length);
+        return palette[colorIndex];
+    },
+
+    getBladeCount: (x, height) => {
+        const randomValue = Math.abs(seededRandom(x, height));
+        return GRASS_CONFIG.MIN_BLADES + 
+            Math.round(randomValue * (GRASS_CONFIG.MAX_BLADES - GRASS_CONFIG.MIN_BLADES));
+    },
+
+    calculateTaper: (t) => Math.pow(1 - t, FIR_CONFIG.TAPER_POWER)
+};
+
+// Create a world with platforms, trees, and rocks
+const createWorld = () => {
+    const world = {
+        groundHeight: 12,
+        // Separate arrays for different layers
+        backgroundTrees: [
+            // Far left trees
+            {
+                type: WORLD_OBJECTS.FIR_BACKGROUND,
+                x: -1500,
+                config: FIR_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_BACKGROUND,
+                x: -1200,
+                config: MAPLE_TYPES.SAGE
+            },
+            {
+                type: WORLD_OBJECTS.FIR_BACKGROUND,
+                x: -900,
+                config: FIR_TYPES.SMALL
+            },
+            // Existing trees
+            {
+                type: WORLD_OBJECTS.FIR_BACKGROUND,
+                x: -400,
+                config: FIR_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_BACKGROUND,
+                x: -250,
+                config: MAPLE_TYPES.BRIGHT
+            },
+            {
+                type: WORLD_OBJECTS.FIR_BACKGROUND,
+                x: 50,
+                config: FIR_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_BACKGROUND,
+                x: 250,
+                config: MAPLE_TYPES.MEDIUM
+            },
+            {
+                type: WORLD_OBJECTS.FIR_BACKGROUND,
+                x: 500,
+                config: FIR_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_BACKGROUND,
+                x: 650,
+                config: MAPLE_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.FIR_BACKGROUND,
+                x: 900,
+                config: FIR_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_BACKGROUND,
+                x: 1100,
+                config: MAPLE_TYPES.LARGE
+            },
+            // Far right trees
+            {
+                type: WORLD_OBJECTS.MAPLE_BACKGROUND,
+                x: 1400,
+                config: MAPLE_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.FIR_BACKGROUND,
+                x: 1700,
+                config: FIR_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_BACKGROUND,
+                x: 2000,
+                config: MAPLE_TYPES.LARGE
+            }
+        ],
+        backgroundRocks: [
+            // Far left rocks
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: -1300,
+                config: ROCK_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: -1000,
+                config: ROCK_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: -300,
+                config: ROCK_TYPES.MEDIUM
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: -100,
+                config: ROCK_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: 150,
+                config: ROCK_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: 400,
+                config: ROCK_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: 750,
+                config: ROCK_TYPES.MEDIUM
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: 1000,
+                config: ROCK_TYPES.SMALL
+            },
+            // Far right rocks
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: 1600,
+                config: ROCK_TYPES.MEDIUM
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_BACKGROUND,
+                x: 1900,
+                config: ROCK_TYPES.SMALL
+            }
+        ],
+        platforms: [
+            // {
+            //     type: WORLD_OBJECTS.PLATFORM,
+            //     x: 300,
+            //     y: 300,
+            //     width: 200,
+            //     height: 20,
+            //     color: '#484'
+            // },
+            // {
+            //     type: WORLD_OBJECTS.PLATFORM,
+            //     x: 600,
+            //     y: 200,
+            //     width: 200,
+            //     height: 20,
+            //     color: '#484'
+            // },
+            // {
+            //     type: WORLD_OBJECTS.PLATFORM,
+            //     x: -200,
+            //     y: 250,
+            //     width: 200,
+            //     height: 20,
+            //     color: '#484'
+            // }
+        ],
+        foregroundTrees: [
+            // Far left trees
+            {
+                type: WORLD_OBJECTS.FIR_FOREGROUND,
+                x: -1400,
+                config: FIR_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_FOREGROUND,
+                x: -1100,
+                config: MAPLE_TYPES.SMALL
+            },
+            // Existing trees
+            {
+                type: WORLD_OBJECTS.MAPLE_FOREGROUND,
+                x: -150,
+                config: MAPLE_TYPES.BRIGHT
+            },
+            {
+                type: WORLD_OBJECTS.FIR_FOREGROUND,
+                x: 200,
+                config: FIR_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_FOREGROUND,
+                x: 450,
+                config: MAPLE_TYPES.SAGE
+            },
+            {
+                type: WORLD_OBJECTS.FIR_FOREGROUND,
+                x: 800,
+                config: FIR_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_FOREGROUND,
+                x: 1200,
+                config: MAPLE_TYPES.SMALL
+            },
+            // Far right trees
+            {
+                type: WORLD_OBJECTS.FIR_FOREGROUND,
+                x: 1500,
+                config: FIR_TYPES.LARGE
+            },
+            {
+                type: WORLD_OBJECTS.MAPLE_FOREGROUND,
+                x: 1800,
+                config: MAPLE_TYPES.SMALL
+            }
+        ],
+        foregroundRocks: [
+            // Far left rocks
+            {
+                type: WORLD_OBJECTS.ROCK_FOREGROUND,
+                x: -1000,
+                config: ROCK_TYPES.MEDIUM
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_FOREGROUND,
+                x: 0,
+                config: ROCK_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_FOREGROUND,
+                x: 300,
+                config: ROCK_TYPES.MEDIUM
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_FOREGROUND,
+                x: 700,
+                config: ROCK_TYPES.SMALL
+            },
+            {
+                type: WORLD_OBJECTS.ROCK_FOREGROUND,
+                x: 950,
+                config: ROCK_TYPES.LARGE
+            },
+            // Far right rocks
+            {
+                type: WORLD_OBJECTS.ROCK_FOREGROUND,
+                x: 1500,
+                config: ROCK_TYPES.LARGE
+            }
+        ],
+        backgroundGrass: [
+            // Far left grass
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -1400, config: GRASS_TYPES.BLUE_TALL },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -1100, config: GRASS_TYPES.GOLDEN_SHORT },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -950, config: GRASS_TYPES.TALL },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -350, config: GRASS_TYPES.SHORT },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -180, config: GRASS_TYPES.GOLDEN_TALL },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 100, config: GRASS_TYPES.TALL },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 320, config: GRASS_TYPES.BLUE_SHORT },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 580, config: GRASS_TYPES.GOLDEN_SHORT },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 750, config: GRASS_TYPES.BLUE_TALL },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 950, config: GRASS_TYPES.TALL },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1050, config: GRASS_TYPES.GOLDEN_TALL },
+            // Far right grass
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1500, config: GRASS_TYPES.BLUE_SHORT },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1750, config: GRASS_TYPES.GOLDEN_TALL },
+            { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1850, config: GRASS_TYPES.SHORT }
+        ],
+        foregroundGrass: [
+            // Far left grass
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -1250, config: GRASS_TYPES.TALL },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -1050, config: GRASS_TYPES.BLUE_SHORT },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -280, config: GRASS_TYPES.BLUE_TALL },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -50, config: GRASS_TYPES.GOLDEN_SHORT },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 150, config: GRASS_TYPES.TALL },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 420, config: GRASS_TYPES.BLUE_SHORT },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 680, config: GRASS_TYPES.GOLDEN_TALL },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 880, config: GRASS_TYPES.SHORT },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1150, config: GRASS_TYPES.BLUE_TALL },
+            // Far right grass
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1650, config: GRASS_TYPES.GOLDEN_TALL },
+            { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1850, config: GRASS_TYPES.BLUE_SHORT }
+        ],
+        // Track grass animation states
+        grassStates: {},
+        // Add methods for quick spatial lookups
+        getObjectsInView: function(bounds) {
+            return {
+                backgroundTrees: this.backgroundTrees.filter(tree => 
+                    tree.x > bounds.left && tree.x < bounds.right
+                ),
+                // ... similar for other object types
+            };
+        }
+    };
+    
+    return world;
+};
+
+// Add seededRandom function
+const seededRandom = (x, y) => {
+    const dot = x * 12.9898 + y * 78.233;
+    return (Math.sin(dot) * 43758.5453123) % 1;
+};
+
+// Add hatching helper function
+const addHatchingPattern = (ctx, x, y, width, height, color) => {
+    const spacing = 4; // Space between hatch lines
+    const length = 10;  // Length of each hatch line
+    const margin = 4; // Increased margin from edges
+    const baseAngle = Math.PI * 1.5; // Vertical angle (pointing down)
+    const angleVariation = Math.PI / 12; // Reduced angle variation
+    
+    // Define darker brown shades
+    const brownShades = [
+        '#442211', // Dark brown
+        '#553322', // Medium dark brown
+        '#443311', // Reddish dark brown
+        '#332211', // Very dark brown
+        '#554422', // Warm dark brown
+    ];
+    
+    // Create clipping path for trunk
+    ctx.save();
+    ctx.beginPath();
+    ctx.rect(
+        x - width/2,
+        y - height,
+        width,
+        height
+    );
+    ctx.clip();
+    
+    // Calculate bounds with increased safety margin
+    const bounds = {
+        minX: x - width/2 + margin,
+        maxX: x + width/2 - margin,
+        minY: y - height + margin,
+        maxY: y - margin
+    };
+
+    // Draw hatching
+    ctx.lineWidth = 1;
+    for (let hatchX = bounds.minX; hatchX < bounds.maxX; hatchX += spacing) {
+        for (let hatchY = bounds.minY; hatchY < bounds.maxY; hatchY += spacing) {
+            // Use position for random variation
+            const seed1 = hatchX * 13.37;
+            const seed2 = hatchY * 7.89;
+            
+            // Random variations with reduced range
+            const variation = {
+                x: (seededRandom(seed1, seed2) - 0.5) * 1.5, // Reduced variation
+                y: (seededRandom(seed2, seed1) - 0.5) * 1.5, // Reduced variation
+                angle: baseAngle + (seededRandom(seed1 + seed2, seed2 - seed1) - 0.5) * angleVariation
+            };
+
+            // Pick a random brown shade
+            const colorIndex = Math.floor(seededRandom(seed1 * seed2, seed2 * seed1) * brownShades.length);
+            ctx.strokeStyle = brownShades[colorIndex];
+
+            // Draw hatch line
+            ctx.beginPath();
+            ctx.moveTo(
+                hatchX + variation.x,
+                hatchY + variation.y
+            );
+            ctx.lineTo(
+                hatchX + Math.cos(variation.angle) * length + variation.x,
+                hatchY + Math.sin(variation.angle) * length + variation.y
+            );
+            ctx.stroke();
+        }
+    }
+    
+    ctx.restore();
+};
+
+class FirTreeRenderer {
+    static renderTrunk(ctx, x, width, height, groundY, trunkColor) {
+        const trunkWidth = width/4;
+        const trunkHeight = height;
+        
+        ctx.fillStyle = trunkColor;
+        ctx.fillRect(
+            x - trunkWidth/2,
+            groundY - trunkHeight,
+            trunkWidth,
+            trunkHeight
+        );
+        
+        addHatchingPattern(ctx, x, groundY, trunkWidth, trunkHeight, trunkColor);
+    }
+
+    static renderCenterNeedles(ctx, x, rowY, taper, featherLength, greenColors) {
+        for (let i = -FIR_CONFIG.CENTER_NEEDLES; i <= FIR_CONFIG.CENTER_NEEDLES; i++) {
+            const offset = i * (FIR_CONFIG.NEEDLE_SPACING * 1.5);
+            
+            // Left needle
+            FirTreeRenderer.renderSingleNeedle(
+                ctx, x + offset, rowY, Math.PI, taper, featherLength, greenColors
+            );
+            
+            // Right needle
+            FirTreeRenderer.renderSingleNeedle(
+                ctx, x + offset, rowY, 0, taper, featherLength, greenColors
+            );
+        }
+    }
+
+    static renderSingleNeedle(ctx, x, y, baseAngle, taper, length, colors) {
+        const angleVar = Math.PI * 0.02;
+        const angle = baseAngle + (angleVar * seededRandom(x, y) - angleVar/2);
+        
+        ctx.strokeStyle = utils.getRandomColorFromPalette(colors, x, y);
+        ctx.beginPath();
+        ctx.moveTo(x, y);
+        ctx.lineTo(
+            x + Math.cos(angle) * length * taper,
+            y + Math.sin(angle) * length * taper
+        );
+        ctx.stroke();
+    }
+
+    static renderRowNeedles(ctx, x, rowY, rowWidth, featherCount, t, taper, featherLength, greenColors) {
+        for (let i = 0; i < featherCount; i++) {
+            const featherT = i / (featherCount - 1);
+            const startX = x - rowWidth/2 + rowWidth * featherT;
+            
+            if (Math.abs(startX - x) < 1) continue;
+            
+            const relativeX = (startX - x) / (rowWidth/2);
+            const baseAngle = relativeX < 0 ? Math.PI : 0;
+            const upwardTilt = Math.PI * FIR_CONFIG.UPWARD_TILT * t;
+            const angle = baseAngle + 
+                (FIR_CONFIG.ANGLE_VARIATION * seededRandom(startX, rowY) - FIR_CONFIG.ANGLE_VARIATION/2) - 
+                upwardTilt;
+            
+            const lengthMultiplier = FIR_CONFIG.LENGTH_MULTIPLIER + (1 - t) * 0.3;
+            const finalLength = featherLength * lengthMultiplier * taper * 
+                (0.9 + seededRandom(startX * rowY, rowY) * 0.2);
+            
+            ctx.strokeStyle = utils.getRandomColorFromPalette(greenColors, startX, rowY);
+            ctx.beginPath();
+            ctx.moveTo(startX, rowY);
+            ctx.lineTo(
+                startX + Math.cos(angle) * finalLength,
+                rowY + Math.sin(angle) * finalLength
+            );
+            ctx.stroke();
+        }
+    }
+
+    static renderEdgeNeedles(ctx, x, baseWidth, baseY, tipY, featherLength, greenColors) {
+        for (let i = 0; i < FIR_CONFIG.EDGE_FEATHER_COUNT; i++) {
+            const t = i / (FIR_CONFIG.EDGE_FEATHER_COUNT - 1);
+            const taper = utils.calculateTaper(t);
+            
+            if (taper <= 0.1) continue;
+            
+            // Left edge
+            FirTreeRenderer.renderEdgeNeedle(
+                ctx, x, baseWidth, baseY, tipY, t, taper, 
+                featherLength, Math.PI, greenColors, true
+            );
+            
+            // Right edge
+            FirTreeRenderer.renderEdgeNeedle(
+                ctx, x, baseWidth, baseY, tipY, t, taper, 
+                featherLength, 0, greenColors, false
+            );
+        }
+    }
+
+    static renderEdgeNeedle(ctx, x, baseWidth, baseY, tipY, t, taper, length, baseAngle, colors, isLeft) {
+        const sign = isLeft ? -1 : 1;
+        const needleX = x + sign * (baseWidth/2 * taper - (baseWidth/2 * taper) * t);
+        const needleY = baseY - (baseY - tipY) * t;
+        
+        const upwardTilt = Math.PI * 0.1 * t;
+        const angle = baseAngle + 
+            (Math.PI * 0.05 * seededRandom(needleX, needleY) - Math.PI * 0.025) - 
+            upwardTilt;
+        
+        ctx.strokeStyle = utils.getRandomColorFromPalette(colors, needleX, needleY);
+        ctx.beginPath();
+        ctx.moveTo(needleX, needleY);
+        ctx.lineTo(
+            needleX + Math.cos(angle) * length * taper * 1.2,
+            needleY + Math.sin(angle) * length * taper * 1.2
+        );
+        ctx.stroke();
+    }
+}
+
+// Helper function to darken/lighten colors
+const shadeColor = (color, percent) => {
+    const num = parseInt(color.replace('#', ''), 16);
+    const amt = Math.round(2.55 * percent);
+    const R = (num >> 16) + amt;
+    const G = (num >> 8 & 0x00FF) + amt;
+    const B = (num & 0x0000FF) + amt;
+    return '#' + (0x1000000 +
+        (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
+        (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
+        (B < 255 ? (B < 1 ? 0 : B) : 255)
+    ).toString(16).slice(1);
+};
+
+// Add new maple tree render function
+const renderMapleTree = (ctx, tree, groundY) => {
+    const { x, config } = tree;
+    const { 
+        width, height, trunkHeight, canopyRadius,
+        leafClusters, trunkColor, canopyColor 
+    } = config;
+    
+    // Draw trunk base with narrower width (changed from width/4 to width/5)
+    const trunkWidth = width/5.5;
+    ctx.fillStyle = trunkColor;
+    ctx.fillRect(
+        x - trunkWidth/2,
+        groundY - trunkHeight,
+        trunkWidth,
+        trunkHeight
+    );
+    
+    // Add trunk hatching with updated width
+    addHatchingPattern(
+        ctx,
+        x,
+        groundY,
+        trunkWidth,
+        trunkHeight,
+        trunkColor
+    );
+
+    // Function to create an irregular polygon
+    const createIrregularPolygon = (centerX, centerY, radius, sides, seed1, seed2) => {
+        ctx.beginPath();
+        
+        // Create points array first to allow smoothing
+        const points = [];
+        for (let i = 0; i < sides; i++) {
+            const angle = (i / sides) * Math.PI * 2;
+            
+            // Even more subtle variation range (0.95-1.05)
+            const radiusVariation = 0.95 + seededRandom(seed1 * i, seed2 * i) * 0.10;
+            const pointRadius = radius * radiusVariation;
+            
+            points.push({
+                x: centerX + Math.cos(angle) * pointRadius,
+                y: centerY + Math.sin(angle) * pointRadius
+            });
+        }
+        
+        // Start the path
+        ctx.moveTo(points[0].x, points[0].y);
+        
+        // Draw smooth curves through all points
+        for (let i = 0; i < points.length; i++) {
+            const current = points[i];
+            const next = points[(i + 1) % points.length];
+            const nextNext = points[(i + 2) % points.length];
+            
+            // Calculate control points for bezier curve with more smoothing
+            const cp1x = current.x + (next.x - points[(i - 1 + points.length) % points.length].x) / 4;
+            const cp1y = current.y + (next.y - points[(i - 1 + points.length) % points.length].y) / 4;
+            const cp2x = next.x - (nextNext.x - current.x) / 4;
+            const cp2y = next.y - (nextNext.y - current.y) / 4;
+            
+            // Draw bezier curve
+            ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, next.x, next.y);
+        }
+        
+        ctx.closePath();
+    };
+
+    // Draw single canopy as irregular polygon
+    const sides = Math.floor(48 + seededRandom(x, groundY) * 16); // 48-64 sides
+    const mainY = groundY - trunkHeight - canopyRadius;
+    
+    ctx.fillStyle = canopyColor;
+    createIrregularPolygon(
+        x,
+        mainY,
+        canopyRadius * 1.4, // Increased size
+        sides,
+        x,
+        groundY
+    );
+    ctx.fill();
+};
+
+// Main tree render function that handles both types
+const renderTree = (ctx, tree, groundY) => {
+    if (tree.config.type === 'fir') {
+        renderFirTree(ctx, tree, groundY);
+    } else if (tree.config.type === 'maple') {
+        renderMapleTree(ctx, tree, groundY);
+    }
+};
+
+const renderRock = (ctx, rock, groundY) => {
+    const { x, config } = rock;
+    const { width, height, color, highlights } = config;
+    
+    // Draw main rock shape (slightly irregular pentagon)
+    ctx.fillStyle = color;
+    ctx.beginPath();
+    ctx.moveTo(x - width/2, groundY);
+    ctx.lineTo(x - width/2 + width/6, groundY - height);
+    ctx.lineTo(x + width/3, groundY - height);
+    ctx.lineTo(x + width/2, groundY - height/2);
+    ctx.lineTo(x + width/2, groundY);
+    ctx.closePath();
+    ctx.fill();
+
+    // Add highlights
+    ctx.fillStyle = highlights;
+    ctx.beginPath();
+    ctx.moveTo(x - width/4, groundY - height);
+    ctx.lineTo(x, groundY - height);
+    ctx.lineTo(x + width/6, groundY - height/2);
+    ctx.lineTo(x - width/6, groundY - height/2);
+    ctx.closePath();
+    ctx.fill();
+};
+
+// Add grass rendering function
+const renderGrass = (ctx, grass, groundY, time) => {
+    const { x, config } = grass;
+    const { width, height, color, shadowColor, glowing } = config;
+    
+    // Initialize or get grass state
+    const stateKey = `grass_${x}`;
+    if (!window.gameState.world.grassStates[stateKey]) {
+        window.gameState.world.grassStates[stateKey] = GrassState.create(x, height);
+    }
+    const state = GrassState.validate(window.gameState.world.grassStates[stateKey]);
+    
+    // Update interaction
+    const player = window.gameState.player;
+    const distanceToPlayer = Math.abs(player.x + player.width / 2 - x);
+    GrassState.update(state, distanceToPlayer, width);
+
+    // Handle rendering
+    setupGlowEffect(ctx, glowing, color, time);
+    renderGrassBlades(ctx, state, x, width, height, groundY, time, color, shadowColor);
+    resetGlowEffect(ctx, glowing);
+};
+
+const setupGlowEffect = (ctx, glowing, color, time) => {
+    if (glowing) {
+        ctx.save();
+        ctx.shadowColor = color;
+        ctx.shadowBlur = 10 + Math.sin(time/GRASS_CONFIG.GLOW_SPEED) * 3;
+    }
+};
+
+const resetGlowEffect = (ctx, glowing) => {
+    if (glowing) {
+        ctx.restore();
+    }
+};
+
+const renderGrassBlades = (ctx, state, x, width, height, groundY, time, color, shadowColor) => {
+    const bladeWidth = width / GRASS_CONFIG.BASE_BLADE_WIDTH * 0.7;
+    
+    for (let i = 0; i < state.bladeCount; i++) {
+        renderSingleBlade(
+            ctx, i, state, x, width, height, groundY, time,
+            bladeWidth, color, shadowColor
+        );
+    }
+};
+
+const renderSingleBlade = (
+    ctx, index, state, x, width, height, groundY, time,
+    bladeWidth, color, shadowColor
+) => {
+    const centerOffset = (index - (state.bladeCount - 1) / 2) * 
+        (width * GRASS_CONFIG.SPREAD_FACTOR / state.bladeCount);
+    const bladeX = x + centerOffset;
+    
+    const rustleOffset = Math.sin(time / GRASS_CONFIG.RUSTLE_SPEED + index) * 5 * state.rustleAmount;
+    const baseWave = Math.sin(time / GRASS_CONFIG.WAVE_SPEED + index) * 2;
+    
+    drawBladeCurve(
+        ctx, bladeX, groundY, height, rustleOffset, baseWave,
+        bladeWidth, index % 2 === 0 ? color : shadowColor
+    );
+};
+
+const drawBladeCurve = (
+    ctx, bladeX, groundY, height, rustleOffset, baseWave,
+    bladeWidth, color
+) => {
+    const cp1x = bladeX + rustleOffset + baseWave;
+    const cp1y = groundY - height / 2;
+    const cp2x = bladeX + rustleOffset * 1.5 + baseWave;
+    const cp2y = groundY - height;
+    
+    ctx.fillStyle = color;
+    ctx.beginPath();
+    ctx.moveTo(bladeX, groundY);
+    ctx.quadraticCurveTo(cp1x, cp1y, cp2x, cp2y);
+    ctx.quadraticCurveTo(cp1x + bladeWidth, cp1y, bladeX + bladeWidth, groundY);
+    ctx.fill();
+};
+
+// Collision detection helper
+const checkCollision = (player, platform) => {
+    return player.x < platform.x + platform.width &&
+           player.x + player.width > platform.x &&
+           player.y < platform.y + platform.height &&
+           player.y + player.height > platform.y;
+};
+
+// World physics helper
+const handleWorldCollisions = (player, world) => {
+    let onGround = false;
+    
+    // Check ground collision first
+    const groundY = window.innerHeight - world.groundHeight;
+    if (player.y + player.height > groundY) {
+        player.y = groundY - player.height;
+        player.velocityY = 0;
+        onGround = true;
+    }
+    
+    // Then check platform collisions
+    for (const platform of world.platforms) {
+        if (checkCollision(player, platform)) {
+            // Calculate overlap on each axis
+            const overlapX = Math.min(
+                player.x + player.width - platform.x,
+                platform.x + platform.width - player.x
+            );
+            const overlapY = Math.min(
+                player.y + player.height - platform.y,
+                platform.y + platform.height - player.y
+            );
+
+            // Resolve collision on the axis with smallest overlap
+            if (overlapX < overlapY) {
+                // Horizontal collision
+                if (player.x < platform.x) {
+                    player.x = platform.x - player.width;
+                } else {
+                    player.x = platform.x + platform.width;
+                }
+                player.velocityX = 0;
+            } else {
+                // Vertical collision
+                if (player.y < platform.y) {
+                    player.y = platform.y - player.height;
+                    player.velocityY = 0;
+                    onGround = true;
+                } else {
+                    player.y = platform.y + platform.height;
+                    player.velocityY = 0;
+                }
+            }
+        }
+    }
+    
+    return { ...player, jumping: !onGround };
+};
+
+class GrassState {
+    static create(x, height) {
+        return {
+            rustleAmount: 0,
+            rustleDecay: 0.95,
+            bladeCount: utils.getBladeCount(x, height)
+        };
+    }
+
+    static validate(state) {
+        if (!state.bladeCount || state.bladeCount < GRASS_CONFIG.MIN_BLADES) {
+            state.bladeCount = GRASS_CONFIG.MIN_BLADES;
+        }
+        return state;
+    }
+
+    static update(state, distanceToPlayer, interactionDistance) {
+        if (distanceToPlayer < interactionDistance) {
+            state.rustleAmount = Math.min(1, state.rustleAmount + 0.3);
+        } else {
+            state.rustleAmount *= state.rustleDecay;
+        }
+    }
+}
+
+const renderFirTree = (ctx, tree, groundY) => {
+    const { x, config } = tree;
+    const { width, height, canopyOffset, trunkColor, canopyColor } = config;
+    
+    // Setup
+    const greenColors = NATURE_COLORS.GREENS.filter(color => color !== canopyColor);
+    ctx.lineWidth = FIR_CONFIG.FEATHER_WIDTH;
+    
+    // Render trunk
+    FirTreeRenderer.renderTrunk(
+        ctx, x, width, height - (height - canopyOffset)/2, 
+        groundY, trunkColor
+    );
+    
+    const drawFeatheredTree = (baseWidth, baseY, tipY) => {
+        const featherLength = baseWidth * 0.3;
+        
+        for (let row = 0; row < FIR_CONFIG.ROW_COUNT; row++) {
+            const t = row / (FIR_CONFIG.ROW_COUNT - 1);
+            const rowY = baseY - (baseY - tipY) * t;
+            
+            const taper = utils.calculateTaper(t);
+            const rowWidth = baseWidth * taper;
+            
+            if (rowWidth < 2) continue;
+            
+            const featherCount = Math.max(2, Math.floor(FIR_CONFIG.FEATHERS_PER_ROW * taper));
+            
+            // Render center column of needles
+            FirTreeRenderer.renderCenterNeedles(ctx, x, rowY, taper, featherLength, greenColors);
+            
+            // Render row needles
+            FirTreeRenderer.renderRowNeedles(
+                ctx, x, rowY, rowWidth, featherCount, t, 
+                taper, featherLength, greenColors
+            );
+        }
+        
+        // Render edge needles
+        FirTreeRenderer.renderEdgeNeedles(
+            ctx, x, baseWidth, baseY, tipY, featherLength, greenColors
+        );
+    };
+
+    // Draw complete tree
+    drawFeatheredTree(
+        width * 1.2,
+        groundY - canopyOffset,
+        groundY - height * 1.1
+    );
+};