about summary refs log blame commit diff stats
path: root/html/plains/enemies.js
blob: 3d40c3eed3414c409cae9a666388c247c1365353 (plain) (tree)
1
2
3
4
5
6
                    




                     









                                                            
                                     


                                                             
                                                                           






                                                                                



                                             
                                                       











                                                                                                                    
                                                      






















                                       





























                                                               



                                                                             
                                       
                         
                                            
    
                                             
                             
                                                 










                                                                   
                                      

                                    




                                       
                                              







                                                                

                                                          


                                              


             






                                                             

                                                               
                                                                              
 







                                                                                                    



                                                                            









                                                        
                           












                                                                                                          
                                            




                                                                        
                                                               


                                                     
                                       












                                                                                      
                         



                                                                  
                                      
                                                        
                                  


                                                                                         
                                             
                                                 
                                         

                                        
                                                  




                                                         

         
                             




                                                              
 

                                               



                                                                 
                                                                                                           









                                                                                          







                                                                                               


                                                                                        
                                          
                                  
                                           




                                                                                          
                                                                    
                    
                                                     



















                                                                                           
                                           









                                                                                                                      









                                                                                
 



                                                                        







                                                    



                                                                
                                                  












                                                                     
                                                                        




                                                                      





                                                                           
                         

                                                                                                          
                                                      


                                                   

                                                                          


                                                                         
                                     

                   






                                                                              









                                                                        








                                                                            


       
                                
                                                       



                                            
                                                    
                                                    

                            





                    


                      
                  

                            
  
const blueShades = [
    'rgb(0, 0, 255)',
    'rgb(0, 0, 200)',
    'rgb(0, 0, 150)',
    'rgb(0, 0, 100)',
    'rgb(0, 0, 50)'
];

const generateEnemies = (villagers, collisionMap) => {
    const enemies = [];
    const gridSize = CONFIG.display.grid.size;
    const occupiedCells = new Set([...collisionMap.keys()]);
    
    villagers.forEach(villager => {
        if (villager.status === 'rescued') return;
        
        // 2 - 5 enemies per villager
        const numEnemies = 2 + Math.floor(Math.random() * 4);
        
        for (let i = 0; i < numEnemies; i++) {
            // make sure to place an enemy within 2 - 3 cells of a villager
            const radius = 2 + Math.floor(Math.random());
            const angle = Math.random() * Math.PI * 2;
            
            const cellX = villager.cellX + Math.floor(Math.cos(angle) * radius);
            const cellY = villager.cellY + Math.floor(Math.sin(angle) * radius);
            const cellKey = `${cellX},${cellY}`;
            
            if (occupiedCells.has(cellKey)) {
                continue;
            }
            
            // random color between orange and dark red
            const red = 150 + Math.floor(Math.random() * 105);  // 150-255
            const green = Math.floor(Math.random() * 100);      // 0-100
            const blue = 0;
            
            enemies.push({
                x: (cellX * gridSize) + (gridSize / 2),
                y: (cellY * gridSize) + (gridSize / 2),
                color: `rgb(${red}, ${green}, ${blue})`,
                size: CONFIG.enemies.size.min + Math.random() * (CONFIG.enemies.size.max - CONFIG.enemies.size.min),
                targetVillager: villager,
                patrolAngle: Math.random() * Math.PI * 2,
                isChasing: false,
                hp: 2 + Math.floor(Math.random() * 4),
                stunned: false,
                stunEndTime: 0,
                attacking: false,
                attackCooldown: false,
                attackCooldownUntil: 0,
                knockback: {
                    active: false,
                    startX: 0,
                    startY: 0,
                    targetX: 0,
                    targetY: 0,
                    startTime: 0,
                    duration: 300
                }
            });
            
            occupiedCells.add(cellKey);
        }
    });
    
    return enemies;
};

const createDeathParticles = (enemy) => {
    const numParticles = 15 + Math.floor(Math.random() * 10);
    for (let i = 0; i < numParticles; i++) {
        const particleAngle = (i / numParticles) * Math.PI * 2;
        const speed = 2 + Math.random() * 3;
        state.particles.push({
            x: enemy.x,
            y: enemy.y,
            dx: Math.cos(particleAngle) * speed,
            dy: Math.sin(particleAngle) * speed,
            size: enemy.size * (0.1 + Math.random() * 0.2),
            color: enemy.color,
            lifetime: 1000,
            createdAt: animationTime
        });
    }
};

const createDeathDiamonds = (enemy) => {
    const diamondCount = Math.floor(Math.random() * 5);
    for (let i = 0; i < diamondCount; i++) {
        state.diamonds.push({
            x: enemy.x + (Math.random() - 0.5) * 20,
            y: enemy.y + (Math.random() - 0.5) * 20,
            size: 6,
            collected: false
        });
    }
};

const handleEnemyDamage = (enemy, damage, knockbackForce = 0, angle = 0) => {
    const gridSize = CONFIG.display.grid.size;
    enemy.hp -= damage;
    
    // stun the enemy when you hit them
    enemy.stunned = true;
    enemy.stunEndTime = animationTime + 500;
    
    // knock the enemy back when you hit them
    if (knockbackForce > 0) {
        const knockbackDistance = gridSize * 0.5;
        enemy.knockback = {
            active: true,
            startX: enemy.x,
            startY: enemy.y,
            targetX: enemy.x + Math.cos(angle) * knockbackDistance,
            targetY: enemy.y + Math.sin(angle) * knockbackDistance,
            startTime: animationTime,
            duration: 300
        };
    }
    
    if (damage > 0 && enemy.hp <= 0) {
        createDeathParticles(enemy);
        createDeathDiamonds(enemy);
    }
    
    return damage > 0 && enemy.hp <= 0;
};

// find the nearest lost villager, zoot to 'em
const findNearestLostVillager = (enemyX, enemyY, villagers) => {
    let nearestVillager = null;
    let shortestDistance = Infinity;
    
    villagers.forEach(villager => {
        if (villager.status === 'lost') {
            const dx = villager.x - enemyX;
            const dy = villager.y - enemyY;
            const distance = Math.sqrt(dx * dx + dy * dy);
            
            if (distance < shortestDistance) {
                shortestDistance = distance;
                nearestVillager = villager;
            }
        }
    });
    
    return nearestVillager;
};

const updateEnemies = (enemies, deltaTime) => {
    const gridSize = CONFIG.display.grid.size;
    const aggroRange = gridSize * CONFIG.enemies.chase.range;

    return enemies.filter(enemy => enemy.hp > 0).map(enemy => {
        const baseSpeed = CONFIG.enemies.patrol.speed.base * deltaTime / 1000;

        if (enemy.knockback.active) {
            const progress = (animationTime - enemy.knockback.startTime) / enemy.knockback.duration;
            
            if (progress >= 1) {
                enemy.knockback.active = false;
                enemy.x = enemy.knockback.targetX;
                enemy.y = enemy.knockback.targetY;
                
                const dvx = enemy.x - enemy.targetVillager.x;
                const dvy = enemy.y - enemy.targetVillager.y;
                const distanceToVillager = Math.sqrt(dvx * dvx + dvy * dvy);
                
                if (distanceToVillager > gridSize * 3) {
                    return {
                        ...enemy,
                        x: enemy.x,
                        y: enemy.y,
                        isChasing: false,
                        isReturning: true
                    };
                }
            } else {
                // ease out
                const t = 1 - Math.pow(1 - progress, 3);
                enemy.x = enemy.knockback.startX + (enemy.knockback.targetX - enemy.knockback.startX) * t;
                enemy.y = enemy.knockback.startY + (enemy.knockback.targetY - enemy.knockback.startY) * t;
            }
            
            return {
                ...enemy,
                x: enemy.x,
                y: enemy.y,
                isChasing: false
            };
        }
        
        // IF enemy is returning to villager
        if (enemy.isReturning) {
            const dvx = enemy.targetVillager.x - enemy.x;
            const dvy = enemy.targetVillager.y - enemy.y;
            const distanceToVillager = Math.sqrt(dvx * dvx + dvy * dvy);
            
            // IF back within range, go back to normal behavior
            if (distanceToVillager <= gridSize * 2) {
                enemy.isReturning = false;
            } else {
                // Move to the villager
                const angle = Math.atan2(dvy, dvx);
                const returnSpeed = baseSpeed * CONFIG.enemies.return.speedMultiplier;
                
                return {
                    ...enemy,
                    x: enemy.x + Math.cos(angle) * returnSpeed,
                    y: enemy.y + Math.sin(angle) * returnSpeed,
                    isChasing: false,
                    isReturning: true
                };
            }
        }
        
        // Still stunned?
        if (enemy.stunned && animationTime >= enemy.stunEndTime) {
            enemy.stunned = false;
        }
        
        // Was your villager rescued??
        if (enemy.targetVillager.status === 'rescued') {
            // find a new villager
            const newTarget = findNearestLostVillager(enemy.x, enemy.y, state.villagers);
            
            if (newTarget) {
                // zoot towards that villager
                enemy.targetVillager = newTarget;
                enemy.isReturning = true;
                enemy.isChasing = false;
            } else {
                // no more villagers, get real sad
                return {
                    ...enemy,
                    color: CONFIG.enemies.colors.defeated
                };
            }
        }
        
        // Distance to player
        const dx = state.player.x - enemy.x;
        const dy = state.player.y - enemy.y;
        const distanceToPlayer = Math.sqrt(dx * dx + dy * dy);
        
        if (distanceToPlayer <= aggroRange) {

            const attackRange = gridSize * 0.5;
            
            if (enemy.attackCooldown) {
                if (animationTime >= enemy.attackCooldownUntil) {
                    enemy.attackCooldown = false;
                } else if (distanceToPlayer < gridSize) {
                    const retreatAngle = Math.atan2(dy, dx) + Math.PI; // retreat in the opposite direction
                    const retreatSpeed = baseSpeed * CONFIG.enemies.chase.speedMultiplier;
                    return {
                        ...enemy,
                        x: enemy.x + Math.cos(retreatAngle) * retreatSpeed,
                        y: enemy.y + Math.sin(retreatAngle) * retreatSpeed,
                        isChasing: true
                    };
                }
            }
            
            if (distanceToPlayer <= attackRange && !enemy.attacking && !enemy.attackCooldown) {
                enemy.attacking = true;
                enemy.attackStartPosition = { x: enemy.x, y: enemy.y };
                enemy.attackTargetPosition = {
                    x: state.player.x,
                    y: state.player.y
                };
                enemy.attackStartTime = animationTime;
                enemy.color = blueShades[Math.floor(Math.random() * blueShades.length)];
            }
            
            // attack animation and damage
            if (enemy.attacking) {
                const attackDuration = 200;
                const progress = (animationTime - enemy.attackStartTime) / attackDuration;
                
                if (progress >= 1) {
                    enemy.attacking = false;
                    enemy.attackCooldown = true;
                    enemy.attackCooldownUntil = animationTime + 500;
                    
                    // did the attack hit the player?
                    const finalDx = state.player.x - enemy.x;
                    const finalDy = state.player.y - enemy.y;
                    const finalDistance = Math.sqrt(finalDx * finalDx + finalDy * finalDy);
                    
                    if (finalDistance < enemy.size + CONFIG.player.size && 
                        !state.player.isInvulnerable && 
                        !state.player.isDefending && 
                        !state.player.isSwinging && 
                        state.player.bubbles.length === 0) {
                        state.player.hp -= 1;
                        state.player.isInvulnerable = true;
                        state.player.invulnerableUntil = animationTime + 1000;
                    }
                    
                    return {
                        ...enemy,
                        isChasing: true
                    };
                }
                
                // lunge towards the player
                const t = progress * progress; // Quadratic ease-in
                return {
                    ...enemy,
                    x: enemy.attackStartPosition.x + (enemy.attackTargetPosition.x - enemy.attackStartPosition.x) * t,
                    y: enemy.attackStartPosition.y + (enemy.attackTargetPosition.y - enemy.attackStartPosition.y) * t,
                    isChasing: true,
                    attacking: true
                };
            }
            
            const angle = Math.atan2(dy, dx);
            const chaseSpeed = baseSpeed * CONFIG.enemies.chase.speedMultiplier;
            
            return {
                ...enemy,
                x: enemy.x + Math.cos(angle) * chaseSpeed,
                y: enemy.y + Math.sin(angle) * chaseSpeed,
                isChasing: true
            };
        } else {

            const dvx = enemy.x - enemy.targetVillager.x;
            const dvy = enemy.y - enemy.targetVillager.y;
            const distanceToVillager = Math.sqrt(dvx * dvx + dvy * dvy);
            
            if (distanceToVillager > gridSize * 3) {
                return {
                    ...enemy,
                    isReturning: true,
                    isChasing: false
                };
            }
            
            if (!enemy.patrolAngle) {
                enemy.patrolAngle = Math.random() * Math.PI * 2;
            }
            
            enemy.patrolAngle += baseSpeed * 0.02;
            
            return {
                ...enemy,
                x: enemy.x + Math.cos(enemy.patrolAngle) * baseSpeed,
                y: enemy.y + Math.sin(enemy.patrolAngle) * baseSpeed,
                patrolAngle: enemy.patrolAngle,
                isChasing: false
            };
        }
    });
};

const renderEnemies = (ctx, enemies) => {
    // I tried to generate this per enemy, but it was wildly inefficient
    const healthGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, 2);
    healthGradient.addColorStop(0, 'rgba(255, 0, 0, 0.8)');
    healthGradient.addColorStop(0.7, 'rgba(200, 0, 0, 0.6)');
    healthGradient.addColorStop(1, 'rgba(150, 0, 0, 0.4)');

    enemies.forEach(enemy => {
        ctx.beginPath();
        ctx.arc(enemy.x, enemy.y, enemy.size, 0, Math.PI * 2);
        ctx.fillStyle = enemy.stunned ? 'rgb(150, 150, 150)' : enemy.color;
        ctx.fill();
        
        // she be radiant
        const glowSize = enemy.stunned ? 1.1 : (enemy.attacking ? 1.6 : enemy.isChasing ? 1.4 : 1.2);
        const glowIntensity = enemy.stunned ? 0.1 : (enemy.attacking ? 0.5 : enemy.isChasing ? 0.3 : 0.2);
        const glowGradient = ctx.createRadialGradient(
            enemy.x, enemy.y, enemy.size * 0.5,
            enemy.x, enemy.y, enemy.size * glowSize
        );
        glowGradient.addColorStop(0, `rgba(255, 0, 0, ${glowIntensity})`);
        glowGradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
        
        ctx.beginPath();
        ctx.arc(enemy.x, enemy.y, enemy.size * glowSize, 0, Math.PI * 2);
        ctx.fillStyle = glowGradient;
        ctx.fill();
        
        if (enemy.hp > 0) {
            const circleRadius = 3;
            const circleSpacing = 8;
            const totalCircles = enemy.hp;
            const startX = enemy.x - ((totalCircles - 1) * circleSpacing) / 2;
            const circleY = enemy.y - enemy.size - 15;
            
            ctx.beginPath();
            for (let i = 0; i < totalCircles; i++) {
                const circleX = startX + (i * circleSpacing);
                ctx.moveTo(circleX + circleRadius, circleY);
                ctx.arc(circleX, circleY, circleRadius, 0, Math.PI * 2);
            }
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
            ctx.lineWidth = 1;
            ctx.stroke();
            
            ctx.beginPath();
            for (let i = 0; i < totalCircles; i++) {
                const circleX = startX + (i * circleSpacing);
                ctx.moveTo(circleX + circleRadius - 1, circleY);
                ctx.arc(circleX, circleY, circleRadius - 1, 0, Math.PI * 2);
            }
            ctx.fillStyle = 'rgba(255, 0, 0, 0.6)';
            ctx.fill();
        }
    });
};

const generateDiamonds = () => {
    const diamondCount = Math.floor(Math.random() * 3);
    const diamonds = [];
    
    for (let i = 0; i < diamondCount; i++) {
        diamonds.push({
            x: enemy.x + (Math.random() - 0.5) * 20,
            y: enemy.y + (Math.random() - 0.5) * 20,
            size: 10,
            collected: false
        });
    }
    
    return diamonds;
};

window.enemySystem = {
    generateEnemies,
    updateEnemies,
    renderEnemies,
    findNearestLostVillager,
    handleEnemyDamage
};