LifeSimulator/js/entities/wolf.js

318 lines
12 KiB
JavaScript

/**
* 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();
}
}