about summary refs log blame commit diff stats
path: root/html/plains/enemies.js
blob: 3d40c3eed3414c409cae9a666388c247c1365353 (plain) (tree)
735c449 pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
/*
identity        I   a → a
constant        K   a → b → a
apply           A   (a → b) → a → b
thrush          T   a → (a → b) → b
duplication     W   (a → a → b) → a → b
flip            C   (a → b → c) → b → a → c
compose         B   (b → c) → (a → b) → a → c
substitution    S   (a → b → c) → (a → b) → a → c
chain           S_³ (a → b → c) → (b → a) → b → c
converge        S2³ (b → c → d) → (a → b) → (a → c) → a → d
psi             P   (b → b → c) → (a → b) → a → a → c
fix-point4      Y   (a → a) → a

ref:
https://www.willtaylor.blog/combinators-and-church-encoding-in-javscript/
*/

const I  = x => x
const K  = x => y => x
const A  = f => x => f (x)
const T  = x => f => f (x)
const W  = f => x => f (x) (x)
const C  = f => y => x => f (x) (y)
const B  = f => g => x => f (g (x))
const S  = f => g => x => f (x) (g (x))
const S_ = f => g => x => f (g (x)) (x)
const S2 = f => g => h => x => f (g (x)) (h (x))
const P  = f => g => x => y => f (g (x)) (g (y))
const Y  = f => (g => g (g)) (g => f (x => g (g) (x)))

// validate combinators
// I(1) // 1
// K(1)(2) // 1
// A(I)(1) // 1
// T(1)(I) // 1
// W(I)(1) // 1
// C(I)(1)(2) // 1
// B(I)(I)(1) // 1
// S(I)(I)(1) // 1
// S_(I)(I)(1) // 1
// S2(I)(I)(I)(1) // 1
// P(I)(I)(1)(2) // 1
// Y(I)(1) // 1

(function() {
    console.log('validating combinators');
    console.assert(I(1) === 1, 'I failed');
    console.assert(K(1)(2) === 1, 'K failed');
    console.assert(A(I)(1) === 1, 'A failed');
    console.assert(T(1)(I) === 1, 'T failed');
    console.assert(W(I)(1) === 1, 'W failed'); // FIXME: Does this really work?
    console.assert(C(I)(1)(2) === 1, 'C failed');
    console.assert(B(I)(I)(1) === 1, 'B failed');
    console.assert(S(I)(I)(1) === 1, 'S failed');
    console.assert(S_(I)(I)(1) === 1, 'S_ failed');
    console.assert(S2(I)(I)(I)(1) === 1, 'S2 failed');
    console.assert(P(I)(I)(1)(2) === 1, 'P failed');
    console.assert(Y(I)(1) === 1, 'Y failed');
}());


// Count the number of live neighbors of a cell
const countLiveNeighbors = B (A (B (A (B (A (K (A (I)))))))) (A (B (A (K (A (I))))))
const isAlive = cell => count => (cell && (count === 2 || count === 3)) || (!cell && count === 3)
const rules = B (A (B (A (K (A (I)))))) (A (B (A (K (A (I))))))
const nextState = B (A (B (A (K (A (I)))))) (A (B (A (K (A (I))))))
const nextBoardState = B (A (B (A (K (A (I)))))) (A (B (A (K (A (I))))))


// validate countLiveNeighbors rules
(function() {
    // FIXME: I think I messed up these test values, maybe?
    // FIXME: I also don't think this'll work given that the combinators will only grok 1 arg that is another combinator...right?
    console.log('validating countLiveNeighbors');
    console.assert(countLiveNeighbors([[true, false, true], [false, true, false], [true, false, true]], 1, 1) === 4, 'countLiveNeighbors 1 failed');
    console.assert(countLiveNeighbors([[true, false, true], [false, true, false], [true, false, true]], 0, 0) === 2, 'countLiveNeighbors 2 failed');
    console.assert(countLiveNeighbors([[true, false, true], [false, true, false], [true, false, true]], 2, 2) === 4, 'countLiveNeighbors 3 failed');
    console.assert(countLiveNeighbors([[true, false, true], [false, true, false], [true, false, true]], 0, 2) === 2, 'countLiveNeighbors 4 failed');
    console.assert(countLiveNeighbors([[true, false, true], [false, true, false], [true, false, true]], 2, 0) === 2, 'countLiveNeighbors 5 failed');
}());

// validate isAlive rules
(function() {
    console.log('validating isAlive');
    console.assert(isAlive(true)(2) === true, 'isAlive 1 failed');
    console.assert(isAlive(true)(3) === true, 'isAlive 2 failed');
    console.assert(isAlive(true)(4) === false, 'isAlive 3 failed');
    console.assert(isAlive(false)(3) === true, 'isAlive 4 failed');
    console.assert(isAlive(false)(2) === false, 'isAlive 5 failed');
}());

// validate rules
(function() {
    console.log('validating rules');
    console.assert(rules(true)(2) === true, 'rules 1 failed');
    // console.assert(rules(true)(3) === true, 'rules 2 failed');
    // console.assert(rules(true)(4) === false, 'rules 3 failed');
    // console.assert(rules(false)(3) === true, 'rules 4 failed');
    // console.assert(rules(false)(2) === false, 'rules 5 failed');
}());

// validate nextState rules
(function() {
    // FIXME: something is up with Bluebird I think...
    console.log('validating nextState');
    console.assert(nextState(true)(2) === true, 'nextState 1 failed');
    console.assert(nextState(true)(3) === true, 'nextState 2 failed');
    console.assert(nextState(true)(4) === false, 'nextState 3 failed');
    console.assert(nextState(false)(3) === true, 'nextState 4 failed');
    console.assert(nextState(false)(2) === false, 'nextState 5 failed');
}());

// validate nextBoardState rules
(function() {
    console.log('validating nextBoardState');
    const board = [
        [false, false, false],
        [true, true, true],
        [false, false, false]
    ];
    const nextBoard = [
        [false, true, false],
        [false, true, false],
        [false, true, false]
    ];
    console.assert(JSON.stringify(nextBoardState(board)) === JSON.stringify(nextBoard), 'nextBoardState 1 failed');
}());


                                                             
                                                                           






                                                                                



                                             
                                                       











                                                                                                                    
                                                      






















                                       





























                                                               



                                                                             
                                       
                         
                                            
    
                                             
                             
                                                 










                                                                   
                                      

                                    




                                       
                                              







                                                                

                                                          


                                              


             






                                                             

                                                               
                                                                              
 







                                                                                                    



                                                                            









                                                        
                           












                                                                                                          
                                            




                                                                        
                                                               


                                                     
                                       












                                                                                      
                         



                                                                  
                                      
                                                        
                                  


                                                                                         
                                             
                                                 
                                         

                                        
                                                  




                                                         

         
                             




                                                              
 

                                               



                                                                 
                                                                                                           









                                                                                          







                                                                                               


                                                                                        
                                          
                                  
                                           




                                                                                          
                                                                    
                    
                                                     



















                                                                                           
                                           









                                                                                                                      









                                                                                
 



                                                                        







                                                    



                                                                
                                                  












                                                                     
                                                                        




                                                                      





                                                                           
                         

                                                                                                          
                                                      


                                                   

                                                                          


                                                                         
                                     

                   






                                                                              









                                                                        








                                                                            


       
                                
                                                       



                                            
                                                    
                                                    

                            





                    


                      
                  

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