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