LifeSimulator/js/entities/sheep.js

256 lines
8.7 KiB
JavaScript

/**
* Sheep Entity
* Grazes on grass and dense flower tufts, flees from predators,
* seeks mates when well-fed, and gives birth to growing baby sheep.
*/
import { Animal } from './animal.js';
export class Sheep extends Animal {
constructor(x, y, isBaby = false) {
super(x, y, 'sheep', isBaby);
this.grazingTimer = 0;
}
update(dt, worldWidth, worldHeight, config, plants, wolves, allSheep, particleSystem, onBabyBorn, terrain = null, audio = null) {
if (this.isDead) return;
// Deplete hunger over time using config
this.hunger -= (config.sheepHungerDepletionRate || 1.2) * dt;
// Base update (boundaries, health tick if starving, walk cycle)
super.updateBase(dt, worldWidth, worldHeight, config);
if (this.isDead) return;
// Flock Separation: maintain personal space so sheep don't bunch into a single spot
for (let i = 0; i < allSheep.length; i++) {
const other = allSheep[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 minFlockDist = 26 * this.growthScale;
if (dist < minFlockDist && dist > 0.05) {
const force = ((minFlockDist - dist) / minFlockDist) * 0.3;
this.vx -= (dx / dist) * force;
this.vy -= (dy / dist) * force;
}
}
// 1. Check for nearby threats (Wolves) - TOP PRIORITY: FLEE!
let nearestWolf = null;
let minWolfDist = config.sheepPerceptionRadius || 160;
for (let i = 0; i < wolves.length; i++) {
const w = wolves[i];
if (w.isDead) continue;
const dist = Math.hypot(w.x - this.x, w.y - this.y);
if (dist < minWolfDist) {
minWolfDist = dist;
nearestWolf = w;
}
}
if (nearestWolf) {
this.actionState = 'flee';
const fleeSpeed = config.sheepFleeSpeed || 2.8;
this.steerAwayFrom(nearestWolf.x, nearestWolf.y, fleeSpeed, worldWidth, worldHeight);
return;
}
// 2. Mating Behavior:
// If sheep is full, adult, and off cooldown -> search for another full sheep
if (this.canMate()) {
let nearestMate = null;
let minMateDist = 180;
for (let i = 0; i < allSheep.length; i++) {
const s = allSheep[i];
if (s === this || s.isDead || !s.canMate()) continue;
const dist = Math.hypot(s.x - this.x, s.y - this.y);
if (dist < minMateDist) {
minMateDist = dist;
nearestMate = s;
}
}
if (nearestMate) {
this.actionState = 'mating';
const approachSpeed = config.sheepMaxSpeed || 1.8;
this.steerTowards(nearestMate.x, nearestMate.y, approachSpeed);
const collisionThreshold = (16 * this.growthScale) + (16 * nearestMate.growthScale);
if (minMateDist <= collisionThreshold) {
const cooldown = config.sheepMatingCooldown || 22;
this.consumeMatingEnergy(cooldown);
nearestMate.consumeMatingEnergy(cooldown);
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, 'sheep');
}
}
return;
}
}
// 3. Foraging / Grazing:
// Sheep are active grazers — will seek and mow down plants if hunger < 96%
if (this.hunger < 96 || this.isBaby) {
let bestPlant = null;
let bestScore = -Infinity;
for (let i = 0; i < plants.length; i++) {
const p = plants[i];
if (p.isDead || p.growth < 0.25) continue;
const dist = Math.hypot(p.x - this.x, p.y - this.y);
if (dist > 280) continue;
const flowerBonus = p.type === 'flower' ? 50 : 0;
const score = 320 - dist + flowerBonus;
if (score > bestScore) {
bestScore = score;
bestPlant = p;
}
}
if (bestPlant) {
this.actionState = 'seek_food';
const dist = Math.hypot(bestPlant.x - this.x, bestPlant.y - this.y);
const grazeReach = Math.max(18, (config.sheepGrazingReach || 18) * this.growthScale);
if (dist <= grazeReach) {
// Rapid chomp: instantly consume & remove plant node
const nutrition = bestPlant.type === 'flower' ? 45 : 30;
bestPlant.takeBite(); // removes the node immediately
this.recordMeal(nutrition, particleSystem);
// Play synthesized retro MIDI chomp sound!
if (audio) {
audio.playSheepChomp();
}
if (terrain) {
terrain.absorbDecalsNear(bestPlant.x, bestPlant.y, 25);
}
if (particleSystem) {
particleSystem.emitEatingCrumbs(
bestPlant.x,
bestPlant.y,
bestPlant.type === 'flower' ? bestPlant.flowerColor : '#48C738',
5
);
}
} else {
this.steerTowards(bestPlant.x, bestPlant.y, config.sheepMaxSpeed || 1.8);
// Maintain flock spacing during grazing approach
for (let i = 0; i < allSheep.length; i++) {
const other = allSheep[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 minFlockDist = 26 * this.growthScale;
if (dist < minFlockDist && dist > 0.05) {
const force = ((minFlockDist - dist) / minFlockDist) * 0.35;
this.vx -= (dx / dist) * force;
this.vy -= (dy / dist) * force;
}
}
}
return;
}
}
// 4. Default: Casual Wandering
this.actionState = 'wander';
this.wander(1.1);
}
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;
// Body dimensions
const bodyW = Math.round(22 * scale);
const bodyH = Math.round(16 * scale);
const halfW = Math.round(bodyW / 2);
const halfH = Math.round(bodyH / 2);
// 1. Hooves (4 little black pixel feet bobbing)
ctx.fillStyle = '#222224';
const legW = Math.max(2, Math.round(3 * scale));
const legH = Math.max(3, Math.round(5 * scale));
const legBob1 = Math.round(Math.sin(this.walkCycle) * 2);
const legBob2 = Math.round(Math.sin(this.walkCycle + Math.PI) * 2);
// Front/Back legs
ctx.fillRect(px - halfW + 3, py + halfH - 2 + legBob1, legW, legH);
ctx.fillRect(px - halfW + 8, py + halfH - 2 + legBob2, legW, legH);
ctx.fillRect(px + halfW - 9, py + halfH - 2 + legBob2, legW, legH);
ctx.fillRect(px + halfW - 4, py + halfH - 2 + legBob1, legW, legH);
// 2. Fluffy Wool Body (Squarish pixel block with wool puff corners)
ctx.fillStyle = '#E8ECEF'; // main wool
ctx.fillRect(px - halfW, py - halfH, bodyW, bodyH);
// Wool shadow/shading underneath
ctx.fillStyle = '#CBD2D8';
ctx.fillRect(px - halfW, py + halfH - Math.round(4 * scale), bodyW, Math.round(4 * scale));
// Wool puff details
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(px - halfW + 2, py - halfH + 2, Math.round(6 * scale), Math.round(4 * scale));
ctx.fillRect(px + 2, py - halfH + 2, Math.round(5 * scale), Math.round(3 * scale));
// 3. Head & Ears
const headW = Math.round(8 * scale);
const headH = Math.round(9 * scale);
const headX = facingLeft ? px - halfW - headW + 2 : px + halfW - 2;
const headY = py - halfH + 1;
ctx.fillStyle = '#2B2B2E'; // black sheep face
ctx.fillRect(headX, headY, headW, headH);
// Ears twitch
const earX = facingLeft ? headX + headW - 2 : headX;
ctx.fillStyle = '#1A1A1C';
ctx.fillRect(earX, headY - Math.round(2 * scale), Math.round(3 * scale), Math.round(3 * scale));
// Eye (little white pixel + pupil)
const eyeX = facingLeft ? headX + 2 : headX + headW - 3;
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(eyeX, headY + 2, Math.max(1, Math.round(2 * scale)), Math.max(1, Math.round(2 * scale)));
// 4. Status Indicator (Fleeing panic / Mating heart / Baby icon)
if (this.actionState === 'flee') {
ctx.fillStyle = '#FF4444';
ctx.font = 'bold 10px monospace';
ctx.fillText('!', px - 2, py - halfH - 4);
} else if (this.actionState === 'mating') {
ctx.fillStyle = '#FF5588';
ctx.font = '8px monospace';
ctx.fillText('♥', px - 3, py - halfH - 4);
}
ctx.restore();
}
}