/** * Wolf Entity * Stalks and hunts sheep via multi-bite combat, triggers blood splatter decals, * feeds on prey, mates when satiated, and nurtures growing wolf pups. */ import { Animal } from './animal.js'; export class Wolf extends Animal { constructor(x, y, isBaby = false) { super(x, y, 'wolf', isBaby); this.attackCooldownTimer = 0; } canMate(threshold = 86) { return !this.isBaby && this.hunger >= threshold && this.mateCooldown <= 0; } update(dt, worldWidth, worldHeight, config, sheepList, allWolves, terrain, particleSystem, onBabyBorn, onSheepKilled, audio = null) { if (this.isDead) return; // Deplete hunger over time (gentler for growing pups) const baseDeplete = (config.wolfHungerDepletionRate || 1.3) * (this.isBaby ? 0.8 : 1.0); const isHunting = this.actionState === 'hunt'; this.hunger -= (isHunting ? baseDeplete * 1.4 : baseDeplete) * dt; if (this.attackCooldownTimer > 0) { this.attackCooldownTimer -= dt; } // Base update (boundaries, health tick if starving, walk cycle) super.updateBase(dt, worldWidth, worldHeight, config); if (this.isDead) return; // Pack Separation: maintain personal space so wolves don't stack on top of each other for (let i = 0; i < allWolves.length; i++) { const other = allWolves[i]; if (other === this || other.isDead) continue; const dx = other.x - this.x; const dy = other.y - this.y; const dist = Math.hypot(dx, dy); const minPackDist = 32 * this.growthScale; if (dist < minPackDist && dist > 0.05) { const force = ((minPackDist - dist) / minPackDist) * 0.35; this.vx -= (dx / dist) * force; this.vy -= (dy / dist) * force; } } // 1. Mating Behavior: // Requires high satiety (wolfMatingThreshold) and long cooldown const mateThreshold = config.wolfMatingThreshold || 86; const mateCooldown = config.wolfMatingCooldown || 35; if (this.canMate(mateThreshold)) { let nearestMate = null; let minMateDist = 220; for (let i = 0; i < allWolves.length; i++) { const w = allWolves[i]; if (w === this || w.isDead || !w.canMate(mateThreshold)) continue; const dist = Math.hypot(w.x - this.x, w.y - this.y); if (dist < minMateDist) { minMateDist = dist; nearestMate = w; } } if (nearestMate) { this.actionState = 'mating'; const approachSpeed = config.wolfMaxSpeed || 2.2; this.steerTowards(nearestMate.x, nearestMate.y, approachSpeed); const collisionThreshold = (18 * this.growthScale) + (18 * nearestMate.growthScale); if (minMateDist <= collisionThreshold) { this.consumeMatingEnergy(mateCooldown); nearestMate.consumeMatingEnergy(mateCooldown); if (particleSystem) { particleSystem.emitHeart(this.x, this.y); particleSystem.emitHeart(nearestMate.x, nearestMate.y); } if (audio) { audio.playHeartChime(); } if (onBabyBorn) { const babyX = (this.x + nearestMate.x) / 2 + (Math.random() - 0.5) * 12; const babyY = (this.y + nearestMate.y) / 2 + (Math.random() - 0.5) * 12; onBabyBorn(babyX, babyY, 'wolf'); } } return; } } // 2. Hunting & Scavenging Behavior: if (this.hunger < 92 || this.isBaby) { // 2a. Scavenging: Baby wolves can feast on fresh sheep carcasses left by the pack if (this.isBaby && terrain && terrain.carcasses.length > 0 && this.hunger < 85) { let nearestCarcass = null; let minCarcassDist = 200; for (let i = 0; i < terrain.carcasses.length; i++) { const c = terrain.carcasses[i]; if (c.species === 'sheep' && c.decayProgress < 0.75) { const d = Math.hypot(c.x - this.x, c.y - this.y); if (d < minCarcassDist) { minCarcassDist = d; nearestCarcass = c; } } } if (nearestCarcass) { this.actionState = 'hunt'; if (minCarcassDist <= 24) { // Feast on carcass this.recordMeal(35, particleSystem); if (audio) { audio.playWolfChomp(); } nearestCarcass.decayProgress = Math.min(1.0, nearestCarcass.decayProgress + 0.18); if (particleSystem) { particleSystem.emitEatingCrumbs(nearestCarcass.x, nearestCarcass.y, '#8A1010', 4); } return; } else { this.steerTowards(nearestCarcass.x, nearestCarcass.y, 2.6, 0.2); return; } } } // 2b. Hunt Live Sheep: let targetSheep = null; let minSheepDist = config.wolfPerceptionRadius || 260; for (let i = 0; i < sheepList.length; i++) { const s = sheepList[i]; if (s.isDead) continue; const dist = Math.hypot(s.x - this.x, s.y - this.y); // Baby wolves prioritize baby sheep or nearby targets const scoreDist = (this.isBaby && s.isBaby) ? dist * 0.6 : dist; if (scoreDist < minSheepDist) { minSheepDist = scoreDist; targetSheep = s; } } if (targetSheep) { this.actionState = 'hunt'; const rawDist = Math.hypot(targetSheep.x - this.x, targetSheep.y - this.y); // Fast, agile chase speed (4.1+ vs sheep's 2.8) so wolves reliably catch targets without agonizing long chases const chaseSpeed = (config.wolfChaseSpeed || 4.1) * (this.isBaby ? 1.05 : 1.0); const turnRate = this.isBaby ? 0.25 : 0.22; // Attack reach ensures wolf strikes cleanly before collision bounce const attackDist = Math.max(30, 34 * this.growthScale); if (rawDist <= attackDist) { // Attack range! Multi-bite combat if (this.attackCooldownTimer <= 0) { this.attackCooldownTimer = config.wolfAttackCooldown || 0.65; // Wolves expend health on attack lunge (struggle/exertion) - reduced for babies const baseLunge = config.wolfLungeCost !== undefined ? config.wolfLungeCost : 7; const lungeCost = this.isBaby ? baseLunge * 0.35 : baseLunge; this.health -= lungeCost; if (this.health <= 0) { const biteDamage = (config.wolfAttackDamage || 38) * (this.isBaby ? 0.75 : 1.0); if (targetSheep.health <= biteDamage) { // Clutch kill! Spent last burst of energy to take down prey and survive this.health = 18; } else { this.health = 0; this.die('exertion'); return; } } const biteDamage = (config.wolfAttackDamage || 38) * (this.isBaby ? 0.75 : 1.0); const biteAngle = Math.atan2(targetSheep.y - this.y, targetSheep.x - this.x); const sheepDied = targetSheep.takeDamage(biteDamage, biteAngle, particleSystem); // Play synthesized arcade wolf chomp / crunch! if (audio) { audio.playWolfChomp(); } // Stamp blood on the ground where attack occurred if (terrain) { terrain.addBloodSplotch(targetSheep.x, targetSheep.y, 4 + Math.random() * 3); } if (sheepDied) { // Target killed! Wolf feasts this.recordMeal(42, particleSystem); if (onSheepKilled) onSheepKilled(targetSheep, this); } else { // Taste on bite strike (sustains pursuit and rewards successful strikes) this.recordMeal(this.isBaby ? 16 : 10, particleSystem); } } } else { // Sprint towards sheep with high agility this.steerTowards(targetSheep.x, targetSheep.y, chaseSpeed, turnRate); // Pack dispersion during chase: wolves flank around target rather than stacking in single file for (let i = 0; i < allWolves.length; i++) { const other = allWolves[i]; if (other === this || other.isDead) continue; const dx = other.x - this.x; const dy = other.y - this.y; const dist = Math.hypot(dx, dy); const minPackDist = 34 * this.growthScale; if (dist < minPackDist && dist > 0.05) { const force = ((minPackDist - dist) / minPackDist) * 0.4; this.vx -= (dx / dist) * force; this.vy -= (dy / dist) * force; } } } return; } } // 3. Default: Stalking / Wandering this.actionState = 'wander'; this.wander(1.3); } draw(ctx) { if (this.isDead) return; ctx.save(); const scale = this.growthScale; const px = Math.round(this.x); const py = Math.round(this.y - this.bobOffset); // Direction flip const facingLeft = Math.cos(this.heading) < 0; // Dimensions const bodyW = Math.round(26 * scale); const bodyH = Math.round(14 * scale); const halfW = Math.round(bodyW / 2); const halfH = Math.round(bodyH / 2); // 1. Paws / Legs (4 sleek grey legs bobbing) ctx.fillStyle = '#26292C'; const legW = Math.max(2, Math.round(3 * scale)); const legH = Math.max(4, Math.round(6 * scale)); const legBob1 = Math.round(Math.sin(this.walkCycle) * 2.5); const legBob2 = Math.round(Math.sin(this.walkCycle + Math.PI) * 2.5); ctx.fillRect(px - halfW + 3, py + halfH - 2 + legBob1, legW, legH); ctx.fillRect(px - halfW + 9, py + halfH - 2 + legBob2, legW, legH); ctx.fillRect(px + halfW - 10, py + halfH - 2 + legBob2, legW, legH); ctx.fillRect(px + halfW - 4, py + halfH - 2 + legBob1, legW, legH); // 2. Tail const tailX = facingLeft ? px + halfW - 2 : px - halfW - Math.round(6 * scale) + 2; const tailBob = Math.round(Math.sin(this.walkCycle * 1.5) * 2); ctx.fillStyle = '#3F4448'; ctx.fillRect(tailX, py - Math.round(2 * scale) + tailBob, Math.round(6 * scale), Math.round(3 * scale)); // 3. Torso ctx.fillStyle = '#44494E'; // slate grey coat ctx.fillRect(px - halfW, py - halfH, bodyW, bodyH); // Belly & Back shading ctx.fillStyle = '#2C3033'; // underbelly shadow ctx.fillRect(px - halfW, py + halfH - Math.round(3 * scale), bodyW, Math.round(3 * scale)); ctx.fillStyle = '#5A6066'; // back highlight ctx.fillRect(px - halfW + 2, py - halfH, bodyW - 4, Math.round(2 * scale)); // 4. Head, Muzzle & Pointed Ears const headW = Math.round(10 * scale); const headH = Math.round(9 * scale); const headX = facingLeft ? px - halfW - headW + 2 : px + halfW - 2; const headY = py - halfH - 1; // Head base ctx.fillStyle = '#3A3F43'; ctx.fillRect(headX, headY, headW, headH); // Snout / Muzzle const snoutW = Math.round(5 * scale); const snoutH = Math.round(4 * scale); const snoutX = facingLeft ? headX - snoutW : headX + headW; ctx.fillStyle = '#2B2F32'; ctx.fillRect(snoutX, headY + headH - snoutH - 1, snoutW, snoutH); // Pointed Ears const earX = facingLeft ? headX + headW - Math.round(3 * scale) : headX; ctx.fillStyle = '#232629'; ctx.fillRect(earX, headY - Math.round(4 * scale), Math.round(3 * scale), Math.round(4 * scale)); // Predator Eye (Glowing Amber/Red) const eyeX = facingLeft ? headX + 3 : headX + headW - 4; ctx.fillStyle = this.actionState === 'hunt' ? '#FF2200' : '#FFB703'; ctx.fillRect(eyeX, headY + 2, Math.max(1, Math.round(2 * scale)), Math.max(1, Math.round(2 * scale))); // 5. Status Indicator if (this.actionState === 'hunt') { ctx.fillStyle = '#FF2222'; ctx.font = 'bold 9px monospace'; ctx.fillText('⚡', px - 3, py - halfH - 5); } else if (this.actionState === 'mating') { ctx.fillStyle = '#FF5588'; ctx.font = '8px monospace'; ctx.fillText('♥', px - 3, py - halfH - 5); } ctx.restore(); } }