279 lines
8.2 KiB
JavaScript
279 lines
8.2 KiB
JavaScript
/**
|
|
* 🐮 ANIMAL AI BEHAVIOR SYSTEM
|
|
* "Krvava Žetev" - Gothic Farm Game
|
|
*
|
|
* Behaviors:
|
|
* - Wander: Random smooth movement (not grid-based)
|
|
* - Flee: Run away from player when close
|
|
* - Glowing Eyes: Neon eyes visible in darkness (noir effect!)
|
|
* - Follow: Cargo animals (llama, horse) follow with delay
|
|
*/
|
|
|
|
export class AnimalBehavior {
|
|
constructor(scene, sprite, animalType) {
|
|
this.scene = scene;
|
|
this.sprite = sprite;
|
|
this.animalType = animalType;
|
|
|
|
// Behavior states
|
|
this.state = 'wander'; // 'wander', 'flee', 'follow', 'idle'
|
|
this.wanderTimer = 0;
|
|
this.wanderDuration = Phaser.Math.Between(2000, 5000);
|
|
this.wanderAngle = Math.random() * Math.PI * 2;
|
|
|
|
// Movement params
|
|
this.speed = this.getSpeed();
|
|
this.fleeDistance = 100; // pixels
|
|
this.fleeSpeed = this.speed * 2;
|
|
|
|
// Glowing eyes effect (NOIR!)
|
|
this.glowingEyes = null;
|
|
this.eyeColor = this.getEyeColor();
|
|
this.createGlowingEyes();
|
|
|
|
// Cargo follow system (for llama, horse, donkey)
|
|
this.isCargoAnimal = ['llama', 'horse', 'donkey'].includes(animalType);
|
|
this.followTarget = null;
|
|
this.followDelay = 0.5; // seconds
|
|
this.followHistory = [];
|
|
}
|
|
|
|
getSpeed() {
|
|
const speeds = {
|
|
cow: 30,
|
|
pig: 35,
|
|
sheep: 25,
|
|
chicken: 50,
|
|
duck: 45,
|
|
goat: 40,
|
|
horse: 60,
|
|
rabbit: 70,
|
|
donkey: 35,
|
|
llama: 40
|
|
};
|
|
return speeds[this.animalType] || 30;
|
|
}
|
|
|
|
getEyeColor() {
|
|
// Noir glowing eyes - pink or green like Kai's dreads!
|
|
const colors = {
|
|
cow: 0xff0066, // neon pink
|
|
pig: 0xff0066, // neon pink
|
|
sheep: 0x00ff88, // neon green
|
|
chicken: 0xffff00, // yellow
|
|
duck: 0x00ffff, // cyan
|
|
goat: 0xff0066, // neon pink
|
|
horse: 0x00ff88, // neon green
|
|
rabbit: 0xff00ff, // purple
|
|
donkey: 0x00ff88, // neon green
|
|
llama: 0xff0066 // neon pink
|
|
};
|
|
return colors[this.animalType] || 0xff0066;
|
|
}
|
|
|
|
createGlowingEyes() {
|
|
// Create glowing eye sprites for noir darkness effect
|
|
this.glowingEyes = this.scene.add.container(this.sprite.x, this.sprite.y);
|
|
|
|
// Two small glowing circles for eyes
|
|
const leftEye = this.scene.add.circle(-8, -10, 3, this.eyeColor);
|
|
const rightEye = this.scene.add.circle(8, -10, 3, this.eyeColor);
|
|
|
|
// Add glow effect
|
|
leftEye.setAlpha(0.8);
|
|
rightEye.setAlpha(0.8);
|
|
|
|
this.glowingEyes.add([leftEye, rightEye]);
|
|
|
|
// Hide by default, show only in darkness or at distance
|
|
this.glowingEyes.setVisible(false);
|
|
this.glowingEyes.setDepth(this.sprite.depth + 1);
|
|
}
|
|
|
|
update(time, delta) {
|
|
if (!this.sprite.active) return;
|
|
|
|
// Update glowing eyes position
|
|
if (this.glowingEyes) {
|
|
this.glowingEyes.setPosition(this.sprite.x, this.sprite.y);
|
|
this.updateGlowingEyesVisibility();
|
|
}
|
|
|
|
// Behavior state machine
|
|
switch (this.state) {
|
|
case 'wander':
|
|
this.updateWander(time, delta);
|
|
break;
|
|
case 'flee':
|
|
this.updateFlee(time, delta);
|
|
break;
|
|
case 'follow':
|
|
this.updateFollow(time, delta);
|
|
break;
|
|
case 'idle':
|
|
this.updateIdle(time, delta);
|
|
break;
|
|
}
|
|
|
|
// Check for player proximity (flee trigger)
|
|
this.checkPlayerProximity();
|
|
}
|
|
|
|
updateWander(time, delta) {
|
|
this.wanderTimer += delta;
|
|
|
|
// Change direction periodically
|
|
if (this.wanderTimer >= this.wanderDuration) {
|
|
this.wanderTimer = 0;
|
|
this.wanderDuration = Phaser.Math.Between(2000, 5000);
|
|
this.wanderAngle = Math.random() * Math.PI * 2;
|
|
|
|
// Sometimes stop and idle
|
|
if (Math.random() < 0.3) {
|
|
this.state = 'idle';
|
|
this.sprite.setVelocity(0, 0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Smooth movement in wander direction
|
|
const vx = Math.cos(this.wanderAngle) * this.speed;
|
|
const vy = Math.sin(this.wanderAngle) * this.speed;
|
|
this.sprite.setVelocity(vx, vy);
|
|
}
|
|
|
|
updateFlee(time, delta) {
|
|
const player = this.scene.player;
|
|
if (!player) return;
|
|
|
|
// Flee away from player
|
|
const angle = Phaser.Math.Angle.Between(
|
|
player.x, player.y,
|
|
this.sprite.x, this.sprite.y
|
|
);
|
|
|
|
const vx = Math.cos(angle) * this.fleeSpeed;
|
|
const vy = Math.sin(angle) * this.fleeSpeed;
|
|
this.sprite.setVelocity(vx, vy);
|
|
|
|
// Return to wander if far enough
|
|
const dist = Phaser.Math.Distance.Between(
|
|
player.x, player.y,
|
|
this.sprite.x, this.sprite.y
|
|
);
|
|
|
|
if (dist > this.fleeDistance * 2) {
|
|
this.state = 'wander';
|
|
}
|
|
}
|
|
|
|
updateFollow(time, delta) {
|
|
if (!this.followTarget || !this.isCargoAnimal) return;
|
|
|
|
// Record follow history for delayed follow
|
|
this.followHistory.push({
|
|
x: this.followTarget.x,
|
|
y: this.followTarget.y,
|
|
time: time
|
|
});
|
|
|
|
// Remove old history
|
|
const cutoffTime = time - (this.followDelay * 1000);
|
|
this.followHistory = this.followHistory.filter(h => h.time > cutoffTime);
|
|
|
|
// Follow delayed position
|
|
if (this.followHistory.length > 0) {
|
|
const target = this.followHistory[0];
|
|
const angle = Phaser.Math.Angle.Between(
|
|
this.sprite.x, this.sprite.y,
|
|
target.x, target.y
|
|
);
|
|
|
|
const dist = Phaser.Math.Distance.Between(
|
|
this.sprite.x, this.sprite.y,
|
|
target.x, target.y
|
|
);
|
|
|
|
// Only move if far enough
|
|
if (dist > 50) {
|
|
const vx = Math.cos(angle) * this.speed;
|
|
const vy = Math.sin(angle) * this.speed;
|
|
this.sprite.setVelocity(vx, vy);
|
|
} else {
|
|
this.sprite.setVelocity(0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateIdle(time, delta) {
|
|
// Stop moving
|
|
this.sprite.setVelocity(0, 0);
|
|
|
|
// Return to wander after random time
|
|
if (Math.random() < 0.01) { // ~1% chance per frame
|
|
this.state = 'wander';
|
|
}
|
|
}
|
|
|
|
checkPlayerProximity() {
|
|
const player = this.scene.player;
|
|
if (!player) return;
|
|
|
|
const dist = Phaser.Math.Distance.Between(
|
|
player.x, player.y,
|
|
this.sprite.x, this.sprite.y
|
|
);
|
|
|
|
// Trigger flee if player too close
|
|
if (dist < this.fleeDistance && this.state !== 'flee') {
|
|
this.state = 'flee';
|
|
|
|
// Play flee sound if available
|
|
if (this.scene.sound.get(`${this.animalType}_flee`)) {
|
|
this.scene.sound.play(`${this.animalType}_flee`);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateGlowingEyesVisibility() {
|
|
if (!this.glowingEyes) return;
|
|
|
|
const player = this.scene.player;
|
|
if (!player) return;
|
|
|
|
const dist = Phaser.Math.Distance.Between(
|
|
player.x, player.y,
|
|
this.sprite.x, this.sprite.y
|
|
);
|
|
|
|
// Show glowing eyes in darkness or at distance
|
|
// NOIR EFFECT: When Kai shines light in forest, just glowing eyes visible!
|
|
const isDark = this.scene.darkness || false; // Check if darkness system active
|
|
const isDistant = dist > 200 && dist < 400; // Medium distance
|
|
|
|
if (isDark || isDistant) {
|
|
this.glowingEyes.setVisible(true);
|
|
// Hide actual sprite if far
|
|
if (dist > 300) {
|
|
this.sprite.setAlpha(0.2);
|
|
}
|
|
} else {
|
|
this.glowingEyes.setVisible(false);
|
|
this.sprite.setAlpha(1.0);
|
|
}
|
|
}
|
|
|
|
setFollowTarget(target) {
|
|
if (this.isCargoAnimal) {
|
|
this.followTarget = target;
|
|
this.state = 'follow';
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
if (this.glowingEyes) {
|
|
this.glowingEyes.destroy();
|
|
}
|
|
}
|
|
}
|