LifeSimulator/js/entities/animal.js

219 lines
6.4 KiB
JavaScript
Raw Permalink Normal View History

/**
* Base Animal Entity Class
* Encapsulates movement, steering behaviors, soft boundaries, hunger, health,
* reproduction prerequisites, baby growth cycle, and walking bob animations.
*/
export class Animal {
constructor(x, y, species, isBaby = false) {
this.x = x;
this.y = y;
this.species = species; // 'sheep' or 'wolf'
// Movement
this.vx = (Math.random() - 0.5) * 1.5;
this.vy = (Math.random() - 0.5) * 1.5;
this.heading = Math.atan2(this.vy, this.vx);
this.wanderAngle = Math.random() * Math.PI * 2;
// Biological stats
this.health = 100;
this.hunger = isBaby ? 80 : 75; // 0 to 100
this.age = 0;
this.isDead = false;
this.deathCause = null;
// Baby & Growth
this.isBaby = isBaby;
this.foodEatenAsBaby = 0;
this.growthScale = isBaby ? 0.55 : 1.0;
// Mating
this.mateCooldown = 12 + Math.random() * 6; // seconds before ready
this.isSeekingMate = false;
// Animation
this.walkCycle = Math.random() * Math.PI * 2;
this.bobOffset = 0;
this.actionState = 'wander'; // 'wander', 'seek_food', 'flee', 'hunt', 'mating'
this.actionTimer = 0;
}
updateBase(dt, worldWidth, worldHeight, config) {
if (this.isDead) return;
this.age += dt;
this.walkCycle += Math.hypot(this.vx, this.vy) * dt * 4.5;
this.bobOffset = Math.abs(Math.sin(this.walkCycle)) * 2.5;
// Mating cooldown
if (this.mateCooldown > 0) {
this.mateCooldown -= dt;
}
// Health decay if starving
if (this.hunger <= 0) {
this.hunger = 0;
this.health -= 8.0 * dt; // dies in 12.5s without food
if (this.health <= 0) {
this.health = 0;
this.die('starvation');
return;
}
}
// Soft boundary steering
this.applyBoundarySteering(worldWidth, worldHeight, config.boundaryMargin || 45);
// Integrate position
this.x += this.vx * dt * 45;
this.y += this.vy * dt * 45;
// Update facing heading if moving
const currentSpeed = Math.hypot(this.vx, this.vy);
if (currentSpeed > 0.05) {
this.heading = Math.atan2(this.vy, this.vx);
}
}
applyBoundarySteering(width, height, margin = 55) {
const turnForce = 0.45;
const bodyPadding = 22; // Keeps entire head and body comfortably inside visible canvas
// Proactive steering away from boundaries
if (this.x < margin) {
this.vx += turnForce * ((margin - this.x) / margin);
} else if (this.x > width - margin) {
this.vx -= turnForce * ((this.x - (width - margin)) / margin);
}
if (this.y < margin) {
this.vy += turnForce * ((margin - this.y) / margin);
} else if (this.y > height - margin) {
this.vy -= turnForce * ((this.y - (height - margin)) / margin);
}
// Physical boundary bounce: reflect velocity inward so animals never get stuck facing walls
if (this.x <= bodyPadding) {
this.x = bodyPadding;
if (this.vx < 0) {
this.vx = Math.abs(this.vx) * 0.7 + 0.3;
this.wanderAngle = 0; // face inward (right)
}
} else if (this.x >= width - bodyPadding) {
this.x = width - bodyPadding;
if (this.vx > 0) {
this.vx = -Math.abs(this.vx) * 0.7 - 0.3;
this.wanderAngle = Math.PI; // face inward (left)
}
}
if (this.y <= bodyPadding) {
this.y = bodyPadding;
if (this.vy < 0) {
this.vy = Math.abs(this.vy) * 0.7 + 0.3;
this.wanderAngle = Math.PI * 0.5; // face inward (down)
}
} else if (this.y >= height - bodyPadding) {
this.y = height - bodyPadding;
if (this.vy > 0) {
this.vy = -Math.abs(this.vy) * 0.7 - 0.3;
this.wanderAngle = Math.PI * 1.5; // face inward (up)
}
}
}
wander(speed = 1.2) {
this.wanderAngle += (Math.random() - 0.5) * 0.45;
const targetVx = Math.cos(this.wanderAngle) * speed;
const targetVy = Math.sin(this.wanderAngle) * speed;
this.vx += (targetVx - this.vx) * 0.1;
this.vy += (targetVy - this.vy) * 0.1;
}
steerTowards(targetX, targetY, speed = 2.0, turnRate = 0.15) {
const dx = targetX - this.x;
const dy = targetY - this.y;
const dist = Math.hypot(dx, dy);
if (dist > 1) {
const targetVx = (dx / dist) * speed;
const targetVy = (dy / dist) * speed;
this.vx += (targetVx - this.vx) * turnRate;
this.vy += (targetVy - this.vy) * turnRate;
}
}
steerAwayFrom(hazardX, hazardY, speed = 2.6, worldWidth = 1280, worldHeight = 760) {
let dx = this.x - hazardX;
let dy = this.y - hazardY;
// Wall deflection: curve along perimeter into open field rather than pinning against wall
const wallBuffer = 55;
if (this.x < wallBuffer) dx += 1.8;
else if (this.x > worldWidth - wallBuffer) dx -= 1.8;
if (this.y < wallBuffer) dy += 1.8;
else if (this.y > worldHeight - wallBuffer) dy -= 1.8;
const dist = Math.hypot(dx, dy);
if (dist > 0.1) {
const targetVx = (dx / dist) * speed;
const targetVy = (dy / dist) * speed;
this.vx += (targetVx - this.vx) * 0.25;
this.vy += (targetVy - this.vy) * 0.25;
}
}
recordMeal(nutritionAmount = 40, particleSystem = null) {
this.hunger = Math.min(100, this.hunger + nutritionAmount);
if (this.isBaby) {
this.foodEatenAsBaby++;
// Baby grows progressively with each meal:
// 0 meals: 0.55 scale, 1 meal: 0.70 scale, 2 meals: 0.85 scale, 3 meals: 1.0 adult!
this.growthScale = 0.55 + (this.foodEatenAsBaby / 3) * 0.45;
if (this.foodEatenAsBaby >= 3) {
this.isBaby = false;
this.growthScale = 1.0;
if (particleSystem) {
particleSystem.emitGrowthSparkles(this.x, this.y, 14);
}
}
}
}
canMate() {
return !this.isBaby && this.hunger >= 82 && this.mateCooldown <= 0;
}
consumeMatingEnergy(cooldown = 20) {
this.hunger = Math.max(25, this.hunger - 45);
this.mateCooldown = cooldown + Math.random() * 6;
}
takeDamage(amount, fromAngle = 0, particleSystem = null) {
this.health -= amount;
// Bite strike knockback impulse pushes prey away from predator
const knockback = 2.6;
this.vx += Math.cos(fromAngle) * knockback;
this.vy += Math.sin(fromAngle) * knockback;
if (particleSystem) {
particleSystem.emitBloodSpray(this.x, this.y, fromAngle, 12);
}
if (this.health <= 0) {
this.health = 0;
this.die('wolf');
return true; // died
}
return false;
}
die(cause = 'starvation') {
this.isDead = true;
this.deathCause = cause;
}
}