about summary refs log blame commit diff stats
path: root/html/plains/game.js
blob: 68a31ed328347ee5449bdc4d197e1484d8208caf (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16















                                                                 
                



                      


                                              











                                     
                                           


                            


                 
                            









                                                               

               
                                                                          
                                                                  
                                                                       
         



                        















                              








                                   



                       











































































                                                                                                    
     

  
 


                                             

                                           

                                     

                      


                                                                                  
 

                                               
                                   
             

                                                             


                                   




                            





                                                               








                         

                           


                                 
 
 
                                             

                       






















































                                                                                          




























                                                                                                            


     
 



                                              
    






                                        

     

                                               
    


                                                
    
                                                         
    







                                                                

            



                               


      



                                                   
 

















                                                                                        




                                                   
        










                                                 

         














                                                                                      
                                                  







                                                                                        
         




                                                                   

















                                                                               


























                                                                                                                        



                                

                          












                                                                                 





                                                        
    









                                                            
    

                                                                           
     
    


































































                                                                                                                       


                                




                                                                                          
             

          
 






                                                                                                                      


                                

                                          
             
          


     
 
                                               



                                          



                                                                                                                            

   
                                               







                                                               

                                               


                            



                                                                                          
                                                                                                     



                                                                             
                                                                                                                   




                                                                             








                                                                                










                                                                                      



                                                  


                                                                                                                      










                                                             













                                                                        

















                                                                                                       





































































                                                                                                           
                          









                                                                                            


                                                           
































                                                                                                                 
        
                                    

                                                              
                                      
                                                                                
                                                                                              
        
                                                   












                                                                       
            
                             






                                                    






                                                                             
                                                                                
                                                                                              
        
                                                   












                                                                          




                  



















                                                                          

                                                                                            


                                                  
 
                                              

                                                               
    
                             




                                                                       
























































                                                                                          
                        

                                 

                     


                                                   
                        

                                 

                     

























































































































































































                                                                                                                                          

























                                                                                       


                  



































                                                                                                                     



















                                                  



                                                     











                                         

                                                  


                                                

                                















                                                                                                        
 

                                   
                                                           
                               


                                                        
    

                                                              

































































































































                                                                                        
 
// ============= 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: 'rgba(221, 221, 221, 0.5)',
            worldSize: 100,
            voidColor: '#e6f3ff'
        },
        camera: {
            deadzoneMultiplierX: 0.6,
            deadzoneMultiplierY: 0.6,
            ease: 0.08
        }
    },
    effects: {
        colors: {
            primary: '#4169E1',
            secondary: '#1E90FF',
            tertiary: '#0000CD',
            glow: 'rgba(0, 128, 255, 0.5)',
            inner: '#0000CD'
        }
    },
    player: {
        size: 30,
        speed: 5,
        sprintMultiplier: 2,
        color: '#111',
        strafeKey: ' ',
        directionIndicator: {
            size: 10,
            color: 'rgba(32, 178, 170, 1)'
        },
        dash: {
            duration: 3000,    // 3 seconds of use
            cooldown: 1000,    // 1 second cooldown
            exhaustedAt: 0     // Track when dash was exhausted
        },
        idle: {
            startDelay: 1500,    // Start idle animation after 1.5 seconds
            lookSpeed: 0.001,    // Speed of the looking animation
            lookRadius: 0.4      // How far to look around (in radians)
        }
    },
    sword: {
        length: 60,
        swingSpeed: 0.6,
        colors: null
    },
    bubble: {
        size: 20,
        speed: 8,
        lifetime: 800,
        cooldown: 1000,
        arcWidth: Math.PI / 3,
        colors: null,
        particleEmitRate: 0.3,
        fadeExponent: 2.5
    },
    bubbleParticle: {
        lifetime: 700,
        speedMultiplier: 0.3,
        size: 3
    },
    defense: {
        numLayers: 6,
        maxRadiusMultiplier: 2,
        baseAlpha: 0.15,
        particleCount: 12,
        orbitRadiusMultiplier: 0.8,
        rotationSpeed: 1.5
    },
    footprints: {
        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
            }
        }
    }
};


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: 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,
        isSwinging: false,
        equipment: 'sword',
        bubbles: [],
        bubbleParticles: [],
        lastBubbleTime: 0,
        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: [],
    lastFootprintTime: 0,
    camera: {
        x: 0,
        y: 0,
        targetX: 0,
        targetY: 0
    },
    collisionMap: new Map()
});

let state = createInitialState();


// ============= 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;
        
        if (state.player.equipment === 'sword' && !state.player.isSwinging) {
            return {
                ...state,
                player: {
                    ...state.player,
                    isSwinging: true,
                    swordAngle: Math.atan2(state.player.direction.y, state.player.direction.x) - Math.PI / 2
                }
            };
        } else if (state.player.equipment === 'unarmed') {
            return createBubbleAttack(state, animationTime);
        }
        return state;
    },

    handleEquipmentSwitch: (state) => {
        const equipment = ['sword', 'unarmed'];
        const currentIndex = equipment.indexOf(state.player.equipment);
        return {
            ...state,
            player: {
                ...state.player,
                equipment: equipment[(currentIndex + 1) % equipment.length]
            }
        };
    }
};


// ============= 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;
    const speedMultiplier = Math.pow(1 - ageRatio, 0.5);
    
    return {
        ...bubble,
        x: bubble.x + bubble.dx * speedMultiplier,
        y: bubble.y + bubble.dy * speedMultiplier
    };
};

const generateBubbleParticles = (bubble, animationTime) => {
    const age = animationTime - bubble.createdAt;
    const ageRatio = age / CONFIG.bubble.lifetime;
    
    if (Math.random() >= CONFIG.bubble.particleEmitRate * (1 - ageRatio)) {
        return [];
    }
    
    const trailDistance = Math.random() * 20;
    const particleX = bubble.x - bubble.dx * (trailDistance / CONFIG.bubble.speed);
    const particleY = bubble.y - bubble.dy * (trailDistance / CONFIG.bubble.speed);
    
    const particleAngle = bubble.angle + (Math.random() - 0.5) * CONFIG.bubble.arcWidth * 2;
    const spreadSpeed = CONFIG.bubble.speed * 0.2 * (1 - ageRatio);
    const spreadAngle = Math.random() * Math.PI * 2;
    const speedMultiplier = Math.pow(1 - ageRatio, 0.5);
    
    return [{
        x: particleX,
        y: particleY,
        dx: (Math.cos(particleAngle) * CONFIG.bubble.speed * CONFIG.bubbleParticle.speedMultiplier * speedMultiplier) +
            (Math.cos(spreadAngle) * spreadSpeed),
        dy: (Math.sin(particleAngle) * CONFIG.bubble.speed * CONFIG.bubbleParticle.speedMultiplier * speedMultiplier) +
            (Math.sin(spreadAngle) * spreadSpeed),
        size: CONFIG.bubbleParticle.size * (0.5 + Math.random() * 0.5),
        createdAt: animationTime
    }];
};

const updateBubbleParticles = (particles, animationTime) => {
    return particles.filter(particle => {
        const age = animationTime - particle.createdAt;
        return age < CONFIG.bubbleParticle.lifetime;
    }).map(particle => ({
        ...particle,
        x: particle.x + particle.dx,
        y: particle.y + particle.dy
    }));
};

const createBubbleAttack = (state, animationTime) => {
    const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
    if (timeSinceLastBubble < CONFIG.bubble.cooldown) return state;
    
    const angle = Math.atan2(state.player.direction.y, state.player.direction.x);
    const bubble = {
        x: state.player.x,
        y: state.player.y,
        dx: state.player.direction.x * CONFIG.bubble.speed,
        dy: state.player.direction.y * CONFIG.bubble.speed,
        angle: angle,
        createdAt: animationTime,
        size: CONFIG.bubble.size * (0.8 + Math.random() * 0.4)
    };
    
    return {
        ...state,
        player: {
            ...state.player,
            bubbles: [...state.player.bubbles, bubble],
            lastBubbleTime: animationTime
        }
    };
};

const weaponSystems = {
    updateBubbles: (state, animationTime) => {
        const updatedBubbles = state.player.bubbles
            .filter(bubble => animationTime - bubble.createdAt < CONFIG.bubble.lifetime)
            .map(bubble => updateBubble(bubble, animationTime));
            
        const newParticles = updatedBubbles
            .flatMap(bubble => generateBubbleParticles(bubble, animationTime));
            
        return {
            ...state,
            player: {
                ...state.player,
                bubbles: updatedBubbles,
                bubbleParticles: [
                    ...updateBubbleParticles(state.player.bubbleParticles, animationTime),
                    ...newParticles
                ]
            }
        };
    },

    updateSwordSwing: (state, animationTime) => {
        if (!state.player.isSwinging) return state;
        
        const newAngle = state.player.swordAngle + CONFIG.sword.swingSpeed;
        const swingComplete = newAngle > Math.atan2(state.player.direction.y, state.player.direction.x) + Math.PI / 2;
        
        return {
            ...state,
            player: {
                ...state.player,
                swordAngle: newAngle,
                isSwinging: !swingComplete
            }
        };
    }
};


// ============= Particle Systems =============
const createParticle = (x, y, angle) => ({
    x,
    y,
    angle,
    createdAt: animationTime,
    lifetime: CONFIG.swordParticles.lifetime,
    speed: CONFIG.swordParticles.speed * (0.5 + Math.random() * 0.5),
    size: CONFIG.swordParticles.size.min + Math.random() * (CONFIG.swordParticles.size.max - CONFIG.swordParticles.size.min)
});

const createFootprint = (x, y, direction) => ({
    x,
    y,
    direction,
    createdAt: animationTime,
    size: CONFIG.footprints.size * (0.8 + Math.random() * 0.4),
    offset: (Math.random() - 0.5) * 5
});


// ============= Rendering System =============
const renderPlayer = () => {
    ctx.save();
    
    state.player.bubbleParticles.forEach(particle => {
        const age = (animationTime - particle.createdAt) / CONFIG.bubbleParticle.lifetime;
        const alpha = (1 - age) * 0.8;
        
        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();
        
        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();
    });
    
    state.player.bubbles.forEach(bubble => {
        const age = (animationTime - bubble.createdAt) / CONFIG.bubble.lifetime;
        const alpha = Math.pow(1 - age, CONFIG.bubble.fadeExponent);
        const expandedSize = bubble.size * (1 + age * 2);
        
        ctx.save();
        ctx.translate(bubble.x, bubble.y);
        ctx.rotate(bubble.angle);
        
        ctx.beginPath();
        ctx.arc(0, 0, expandedSize * 1.5, 
            -CONFIG.bubble.arcWidth * (1 + age * 0.5),
            CONFIG.bubble.arcWidth * (1 + age * 0.5), 
            false
        );
        ctx.lineCap = 'round';
        ctx.lineWidth = expandedSize * 0.5 * (1 - age * 0.3);
        ctx.strokeStyle = CONFIG.bubble.colors.glow.replace(')', `, ${alpha * 0.5})`);
        ctx.stroke();
        
        const gradient = ctx.createLinearGradient(
            -expandedSize, 0,
            expandedSize, 0
        );
        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, 
            -CONFIG.bubble.arcWidth * 0.8 * (1 + age * 0.5),
            CONFIG.bubble.arcWidth * 0.8 * (1 + age * 0.5),
            false
        );
        ctx.lineWidth = expandedSize * 0.3 * (1 - age * 0.3);
        ctx.strokeStyle = gradient;
        ctx.stroke();
        
        ctx.beginPath();
        ctx.arc(0, 0, expandedSize * 0.9, 
            -CONFIG.bubble.arcWidth * 0.6 * (1 + age * 0.5),
            CONFIG.bubble.arcWidth * 0.6 * (1 + age * 0.5),
            false
        );
        ctx.lineWidth = expandedSize * 0.1 * (1 - age * 0.3);
        ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.8})`;
        ctx.stroke();
        
        ctx.restore();
    });
    
    if (state.player.isSwinging && state.player.equipment === 'sword') {
        const blurSteps = 12;
        const blurSpread = 0.2;
        
        for (let i = 0; i < blurSteps; i++) {
            const alpha = 0.35 - (i * 0.02);
            const angleOffset = -blurSpread * i;
            
            ctx.strokeStyle = `rgba(30, 144, 255, ${alpha})`;
            ctx.lineWidth = 4 + (blurSteps - i);
            ctx.beginPath();
            ctx.moveTo(state.player.x, state.player.y);
            ctx.lineTo(
                state.player.x + Math.cos(state.player.swordAngle + angleOffset) * CONFIG.sword.length,
                state.player.y + Math.sin(state.player.swordAngle + angleOffset) * CONFIG.sword.length
            );
            ctx.stroke();
        }
        
        const gradient = ctx.createLinearGradient(
            state.player.x,
            state.player.y,
            state.player.x + Math.cos(state.player.swordAngle) * CONFIG.sword.length,
            state.player.y + Math.sin(state.player.swordAngle) * CONFIG.sword.length
        );
        gradient.addColorStop(0, CONFIG.sword.colors.primary);
        gradient.addColorStop(0.6, CONFIG.sword.colors.secondary);
        gradient.addColorStop(1, CONFIG.sword.colors.tertiary);
        
        ctx.strokeStyle = CONFIG.sword.colors.glow;
        ctx.lineWidth = 10;
        ctx.beginPath();
        ctx.moveTo(state.player.x, state.player.y);
        ctx.lineTo(
            state.player.x + Math.cos(state.player.swordAngle) * CONFIG.sword.length,
            state.player.y + Math.sin(state.player.swordAngle) * CONFIG.sword.length
        );
        ctx.stroke();
        
        ctx.strokeStyle = gradient;
        ctx.lineWidth = 6;
        ctx.stroke();
    }
    
    if (state.player.isDefending) {
        const numLayers = CONFIG.defense.numLayers;
        const maxRadius = CONFIG.player.size * CONFIG.defense.maxRadiusMultiplier;
        const baseAlpha = CONFIG.defense.baseAlpha;
        
        for (let i = numLayers - 1; i >= 0; i--) {
            const radius = CONFIG.player.size / 2 + (maxRadius - CONFIG.player.size / 2) * (i / numLayers);
            const alpha = baseAlpha * (1 - i / numLayers);
            
            const pulseOffset = Math.sin(Date.now() / 500) * 3;
            
            const glowGradient = ctx.createRadialGradient(
                state.player.x, state.player.y, radius - 5,
                state.player.x, state.player.y, radius + pulseOffset
            );
            glowGradient.addColorStop(0, `rgba(30, 144, 255, ${alpha})`);
            glowGradient.addColorStop(1, 'rgba(30, 144, 255, 0)');
            
            ctx.beginPath();
            ctx.arc(state.player.x, state.player.y, radius + pulseOffset, 0, Math.PI * 2);
            ctx.fillStyle = glowGradient;
            ctx.fill();
        }
        
        const mainAuraGradient = ctx.createRadialGradient(
            state.player.x, state.player.y, CONFIG.player.size / 2 - 2,
            state.player.x, state.player.y, CONFIG.player.size / 2 + 8
        );
        mainAuraGradient.addColorStop(0, 'rgba(30, 144, 255, 0.3)');
        mainAuraGradient.addColorStop(0.5, 'rgba(30, 144, 255, 0.2)');
        mainAuraGradient.addColorStop(1, 'rgba(30, 144, 255, 0)');
        
        ctx.beginPath();
        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2 + 8, 0, Math.PI * 2);
        ctx.fillStyle = mainAuraGradient;
        ctx.fill();
        
        ctx.beginPath();
        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2, 0, Math.PI * 2);
        ctx.fillStyle = '#111';
        ctx.fill();
        
        ctx.beginPath();
        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2, 0, Math.PI * 2);
        ctx.strokeStyle = CONFIG.sword.colors.secondary;
        ctx.lineWidth = 3;
        ctx.stroke();
        
        ctx.beginPath();
        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2 - 3, 0, Math.PI * 2);
        ctx.strokeStyle = 'rgba(30, 144, 255, 0.3)';
        ctx.lineWidth = 2;
        ctx.stroke();
        
        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++) {
            const radialOffset = Math.sin(animationTime * 0.002 + i * 0.5) * 4;
            const orbitRadius = baseOrbitRadius + radialOffset;
            
            const angle = (i / numParticles) * Math.PI * 2 + animationTime * rotationSpeed * 0.001;
            const x = state.player.x + Math.cos(angle) * orbitRadius;
            const y = state.player.y + Math.sin(angle) * orbitRadius;
            
            const size = 2 + Math.sin(animationTime * 0.003 + i * 0.8) * 1.5;
            const baseAlpha = 0.6 + Math.sin(animationTime * 0.002 + i) * 0.2;
            
            ctx.beginPath();
            ctx.arc(x, y, size * 2, 0, Math.PI * 2);
            ctx.fillStyle = `rgba(30, 144, 255, ${baseAlpha * 0.3})`;
            ctx.fill();
            
            ctx.beginPath();
            ctx.arc(x, y, size, 0, Math.PI * 2);
            ctx.fillStyle = `rgba(135, 206, 250, ${baseAlpha})`;
            ctx.fill();
            
            if (i > 0) {
                const prevAngle = ((i - 1) / numParticles) * Math.PI * 2 + animationTime * rotationSpeed * 0.001;
                const prevX = state.player.x + Math.cos(prevAngle) * orbitRadius;
                const prevY = state.player.y + Math.sin(prevAngle) * orbitRadius;
                
                ctx.beginPath();
                ctx.moveTo(prevX, prevY);
                ctx.lineTo(x, y);
                ctx.strokeStyle = `rgba(30, 144, 255, ${baseAlpha * 0.2})`;
                ctx.lineWidth = 1;
                ctx.stroke();
            }
        }
        
        // Draw the eyeball...square
        const dotSize = CONFIG.player.directionIndicator.size;
        
        // Calculate cooldown progress
        const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
        // const cooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
        
        // Set opacity based on cooldown's progress
        const dotOpacity = getDotOpacity(state, animationTime);
        
        ctx.fillStyle = CONFIG.player.directionIndicator.color.replace(
            '1)',
            `${dotOpacity})`
        );
        
        ctx.fillRect(
            state.player.x - dotSize/2,
            state.player.y - dotSize/2,
            dotSize,
            dotSize
        );
    } else {
        // Draw player square
        ctx.fillStyle = CONFIG.player.color;
        ctx.fillRect(
            state.player.x - CONFIG.player.size / 2,
            state.player.y - CONFIG.player.size / 2,
            CONFIG.player.size,
            CONFIG.player.size
        );
        
        // Draw direction indicator square with cooldown opacity
        const dotSize = CONFIG.player.directionIndicator.size;
        const dotDistance = CONFIG.player.size / 3;
        const dotX = state.player.x + state.player.direction.x * dotDistance;
        const dotY = state.player.y + state.player.direction.y * dotDistance;
        
        const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
        // const cooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
        
        // Set opacity based on cooldown's progress
        const dotOpacity = getDotOpacity(state, animationTime);
        
        ctx.fillStyle = CONFIG.player.directionIndicator.color.replace(
            '1)',  // Replace the full opacity with our calculated opacity
            `${dotOpacity})`
        );
        
        ctx.fillRect(
            dotX - dotSize/2,
            dotY - dotSize/2,
            dotSize,
            dotSize
        );
    }
    
    ctx.restore();
};

const render = () => {
    ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
    
    const screenCenterX = -state.camera.x + GAME_WIDTH / 2;
    const screenCenterY = -state.camera.y + GAME_HEIGHT / 2;
    
    const distX = state.player.x - screenCenterX;
    const distY = state.player.y - screenCenterY;
    
    if (Math.abs(distX) > CAMERA_DEADZONE_X / 2) {
        const bufferX = (GAME_WIDTH - CAMERA_DEADZONE_X) / 2;
        const targetOffsetX = distX > 0 ? GAME_WIDTH - bufferX : bufferX;
        state.camera.targetX = -(state.player.x - targetOffsetX);
    }
    if (Math.abs(distY) > CAMERA_DEADZONE_Y / 2) {
        const bufferY = (GAME_HEIGHT - CAMERA_DEADZONE_Y) / 2;
        const targetOffsetY = distY > 0 ? GAME_HEIGHT - bufferY : bufferY;
        state.camera.targetY = -(state.player.y - targetOffsetY);
    }
    
    state.camera.x = lerp(state.camera.x, state.camera.targetX, CONFIG.display.camera.ease);
    state.camera.y = lerp(state.camera.y, state.camera.targetY, CONFIG.display.camera.ease);
    
    ctx.save();
    ctx.translate(state.camera.x, state.camera.y);

    const gridSize = CONFIG.display.grid.size;
    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;
    
    // 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, 0);
        ctx.lineTo(x, worldSize);
        ctx.stroke();
    }

    // Draw horizontal lines
    for (let y = 0; y < worldSize; y += gridSize) {
        ctx.beginPath();
        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;
        
        const alpha = Math.max(0, 1 - age * age);
        
        ctx.save();
        ctx.translate(footprint.x + footprint.offset, footprint.y + footprint.offset);
        
        const radius = Math.max(0.1, footprint.size * (1 - age * 0.5));
        
        if (radius > 0) {
            ctx.beginPath();
            ctx.arc(0, 0, radius * 2, 0, Math.PI * 2);
            ctx.fillStyle = `rgba(17, 17, 17, ${alpha * 0.1})`;
            ctx.fill();
            
            ctx.beginPath();
            ctx.arc(0, 0, radius, 0, Math.PI * 2);
            ctx.fillStyle = `rgba(17, 17, 17, ${alpha * 0.3})`;
            ctx.fill();
        }
        
        ctx.restore();
    });
    
    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;
        animationTime = 0;
    }

    const deltaTime = currentTime - lastFrameTime;
    
    if (deltaTime >= FRAME_TIME) {
        animationTime += FRAME_TIME;
        
        updatePlayer();
        render();
        
        lastFrameTime = currentTime;
    }
    
    requestAnimationFrame(gameLoop);
};


// ============= Setup & Initialization =============
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const resizeCanvas = () => {
    GAME_WIDTH = window.innerWidth;
    GAME_HEIGHT = window.innerHeight;
    canvas.width = GAME_WIDTH;
    canvas.height = GAME_HEIGHT;
    if (!state.player.x) {
        state.player.x = GAME_WIDTH / 2;
        state.player.y = GAME_HEIGHT / 2;
    }
};

window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener('resize', resizeCanvas);

resizeCanvas();
requestAnimationFrame(gameLoop);

const getDotOpacity = (state, animationTime) => {
    // Get bubble cooldown opacity
    const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
    const bubbleCooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
    const bubbleOpacity = 0.1 + (bubbleCooldownProgress * 0.9);
    
    // Get dash cooldown opacity
    let dashOpacity = 1;
    if (state.player.dashExhausted) {
        const timeSinceExhaustion = animationTime - state.player.lastDashEnd;
        const dashCooldownProgress = timeSinceExhaustion / CONFIG.player.dash.cooldown;
        dashOpacity = 0.1 + (Math.min(dashCooldownProgress, 1) * 0.9);
    } else if (state.player.isDashing) {
        const dashProgress = (animationTime - state.player.dashStartTime) / CONFIG.player.dash.duration;
        dashOpacity = 1 - (dashProgress * 0.7);  // Fade to 0.3 during dash
    }

    let blinkOpacity = 1;
    if (state.player.isDefending) {
        const blinkPhase = Math.sin(animationTime * 0.002);
        if (blinkPhase > 0.7) {
            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
        }
    };
};