about summary refs log tree commit diff stats
path: root/html/plains/enemies.js
diff options
context:
space:
mode:
Diffstat (limited to 'html/plains/enemies.js')
-rw-r--r--html/plains/enemies.js438
1 files changed, 438 insertions, 0 deletions
diff --git a/html/plains/enemies.js b/html/plains/enemies.js
new file mode 100644
index 0000000..3d40c3e
--- /dev/null
+++ b/html/plains/enemies.js
@@ -0,0 +1,438 @@
+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
+}; 
\ No newline at end of file