about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2024-12-14 17:18:45 -0500
committerelioat <elioat@tilde.institute>2024-12-14 17:18:45 -0500
commit3d6485da59e89edc672466d168adfa5eb152c159 (patch)
treef8bf94a7b7d6b370a13ff3a96a4445e9eafc48c7
parentd03aa9e95b865a2a89f0d89d4045d16aaef55d72 (diff)
downloadtour-3d6485da59e89edc672466d168adfa5eb152c159.tar.gz
*
-rw-r--r--html/plains/game.js1046
-rw-r--r--html/plains/index.html8
2 files changed, 794 insertions, 260 deletions
diff --git a/html/plains/game.js b/html/plains/game.js
index 9a9fe4c..68a31ed 100644
--- a/html/plains/game.js
+++ b/html/plains/game.js
@@ -1,9 +1,27 @@
+// ============= Utility Functions =============
+const lerp = (start, end, t) => {
+    return start * (1 - t) + end * t;
+};
+
+const seededRandom = (x, y) => {
+    const a = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453123;
+    return a - Math.floor(a);
+};
+
+const worldToGrid = (x, y) => ({
+    x: Math.floor(x / CONFIG.display.grid.size),
+    y: Math.floor(y / CONFIG.display.grid.size)
+});
+
+// ============= Configuration =============
 const CONFIG = {
     display: {
         fps: 60,
         grid: {
             size: 100,
-            color: '#ddd'
+            color: 'rgba(221, 221, 221, 0.5)',
+            worldSize: 100,
+            voidColor: '#e6f3ff'
         },
         camera: {
             deadzoneMultiplierX: 0.6,
@@ -16,7 +34,7 @@ const CONFIG = {
             primary: '#4169E1',
             secondary: '#1E90FF',
             tertiary: '#0000CD',
-            glow: 'rgba(30, 144, 255, 0.25)',
+            glow: 'rgba(0, 128, 255, 0.5)',
             inner: '#0000CD'
         }
     },
@@ -36,9 +54,9 @@ const CONFIG = {
             exhaustedAt: 0     // Track when dash was exhausted
         },
         idle: {
-            startDelay: 2000,    // Start idle animation after 2 seconds
+            startDelay: 1500,    // Start idle animation after 1.5 seconds
             lookSpeed: 0.001,    // Speed of the looking animation
-            lookRadius: 0.3      // How far to look around (in radians)
+            lookRadius: 0.4      // How far to look around (in radians)
         }
     },
     sword: {
@@ -73,27 +91,105 @@ const CONFIG = {
         lifetime: 1000,
         spacing: 300,
         size: 5
+    },
+    world: {
+        village: {
+            size: 25,
+            groundColor: '#f2f2f2'
+        },
+        wilderness: {
+            groundColor: '#e6ffe6',
+            vegetation: {
+                tree: {
+                    frequency: 0.1,  // Chance per grid cell
+                    colors: [
+                        'rgba(100, 144, 79, 1)',
+                        'rgba(85, 128, 64, 1)',
+                        'rgba(128, 164, 98, 1)',
+                        'rgba(110, 139, 61, 1)',
+                        'rgba(95, 133, 73, 1)',
+                        'rgba(248, 239, 58, 1)'
+                    ],
+                    size: { min: 20, max: 30 }
+                },
+                mushroom: {
+                    frequency: 0.03,
+                    colors: [
+                        'rgba(242, 63, 63, 0.25)',
+                        'rgba(245, 131, 148, 0.25)',
+                        'rgba(255, 119, 65, 0.25)',
+                        'rgba(193, 97, 1, 0.5)'
+                    ],
+                    pattern: {
+                        size: 3,
+                        spacing: 10,
+                        margin: 10,
+                        variation: 0.5,
+                        offset: 0.5,
+                        singleColor: 0.7  // % chance that all dots in a cell will be the same color
+                    }
+                },
+                flower: {
+                    frequency: 0.05,
+                    colors: [
+                        'rgba(255, 105, 180, 0.3)',
+                        'rgba(221, 160, 221, 0.3)',
+                        'rgba(147, 112, 219, 0.3)'
+                    ],
+                    pattern: {
+                        size: 12,
+                        spacing: 16,
+                        rotation: Math.PI / 6,  // Base rotation of pattern
+                        margin: 10,
+                        variation: 0.2
+                    }
+                },
+                grass: {
+                    frequency: 0.12,
+                    colors: ['rgba(28, 48, 32, 0.25)'],
+                    hatch: {
+                        spacing: 8,
+                        length: 6,
+                        angle: Math.PI / 4,
+                        variation: 0.4,   // Slight randomness in angle
+                        margin: 4
+                    },
+                    spreadFactor: 0.6  // Add this for grass spreading
+                }
+            }
+        }
+    },
+    collision: {
+        enabled: true,
+        vegetation: {
+            tree: {
+                enabled: true,
+                sizeMultiplier: 1.0
+            }
+        }
     }
 };
 
-// Set references to shared colors
+
 CONFIG.sword.colors = CONFIG.effects.colors;
 CONFIG.bubble.colors = CONFIG.effects.colors;
 
+
+// ============= Global State =============
 let GAME_WIDTH = window.innerWidth;
 let GAME_HEIGHT = window.innerHeight;
-
 let lastFrameTime = 0;
 let animationTime = 0;
-
 const FRAME_TIME = 1000 / CONFIG.display.fps;
 const CAMERA_DEADZONE_X = GAME_WIDTH * CONFIG.display.camera.deadzoneMultiplierX;
 const CAMERA_DEADZONE_Y = GAME_HEIGHT * CONFIG.display.camera.deadzoneMultiplierY;
 
+
+// ============= State Management =============
 const createInitialState = () => ({
     player: {
-        x: window.innerWidth / 2,
-        y: window.innerHeight / 2,
+        x: CONFIG.player.size,  // A bit offset from the edge
+        y: CONFIG.player.size,  // A bit offset from the edge
         isDefending: false,
         direction: { x: 0, y: -1 },
         swordAngle: 0,
@@ -102,12 +198,12 @@ const createInitialState = () => ({
         bubbles: [],
         bubbleParticles: [],
         lastBubbleTime: 0,
-        dashStartTime: 0,      // When the current dash started
-        isDashing: false,      // Currently dashing?
-        dashExhausted: false,  // Is dash on cooldown?
-        lastDashEnd: 0,         // When the last dash ended
-        lastInputTime: 0,        // Track when the last input occurred
-        baseDirection: { x: 0, y: -1 }  // Store the actual facing direction
+        dashStartTime: 0, // When the current dash started
+        isDashing: false, // Currently dashing?
+        dashExhausted: false, // Is dash on cooldown?
+        lastInputTime: 0, // Track when the last input occurred
+        baseDirection: { x: 0, y: -1 },
+        lastDashEnd: 0
     },
     particles: [],
     footprints: [],
@@ -117,16 +213,71 @@ const createInitialState = () => ({
         y: 0,
         targetX: 0,
         targetY: 0
-    }
+    },
+    collisionMap: new Map()
 });
 
 let state = createInitialState();
 
-const canvas = document.getElementById('gameCanvas');
-const ctx = canvas.getContext('2d');
 
+// ============= Input Handling =============
 const keys = new Set();
 
+const handleKeyDown = (e) => {
+    keys.add(e.key);
+    
+    if (e.key === 'z' && !state.player.isDefending) {
+        Object.assign(state, inputHandlers.handleAttack(state, animationTime));
+    }
+    
+    if (e.key === 'e') {
+        Object.assign(state, inputHandlers.handleEquipmentSwitch(state));
+    }
+    
+    if (e.key === 'x') {
+        Object.assign(state, {
+            ...state,
+            player: {
+                ...state.player,
+                isDefending: true
+            }
+        });
+    }
+
+    if (e.key === 'c') {
+        const cellInfo = getCellInfo(state.player.x, state.player.y);
+        console.group('Current Cell Information:');
+        console.log(`Position: (${cellInfo.position.cellX}, ${cellInfo.position.cellY})`);
+        console.log(`Biome: ${cellInfo.biome}`);
+        console.log('Vegetation:');
+        const presentVegetation = Object.entries(cellInfo.vegetation)
+            .filter(([type, present]) => present)
+            .map(([type]) => type);
+        
+        if (presentVegetation.length === 0) {
+            console.log('none');
+        } else {
+            presentVegetation.forEach(type => console.log(type));
+        }
+        console.groupEnd();
+    }
+    
+    state.player.lastInputTime = animationTime;
+};
+
+const handleKeyUp = (e) => {
+    keys.delete(e.key);
+    if (e.key === 'x') {
+        Object.assign(state, {
+            ...state,
+            player: {
+                ...state.player,
+                isDefending: false
+            }
+        });
+    }
+};
+
 const inputHandlers = {
     handleAttack: (state, animationTime) => {
         if (state.player.isDefending) return state;
@@ -159,6 +310,185 @@ const inputHandlers = {
     }
 };
 
+
+// ============= Movement System =============
+const calculateMovement = (keys) => {
+    let dx = 0;
+    let dy = 0;
+    
+    if (keys.has('ArrowLeft')) dx -= 1;
+    if (keys.has('ArrowRight')) dx += 1;
+    if (keys.has('ArrowUp')) dy -= 1;
+    if (keys.has('ArrowDown')) dy += 1;
+    
+    if (dx === 0 && dy === 0) {
+        return { moving: false };
+    }
+    
+    // Update last input time when moving
+    state.player.lastInputTime = animationTime;
+    
+    const length = Math.sqrt(dx * dx + dy * dy);
+    const normalizedDx = dx / length;
+    const normalizedDy = dy / length;
+    
+    const isStrafing = keys.has(CONFIG.player.strafeKey);
+    
+    const newDirection = isStrafing ? 
+        { ...state.player.direction } : // strafe
+        { x: normalizedDx, y: normalizedDy }; // normal movement
+    
+    // Update base direction when not strafing
+    if (!isStrafing) {
+        state.player.baseDirection = { ...newDirection };
+    }
+    
+    return {
+        moving: true,
+        dx: normalizedDx,
+        dy: normalizedDy,
+        direction: newDirection
+    };
+};
+
+const isPositionBlocked = (x, y) => {
+    const cell = worldToGrid(x, y);
+    const key = `${cell.x},${cell.y}`;
+    if (!state.collisionMap.has(key)) return false;
+
+    const obstacle = state.collisionMap.get(key);
+    const obstacleRadius = CONFIG.player.size / 2;  // Use player size for all collision
+
+    // Distance check from center of grid cell
+    const dx = x - obstacle.x;
+    const dy = y - obstacle.y;
+    const distanceSquared = dx * dx + dy * dy;
+    
+    return distanceSquared < obstacleRadius * obstacleRadius;
+};
+
+const addToCollisionMap = (cellX, cellY, type) => {
+    const key = `${cellX},${cellY}`;
+    state.collisionMap.set(key, {
+        type,
+        x: (cellX * CONFIG.display.grid.size) + (CONFIG.display.grid.size / 2),
+        y: (cellY * CONFIG.display.grid.size) + (CONFIG.display.grid.size / 2)
+    });
+};
+
+const movementSystem = {
+    updatePosition: (state, keys) => {
+        if (state.player.isDefending) return state;
+        
+        const movement = calculateMovement(keys);
+        if (!movement.moving) {
+            // Reset dash when not moving
+            return {
+                ...state,
+                player: {
+                    ...state.player,
+                    isDashing: false,
+                    dashStartTime: 0
+                }
+            };
+        }
+        
+        const wantsToDash = keys.has('Shift');
+        const canDash = !state.player.dashExhausted && 
+            (animationTime - state.player.lastDashEnd) >= CONFIG.player.dash.cooldown;
+        
+        let isDashing = false;
+        let dashExhausted = state.player.dashExhausted;
+        let dashStartTime = state.player.dashStartTime;
+        let lastDashEnd = state.player.lastDashEnd;
+        
+        if (wantsToDash && canDash) {
+            if (!state.player.isDashing) {
+                dashStartTime = animationTime;
+            }
+            isDashing = true;
+            
+            // Check if dash duration is exhausted
+            if (animationTime - dashStartTime >= CONFIG.player.dash.duration) {
+                isDashing = false;
+                dashExhausted = true;
+                lastDashEnd = animationTime;
+            }
+        } else if (state.player.dashExhausted && 
+            (animationTime - state.player.lastDashEnd >= CONFIG.player.dash.cooldown)) {
+            dashExhausted = false;
+        }
+        
+        const speed = isDashing ? 
+            CONFIG.player.speed * CONFIG.player.sprintMultiplier : 
+            CONFIG.player.speed;
+            
+        const timeSinceLastFootprint = animationTime - state.lastFootprintTime;
+        const currentSpacing = isDashing ? 
+            CONFIG.footprints.spacing * CONFIG.player.sprintMultiplier : 
+            CONFIG.footprints.spacing;
+            
+        let newFootprints = state.footprints;
+        if (timeSinceLastFootprint > currentSpacing / speed) {
+            const offset = (Math.random() - 0.5) * 6;
+            const perpX = -movement.direction.y * offset;
+            const perpY = movement.direction.x * offset;
+            
+            newFootprints = [...state.footprints, createFootprint(
+                state.player.x + perpX,
+                state.player.y + perpY,
+                Math.atan2(movement.dy, movement.dx)
+            )];
+        }
+            
+        const worldBounds = {
+            min: 0,
+            max: CONFIG.display.grid.size * CONFIG.display.grid.worldSize
+        };
+
+        // After calculating new position, clamp it to world bounds
+        const newX = state.player.x + movement.dx * speed;
+        const newY = state.player.y + movement.dy * speed;
+
+        const clampedX = Math.max(worldBounds.min, Math.min(worldBounds.max, newX));
+        const clampedY = Math.max(worldBounds.min, Math.min(worldBounds.max, newY));
+
+        // Check for collisions at the new position
+        const playerRadius = CONFIG.player.size / 2;
+        const checkPoints = [
+            { x: newX - playerRadius, y: newY - playerRadius }, // Top-left
+            { x: newX + playerRadius, y: newY - playerRadius }, // Top-right
+            { x: newX - playerRadius, y: newY + playerRadius }, // Bottom-left
+            { x: newX + playerRadius, y: newY + playerRadius }  // Bottom-right
+        ];
+
+        const wouldCollide = checkCollision(newX, newY, playerRadius * 0.8); // Use 80% of player radius for better feel
+
+        // Only update position if there's no collision
+        const finalX = wouldCollide ? state.player.x : clampedX;
+        const finalY = wouldCollide ? state.player.y : clampedY;
+
+        return {
+            ...state,
+            player: {
+                ...state.player,
+                x: finalX,
+                y: finalY,
+                direction: movement.direction,
+                isDashing,
+                dashStartTime,
+                dashExhausted,
+                lastDashEnd
+            },
+            footprints: newFootprints,
+            lastFootprintTime: timeSinceLastFootprint > currentSpacing / speed ? 
+                animationTime : state.lastFootprintTime
+        };
+    }
+};
+
+
+// ============= Weapon Systems =============
 const updateBubble = (bubble, animationTime) => {
     const age = animationTime - bubble.createdAt;
     const ageRatio = age / CONFIG.bubble.lifetime;
@@ -275,164 +605,8 @@ const weaponSystems = {
     }
 };
 
-const movementSystem = {
-    updatePosition: (state, keys) => {
-        if (state.player.isDefending) return state;
-        
-        const movement = calculateMovement(keys);
-        if (!movement.moving) {
-            // Reset dash when not moving
-            return {
-                ...state,
-                player: {
-                    ...state.player,
-                    isDashing: false,
-                    dashStartTime: 0
-                }
-            };
-        }
-        
-        const wantsToDash = keys.has('Shift');
-        const canDash = !state.player.dashExhausted && 
-            (animationTime - state.player.lastDashEnd) >= CONFIG.player.dash.cooldown;
-        
-        let isDashing = false;
-        let dashExhausted = state.player.dashExhausted;
-        let dashStartTime = state.player.dashStartTime;
-        let lastDashEnd = state.player.lastDashEnd;
-        
-        if (wantsToDash && canDash) {
-            if (!state.player.isDashing) {
-                dashStartTime = animationTime;
-            }
-            isDashing = true;
-            
-            // Check if dash duration exceeded
-            if (animationTime - dashStartTime >= CONFIG.player.dash.duration) {
-                isDashing = false;
-                dashExhausted = true;
-                lastDashEnd = animationTime;
-            }
-        } else if (state.player.dashExhausted && 
-            (animationTime - state.player.lastDashEnd >= CONFIG.player.dash.cooldown)) {
-            dashExhausted = false;
-        }
-        
-        const speed = isDashing ? 
-            CONFIG.player.speed * CONFIG.player.sprintMultiplier : 
-            CONFIG.player.speed;
-            
-        // Handle footprint creation
-        const timeSinceLastFootprint = animationTime - state.lastFootprintTime;
-        const currentSpacing = isDashing ? 
-            CONFIG.footprints.spacing * CONFIG.player.sprintMultiplier : 
-            CONFIG.footprints.spacing;
-            
-        let newFootprints = state.footprints;
-        if (timeSinceLastFootprint > currentSpacing / speed) {
-            const offset = (Math.random() - 0.5) * 6;
-            const perpX = -movement.direction.y * offset;
-            const perpY = movement.direction.x * offset;
-            
-            newFootprints = [...state.footprints, createFootprint(
-                state.player.x + perpX,
-                state.player.y + perpY,
-                Math.atan2(movement.dy, movement.dx)
-            )];
-        }
-            
-        return {
-            ...state,
-            player: {
-                ...state.player,
-                x: state.player.x + movement.dx * speed,
-                y: state.player.y + movement.dy * speed,
-                direction: movement.direction,
-                isDashing,
-                dashStartTime,
-                dashExhausted,
-                lastDashEnd
-            },
-            footprints: newFootprints,
-            lastFootprintTime: timeSinceLastFootprint > currentSpacing / speed ? 
-                animationTime : state.lastFootprintTime
-        };
-    }
-};
-
-const handleKeyDown = (e) => {
-    keys.add(e.key);
-    
-    if (e.key === 'z' && !state.player.isDefending) {
-        Object.assign(state, inputHandlers.handleAttack(state, animationTime));
-    }
-    
-    if (e.key === 'e') {
-        Object.assign(state, inputHandlers.handleEquipmentSwitch(state));
-    }
-    
-    if (e.key === 'x') {
-        Object.assign(state, {
-            ...state,
-            player: {
-                ...state.player,
-                isDefending: true
-            }
-        });
-    }
-    
-    // Update last input time for any key press
-    state.player.lastInputTime = animationTime;
-};
-
-const handleKeyUp = (e) => {
-    keys.delete(e.key);
-    if (e.key === 'x') {
-        Object.assign(state, {
-            ...state,
-            player: {
-                ...state.player,
-                isDefending: false
-            }
-        });
-    }
-};
-
-const updatePlayer = () => {
-    Object.assign(state, weaponSystems.updateBubbles(state, animationTime));
-    Object.assign(state, weaponSystems.updateSwordSwing(state, animationTime));
-    Object.assign(state, movementSystem.updatePosition(state, keys));
-    
-    state.footprints = state.footprints.filter(footprint => {
-        return (animationTime - footprint.createdAt) < CONFIG.footprints.lifetime;
-    });
-    
-    // Update player direction for idle animation
-    if (!keys.size && !state.player.isSwinging && !state.player.isDefending) {
-        const idleTime = animationTime - state.player.lastInputTime;
-        
-        if (idleTime > CONFIG.player.idle.startDelay) {
-            // Calculate looking direction using sine waves
-            const lookAngle = Math.sin(animationTime * CONFIG.player.idle.lookSpeed) * CONFIG.player.idle.lookRadius;
-            const baseAngle = Math.atan2(state.player.baseDirection.y, state.player.baseDirection.x);
-            const newAngle = baseAngle + lookAngle;
-            
-            state.player.direction = {
-                x: Math.cos(newAngle),
-                y: Math.sin(newAngle)
-            };
-        } else {
-            // Reset direction to base direction when not idle
-            state.player.direction = { ...state.player.baseDirection };
-        }
-    } else {
-        // Update last input time when other actions occur
-        if (state.player.isSwinging || state.player.isDefending) {
-            state.player.lastInputTime = animationTime;
-        }
-    }
-};
 
+// ============= Particle Systems =============
 const createParticle = (x, y, angle) => ({
     x,
     y,
@@ -452,28 +626,26 @@ const createFootprint = (x, y, direction) => ({
     offset: (Math.random() - 0.5) * 5
 });
 
+
+// ============= Rendering System =============
 const renderPlayer = () => {
     ctx.save();
     
-    // Render bubble particles
     state.player.bubbleParticles.forEach(particle => {
         const age = (animationTime - particle.createdAt) / CONFIG.bubbleParticle.lifetime;
         const alpha = (1 - age) * 0.8;
         
-        // Draw outer glow
-        ctx.fillStyle = `rgba(65, 105, 225, ${alpha * 0.3})`; // Royal Blue
+        ctx.fillStyle = CONFIG.effects.colors.glow.replace('0.25)', `${alpha * 0.3})`); // Outer glow
         ctx.beginPath();
         ctx.arc(particle.x, particle.y, particle.size * 2.5, 0, Math.PI * 2);
         ctx.fill();
         
-        // Draw core
-        ctx.fillStyle = `rgba(30, 144, 255, ${alpha})`; // Dodger Blue
+        ctx.fillStyle = CONFIG.effects.colors.secondary.replace('rgb', 'rgba').replace(')', `, ${alpha})`); // Core
         ctx.beginPath();
         ctx.arc(particle.x, particle.y, particle.size * 1.5, 0, Math.PI * 2);
         ctx.fill();
     });
     
-    // Render bubbles with adjusted fade
     state.player.bubbles.forEach(bubble => {
         const age = (animationTime - bubble.createdAt) / CONFIG.bubble.lifetime;
         const alpha = Math.pow(1 - age, CONFIG.bubble.fadeExponent);
@@ -483,7 +655,6 @@ const renderPlayer = () => {
         ctx.translate(bubble.x, bubble.y);
         ctx.rotate(bubble.angle);
         
-        // Draw outer glow with adjusted fade
         ctx.beginPath();
         ctx.arc(0, 0, expandedSize * 1.5, 
             -CONFIG.bubble.arcWidth * (1 + age * 0.5),
@@ -495,14 +666,13 @@ const renderPlayer = () => {
         ctx.strokeStyle = CONFIG.bubble.colors.glow.replace(')', `, ${alpha * 0.5})`);
         ctx.stroke();
         
-        // Draw main arc with gradient
         const gradient = ctx.createLinearGradient(
             -expandedSize, 0,
             expandedSize, 0
         );
-        gradient.addColorStop(0, `rgba(65, 105, 225, ${alpha})`);
-        gradient.addColorStop(0.6, `rgba(30, 144, 255, ${alpha})`);
-        gradient.addColorStop(1, `rgba(0, 0, 205, ${alpha})`);
+        gradient.addColorStop(0, CONFIG.bubble.colors.primary.replace('rgb', 'rgba').replace(')', `, ${alpha})`));
+        gradient.addColorStop(0.6, CONFIG.bubble.colors.secondary.replace('rgb', 'rgba').replace(')', `, ${alpha})`));
+        gradient.addColorStop(1, CONFIG.bubble.colors.tertiary.replace('rgb', 'rgba').replace(')', `, ${alpha})`));
         
         ctx.beginPath();
         ctx.arc(0, 0, expandedSize, 
@@ -514,7 +684,6 @@ const renderPlayer = () => {
         ctx.strokeStyle = gradient;
         ctx.stroke();
         
-        // Draw inner bright line
         ctx.beginPath();
         ctx.arc(0, 0, expandedSize * 0.9, 
             -CONFIG.bubble.arcWidth * 0.6 * (1 + age * 0.5),
@@ -628,7 +797,6 @@ const renderPlayer = () => {
         
         const numParticles = CONFIG.defense.particleCount;
         const baseOrbitRadius = CONFIG.player.size * CONFIG.defense.orbitRadiusMultiplier;
-        
         const rotationSpeed = CONFIG.defense.rotationSpeed;
         
         for (let i = 0; i < numParticles; i++) {
@@ -666,14 +834,14 @@ const renderPlayer = () => {
             }
         }
         
-        // Add direction indicator square in the center
+        // Draw the eyeball...square
         const dotSize = CONFIG.player.directionIndicator.size;
         
-        // Calculate cooldown progress (0 to 1)
+        // Calculate cooldown progress
         const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
-        const cooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
+        // const cooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
         
-        // Set opacity based on cooldown (0.1 to 1)
+        // Set opacity based on cooldown's progress
         const dotOpacity = getDotOpacity(state, animationTime);
         
         ctx.fillStyle = CONFIG.player.directionIndicator.color.replace(
@@ -703,11 +871,10 @@ const renderPlayer = () => {
         const dotX = state.player.x + state.player.direction.x * dotDistance;
         const dotY = state.player.y + state.player.direction.y * dotDistance;
         
-        // Calculate cooldown progress (0 to 1)
         const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
-        const cooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
+        // const cooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
         
-        // Set opacity based on cooldown (0.1 to 1)
+        // Set opacity based on cooldown's progress
         const dotOpacity = getDotOpacity(state, animationTime);
         
         ctx.fillStyle = CONFIG.player.directionIndicator.color.replace(
@@ -726,10 +893,6 @@ const renderPlayer = () => {
     ctx.restore();
 };
 
-const lerp = (start, end, t) => {
-    return start * (1 - t) + end * t;
-};
-
 const render = () => {
     ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
     
@@ -755,30 +918,273 @@ const render = () => {
     
     ctx.save();
     ctx.translate(state.camera.x, state.camera.y);
-    
+
     const gridSize = CONFIG.display.grid.size;
-    ctx.strokeStyle = CONFIG.display.grid.color;
-    ctx.lineWidth = 1;
+    const worldSize = gridSize * CONFIG.display.grid.worldSize;
+    const villageSize = CONFIG.world.village.size * gridSize;
     
+    // Calculate visible area
     const startX = Math.floor((-state.camera.x) / gridSize) * gridSize;
     const startY = Math.floor((-state.camera.y) / gridSize) * gridSize;
     const endX = startX + GAME_WIDTH + gridSize;
     const endY = startY + GAME_HEIGHT + gridSize;
     
-    for (let x = startX; x < endX; x += gridSize) {
+    // Draw void background
+    ctx.fillStyle = CONFIG.display.grid.voidColor;
+    ctx.fillRect(
+        startX, startY,
+        endX - startX, endY - startY
+    );
+
+    // First draw the wilderness ground for the whole world
+    ctx.fillStyle = CONFIG.world.wilderness.groundColor;
+    ctx.fillRect(0, 0, worldSize, worldSize);
+    
+    // Then draw the village ground in the top-left
+    ctx.fillStyle = CONFIG.world.village.groundColor;
+    ctx.fillRect(0, 0, villageSize, villageSize);
+    
+    // After drawing village and wilderness grounds, but before grid:
+    
+    // The shore gradient
+    const shoreWidth = 60;
+    const shoreColor = 'rgba(179, 220, 255, 0.3)';
+    
+
+    // FIXME: There is likely a way to do this all at once, but this was easy
+    // Top shore
+    const topShore = ctx.createLinearGradient(0, 0, 0, shoreWidth);
+    topShore.addColorStop(0, shoreColor);
+    topShore.addColorStop(1, 'rgba(255, 255, 255, 0)');
+    ctx.fillStyle = topShore;
+    ctx.fillRect(0, 0, worldSize, shoreWidth);
+    
+    // Bottom shore
+    const bottomShore = ctx.createLinearGradient(0, worldSize - shoreWidth, 0, worldSize);
+    bottomShore.addColorStop(0, 'rgba(255, 255, 255, 0)');
+    bottomShore.addColorStop(1, shoreColor);
+    ctx.fillStyle = bottomShore;
+    ctx.fillRect(0, worldSize - shoreWidth, worldSize, shoreWidth);
+    
+    // Left shore
+    const leftShore = ctx.createLinearGradient(0, 0, shoreWidth, 0);
+    leftShore.addColorStop(0, shoreColor);
+    leftShore.addColorStop(1, 'rgba(255, 255, 255, 0)');
+    ctx.fillStyle = leftShore;
+    ctx.fillRect(0, 0, shoreWidth, worldSize);
+    
+    // Right shore
+    const rightShore = ctx.createLinearGradient(worldSize - shoreWidth, 0, worldSize, 0);
+    rightShore.addColorStop(0, 'rgba(255, 255, 255, 0)');
+    rightShore.addColorStop(1, shoreColor);
+    ctx.fillStyle = rightShore;
+    ctx.fillRect(worldSize - shoreWidth, 0, shoreWidth, worldSize);
+
+    // Draw grid inside of the world
+    ctx.strokeStyle = CONFIG.display.grid.color;
+    ctx.lineWidth = 1;
+
+    // Draw vertical lines
+    for (let x = 0; x < worldSize; x += gridSize) {
         ctx.beginPath();
-        ctx.moveTo(x, startY);
-        ctx.lineTo(x, endY);
+        ctx.moveTo(x, 0);
+        ctx.lineTo(x, worldSize);
         ctx.stroke();
     }
-    
-    for (let y = startY; y < endY; y += gridSize) {
+
+    // Draw horizontal lines
+    for (let y = 0; y < worldSize; y += gridSize) {
         ctx.beginPath();
-        ctx.moveTo(startX, y);
-        ctx.lineTo(endX, y);
+        ctx.moveTo(0, y);
+        ctx.lineTo(worldSize, y);
         ctx.stroke();
     }
-    
+
+    // Draw vegetation in the wilderness 
+    for (let x = startX; x < endX; x += gridSize) {
+        for (let y = startY; y < endY; y += gridSize) {
+            if (x >= worldSize || y >= worldSize) continue;
+            if (x < villageSize && y < villageSize) continue;
+            
+            const cellX = Math.floor(x / gridSize);
+            const cellY = Math.floor(y / gridSize);
+            
+            if (cellX < 0 || cellY < 0 || cellX >= CONFIG.display.grid.worldSize || cellY >= CONFIG.display.grid.worldSize) continue;
+            
+            const random = seededRandom(cellX, cellY);
+            
+            // Trees
+            if (random < CONFIG.world.wilderness.vegetation.tree.frequency) {
+                const size = CONFIG.world.wilderness.vegetation.tree.size;
+                const treeSize = size.min + seededRandom(cellX * 2, cellY * 2) * (size.max - size.min);
+                
+                // Add tree to collision map
+                if (CONFIG.collision.enabled && CONFIG.collision.vegetation.tree.enabled) {
+                    addToCollisionMap(cellX, cellY, 'tree');
+                }
+                
+                // Generate number of sides for this tree
+                const sides = Math.floor(10 + seededRandom(cellX * 3, cellY * 3) * 13); // 10 to 22 sides
+                
+                // Choose color for this tree
+                const colorIndex = Math.floor(seededRandom(cellX * 4, cellY * 4) * CONFIG.world.wilderness.vegetation.tree.colors.length);
+                ctx.fillStyle = CONFIG.world.wilderness.vegetation.tree.colors[colorIndex];
+                
+                ctx.beginPath();
+                
+                for (let i = 0; i < sides; i++) {
+                    const angle = (i / sides) * Math.PI * 2;
+
+                    const radiusVariation = 0.8 + seededRandom(cellX * i, cellY * i) * 0.4;
+                    const pointRadius = treeSize * radiusVariation;
+                    
+                    const px = x + gridSize/2 + Math.cos(angle) * pointRadius;
+                    const py = y + gridSize/2 + Math.sin(angle) * pointRadius;
+                    
+                    if (i === 0) {
+                        ctx.moveTo(px, py);
+                    } else {
+                        ctx.lineTo(px, py);
+                    }
+                }
+                
+                ctx.closePath();
+                ctx.fill();
+            }
+
+            // Mushrooms
+            else if (random < CONFIG.world.wilderness.vegetation.tree.frequency + 
+                     CONFIG.world.wilderness.vegetation.mushroom.frequency) {
+                const config = CONFIG.world.wilderness.vegetation.mushroom;
+                
+                // Determine if cell uses single color
+                const useSingleColor = seededRandom(cellX * 31, cellY * 31) < config.pattern.singleColor;
+                const cellColorIndex = Math.floor(seededRandom(cellX * 13, cellY * 13) * config.colors.length);
+                
+                // Create regular grid of circles with slight variation
+                for (let i = config.pattern.margin; i < gridSize - config.pattern.margin; i += config.pattern.spacing) {
+                    for (let j = config.pattern.margin; j < gridSize - config.pattern.margin; j += config.pattern.spacing) {
+                        // Offset every other row for more natural pattern
+                        const offsetX = (Math.floor(j / config.pattern.spacing) % 2) * 
+                            (config.pattern.spacing * config.pattern.offset);
+                        const px = x + i + offsetX;
+                        const py = y + j;
+                        
+                        // Add variation to position
+                        const variation = {
+                            x: (seededRandom(cellX * i, cellY * j) - 0.5) * config.pattern.variation * config.pattern.spacing,
+                            y: (seededRandom(cellX * j, cellY * i) - 0.5) * config.pattern.variation * config.pattern.spacing
+                        };
+                        
+                        // Choose color for this dot
+                        const colorIndex = useSingleColor ? cellColorIndex :
+                            Math.floor(seededRandom(cellX * i * j, cellY * i * j) * config.colors.length);
+                        ctx.fillStyle = config.colors[colorIndex];
+                        
+                        ctx.beginPath();
+                        ctx.arc(
+                            px + variation.x,
+                            py + variation.y,
+                            config.pattern.size,
+                            0, Math.PI * 2
+                        );
+                        ctx.fill();
+                    }
+                }
+            }
+            // Flowers
+            else if (random < CONFIG.world.wilderness.vegetation.tree.frequency + 
+                     CONFIG.world.wilderness.vegetation.mushroom.frequency +
+                     CONFIG.world.wilderness.vegetation.flower.frequency) {
+                const config = CONFIG.world.wilderness.vegetation.flower;
+                
+                // Determine base color for this cell
+                const colorIndex = Math.floor(seededRandom(cellX * 13, cellY * 13) * config.colors.length);
+                ctx.fillStyle = config.colors[colorIndex];
+                
+                // Calculate base rotation for this cell
+                const baseRotation = config.pattern.rotation + 
+                    (seededRandom(cellX * 14, cellY * 14) - 0.5) * config.pattern.variation;
+                
+                // Draw tessellating triangle pattern
+                for (let i = config.pattern.margin; i < gridSize - config.pattern.margin; i += config.pattern.spacing) {
+                    for (let j = config.pattern.margin; j < gridSize - config.pattern.margin; j += config.pattern.spacing) {
+                        // Offset every other row
+                        const offsetX = (Math.floor(j / config.pattern.spacing) % 2) * (config.pattern.spacing / 2);
+                        const px = x + i + offsetX;
+                        const py = y + j;
+                        
+                        // Add slight position variation
+                        const variation = {
+                            x: (seededRandom(cellX * i, cellY * j) - 0.5) * 4,
+                            y: (seededRandom(cellX * j, cellY * i) - 0.5) * 4
+                        };
+                        
+                        // Draw triangle
+                        ctx.beginPath();
+                        ctx.save();
+                        ctx.translate(px + variation.x, py + variation.y);
+                        ctx.rotate(baseRotation + (seededRandom(cellX * i, cellY * j) - 0.5) * 0.5);
+                        
+                        const size = config.pattern.size * (0.8 + seededRandom(cellX * i, cellY * j) * 0.4);
+                        ctx.moveTo(-size/2, size/2);
+                        ctx.lineTo(size/2, size/2);
+                        ctx.lineTo(0, -size/2);
+                        ctx.closePath();
+                        
+                        ctx.fill();
+                        ctx.restore();
+                    }
+                }
+            }
+            // Grass
+            else if (random < CONFIG.world.wilderness.vegetation.tree.frequency + 
+                     CONFIG.world.wilderness.vegetation.mushroom.frequency +
+                     CONFIG.world.wilderness.vegetation.flower.frequency +
+                     CONFIG.world.wilderness.vegetation.grass.frequency ||
+                     (seededRandom(cellX * 50, cellY * 50) < 
+                      CONFIG.world.wilderness.vegetation.grass.spreadFactor && 
+                      hasAdjacentGrass(cellX, cellY))) {
+                
+                const config = CONFIG.world.wilderness.vegetation.grass;
+                
+                // Draw hatching pattern
+                ctx.strokeStyle = config.colors[0];
+                ctx.lineWidth = 1;
+
+                // Calculate base angle with slight variation
+                const baseAngle = config.hatch.angle + 
+                    (seededRandom(cellX * 20, cellY * 20) - 0.5) * config.hatch.variation;
+
+                // Create hatching pattern
+                for (let i = config.hatch.margin; i < gridSize - config.hatch.margin; i += config.hatch.spacing) {
+                    for (let j = config.hatch.margin; j < gridSize - config.hatch.margin; j += config.hatch.spacing) {
+                        const hatchX = x + i;
+                        const hatchY = y + j;
+                        
+                        // Add slight position variation
+                        const offsetX = (seededRandom(cellX * i, cellY * j) - 0.5) * 2;
+                        const offsetY = (seededRandom(cellX * j, cellY * i) - 0.5) * 2;
+                        
+                        ctx.beginPath();
+                        ctx.moveTo(
+                            hatchX + offsetX, 
+                            hatchY + offsetY
+                        );
+                        ctx.lineTo(
+                            hatchX + Math.cos(baseAngle) * config.hatch.length + offsetX,
+                            hatchY + Math.sin(baseAngle) * config.hatch.length + offsetY
+                        );
+                        ctx.stroke();
+                    }
+                }
+            }
+        }
+    }
+
+    // Draw player
+    renderPlayer();
+
     state.footprints.forEach(footprint => {
         const age = (animationTime - footprint.createdAt) / CONFIG.footprints.lifetime;
         if (age >= 1) return;
@@ -805,11 +1211,45 @@ const render = () => {
         ctx.restore();
     });
     
-    renderPlayer();
-    
     ctx.restore();
 };
 
+
+// ============= Game Loop =============
+const updatePlayer = () => {
+    Object.assign(state, weaponSystems.updateBubbles(state, animationTime));
+    Object.assign(state, weaponSystems.updateSwordSwing(state, animationTime));
+    Object.assign(state, movementSystem.updatePosition(state, keys));
+    
+    state.footprints = state.footprints.filter(footprint => {
+        return (animationTime - footprint.createdAt) < CONFIG.footprints.lifetime;
+    });
+    
+    // Update player direction for idle animation
+    if (!keys.size && !state.player.isSwinging && !state.player.isDefending) {
+        const idleTime = animationTime - state.player.lastInputTime;
+        
+        if (idleTime > CONFIG.player.idle.startDelay) {
+            const lookAngle = Math.sin(animationTime * CONFIG.player.idle.lookSpeed) * CONFIG.player.idle.lookRadius;
+            const baseAngle = Math.atan2(state.player.baseDirection.y, state.player.baseDirection.x);
+            const newAngle = baseAngle + lookAngle;
+            
+            state.player.direction = {
+                x: Math.cos(newAngle),
+                y: Math.sin(newAngle)
+            };
+        } else {
+            // Reset direction to base direction when not idle
+            state.player.direction = { ...state.player.baseDirection };
+        }
+    } else {
+        // Update last input time when other actions occur
+        if (state.player.isSwinging || state.player.isDefending) {
+            state.player.lastInputTime = animationTime;
+        }
+    }
+};
+
 const gameLoop = (currentTime) => {
     if (!lastFrameTime) {
         lastFrameTime = currentTime;
@@ -830,8 +1270,10 @@ const gameLoop = (currentTime) => {
     requestAnimationFrame(gameLoop);
 };
 
-window.addEventListener('keydown', handleKeyDown);
-window.addEventListener('keyup', handleKeyUp);
+
+// ============= Setup & Initialization =============
+const canvas = document.getElementById('gameCanvas');
+const ctx = canvas.getContext('2d');
 
 const resizeCanvas = () => {
     GAME_WIDTH = window.innerWidth;
@@ -844,51 +1286,13 @@ const resizeCanvas = () => {
     }
 };
 
+window.addEventListener('keydown', handleKeyDown);
+window.addEventListener('keyup', handleKeyUp);
 window.addEventListener('resize', resizeCanvas);
 
 resizeCanvas();
-
 requestAnimationFrame(gameLoop);
 
-const calculateMovement = (keys) => {
-    let dx = 0;
-    let dy = 0;
-    
-    if (keys.has('ArrowLeft')) dx -= 1;
-    if (keys.has('ArrowRight')) dx += 1;
-    if (keys.has('ArrowUp')) dy -= 1;
-    if (keys.has('ArrowDown')) dy += 1;
-    
-    if (dx === 0 && dy === 0) {
-        return { moving: false };
-    }
-    
-    // Update last input time when moving
-    state.player.lastInputTime = animationTime;
-    
-    const length = Math.sqrt(dx * dx + dy * dy);
-    const normalizedDx = dx / length;
-    const normalizedDy = dy / length;
-    
-    const isStrafing = keys.has(CONFIG.player.strafeKey);
-    
-    const newDirection = isStrafing ? 
-        { ...state.player.direction } : // Keep current direction while strafing
-        { x: normalizedDx, y: normalizedDy }; // Update direction normally
-    
-    // Update base direction when not strafing
-    if (!isStrafing) {
-        state.player.baseDirection = { ...newDirection };
-    }
-    
-    return {
-        moving: true,
-        dx: normalizedDx,
-        dy: normalizedDy,
-        direction: newDirection
-    };
-};
-
 const getDotOpacity = (state, animationTime) => {
     // Get bubble cooldown opacity
     const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
@@ -906,18 +1310,144 @@ const getDotOpacity = (state, animationTime) => {
         dashOpacity = 1 - (dashProgress * 0.7);  // Fade to 0.3 during dash
     }
 
-    // Add blinking effect when moving or defending
     let blinkOpacity = 1;
     if (state.player.isDefending) {
-        // Create a slow pulse using sin wave (period of about 1.5 seconds)
         const blinkPhase = Math.sin(animationTime * 0.002);
-        // Only blink occasionally (when sin wave is above 0.7)
         if (blinkPhase > 0.7) {
-            // Quick fade out and in
             blinkOpacity = 0.3 + (blinkPhase - 0.7) * 2;
         }
     }
     
     // Return the lowest opacity of all systems
     return Math.min(bubbleOpacity, dashOpacity, blinkOpacity);
+};
+
+const checkCollision = (x, y, size) => {
+    // Check corners of player square
+    const halfSize = size / 2;
+    const corners = [
+        { x: x - halfSize, y: y - halfSize }, // Top-left
+        { x: x + halfSize, y: y - halfSize }, // Top-right
+        { x: x - halfSize, y: y + halfSize }, // Bottom-left
+        { x: x + halfSize, y: y + halfSize }  // Bottom-right
+    ];
+
+    return corners.some(corner => isPositionBlocked(corner.x, corner.y));
+};
+
+const createNaturalCluster = (centerX, centerY, config, cellX, cellY, i) => {
+    // Base angle and distance
+    const angle = seededRandom(cellX * 8 + i, cellY * 8) * Math.PI * 2;
+    
+    // Make clusters denser in the middle and sparser on edges
+    const distanceFromCenter = seededRandom(cellX * 9 + i, cellY * 9);
+    // Use square root to bias towards center
+    const distance = Math.sqrt(distanceFromCenter) * config.size.max;
+    
+    // Add some variation
+    const variation = {
+        x: (seededRandom(cellX * 10 + i, cellY * 10) - 0.5) * config.size.max,
+        y: (seededRandom(cellX * 11 + i, cellY * 11) - 0.5) * config.size.max
+    };
+
+    return {
+        x: centerX + Math.cos(angle) * distance + variation.x,
+        y: centerY + Math.sin(angle) * distance + variation.y,
+        size: config.size.min + 
+            seededRandom(cellX * 3 + i, cellY * 3) * 
+            (config.size.max - config.size.min) *
+            // Make items on the edge slightly smaller
+            (1 - (distance / config.cluster.spread) * 0.3)
+    };
+};
+
+const renderTree = (ctx, x, y, size, isTop = false) => {
+    ctx.fillStyle = CONFIG.world.wilderness.vegetation.tree.color;
+    if (isTop) {
+        // Draw only the top 2/3 of the tree
+        ctx.beginPath();
+        ctx.arc(
+            x + CONFIG.display.grid.size/2, 
+            y + CONFIG.display.grid.size/2, 
+            size,
+            -Math.PI, 0
+        );
+        ctx.fill();
+    } else {
+        // Draw only the bottom 2/3 of the tree
+        ctx.beginPath();
+        ctx.arc(
+            x + CONFIG.display.grid.size/2, 
+            y + CONFIG.display.grid.size/2, 
+            size,
+            0, Math.PI
+        );
+        ctx.fill();
+    }
+};
+
+const hasAdjacentGrass = (cellX, cellY) => {
+    const adjacentCells = [
+        [-1, -1], [0, -1], [1, -1],
+        [-1,  0],          [1,  0],
+        [-1,  1], [0,  1], [1,  1]
+    ];
+    
+    return adjacentCells.some(([dx, dy]) => {
+        const adjX = cellX + dx;
+        const adjY = cellY + dy;
+        const adjRandom = seededRandom(adjX, adjY);
+        
+        return adjRandom < CONFIG.world.wilderness.vegetation.grass.frequency;
+    });
+};
+
+const getCellInfo = (x, y) => {
+    const cellX = Math.floor(x / CONFIG.display.grid.size);
+    const cellY = Math.floor(y / CONFIG.display.grid.size);
+    const random = seededRandom(cellX, cellY);
+    
+    // Determine biome
+    const isInVillage = x < (CONFIG.world.village.size * CONFIG.display.grid.size) && 
+                       y < (CONFIG.world.village.size * CONFIG.display.grid.size);
+    
+    // If in village, return early with no vegetation
+    if (isInVillage) {
+        return {
+            position: { cellX, cellY },
+            biome: 'Village',
+            vegetation: {
+                tree: false,
+                mushrooms: false,
+                flowers: false,
+                grass: false
+            }
+        };
+    }
+    
+    // Check for vegetation only if in wilderness
+    const hasTree = random < CONFIG.world.wilderness.vegetation.tree.frequency;
+    const hasMushrooms = random < (CONFIG.world.wilderness.vegetation.tree.frequency + 
+                                 CONFIG.world.wilderness.vegetation.mushroom.frequency);
+    const hasFlowers = random < (CONFIG.world.wilderness.vegetation.tree.frequency + 
+                                CONFIG.world.wilderness.vegetation.mushroom.frequency +
+                                CONFIG.world.wilderness.vegetation.flower.frequency);
+    const hasGrass = random < (CONFIG.world.wilderness.vegetation.tree.frequency + 
+                              CONFIG.world.wilderness.vegetation.mushroom.frequency +
+                              CONFIG.world.wilderness.vegetation.flower.frequency +
+                              CONFIG.world.wilderness.vegetation.grass.frequency) ||
+                     (seededRandom(cellX * 50, cellY * 50) < 
+                      CONFIG.world.wilderness.vegetation.grass.spreadFactor && 
+                      hasAdjacentGrass(cellX, cellY));
+
+    return {
+        position: { cellX, cellY },
+        biome: 'Wilderness',
+        vegetation: {
+            tree: hasTree,
+            mushrooms: !hasTree && hasMushrooms,
+            flowers: !hasTree && !hasMushrooms && hasFlowers,
+            grass: !hasTree && !hasMushrooms && !hasFlowers && hasGrass
+        }
+    };
 };
\ No newline at end of file
diff --git a/html/plains/index.html b/html/plains/index.html
index 508e7e0..492e93f 100644
--- a/html/plains/index.html
+++ b/html/plains/index.html
@@ -1,7 +1,10 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <title>Top-down Adventure</title>
+    <title>Plains</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="Little black square wanted to be a fucking Jedi.">
     <style>
         * {
             margin: 0;
@@ -23,4 +26,5 @@
     <canvas id="gameCanvas"></canvas>
     <script src="game.js"></script>
 </body>
-</html>
\ No newline at end of file
+</html>
+