Files
novafarma/src/ai/NPCIdleBehavior.js
David Kotnik 55b496a2ce 🤖 AI Behavior System + Sound Manifest
 NEW SYSTEMS:
1. AnimalBehavior.js - Wander, Flee, Follow, Glowing Eyes
2. NPCIdleBehavior.js - Idle animations (hat fix, sweat wipe)
3. ANIMAL_SOUND_MANIFEST.md - 40+ sound mappings

🎮 AI FEATURES:
- Smooth movement (not grid-based) ✓
- Flee from player (100px range) ✓
- Follow with delay (cargo animals: llama, horse, donkey) ✓
- Glowing eyes in darkness (NOIR EFFECT!) ✓
  - Neon pink/green eyes visible at distance
  - Perfect for 'eyes in forest' atmosphere

👤 NPC FEATURES:
- Random idle animations every 3-8 seconds
- Fix hat, wipe sweat, look around, scratch head
- Gronk-specific: coin counting, item polishing

🔊 SOUND SYSTEM:
- Kenney pack mapping for 23 animal sounds
- Footstep variations (mud, grass, cobble)
- Noir sound design: muffled distant sounds + reverb

DEMO: 56/60 sprites (93%)!
Next: 4 remaining sprites + integration testing
2026-01-08 23:34:22 +01:00

154 lines
4.4 KiB
JavaScript

/**
* 👤 NPC IDLE BEHAVIOR SYSTEM
* "Krvava Žetev" - Gothic Farm Game
*
* Makes NPCs feel alive with random idle animations:
* - Fix hat
* - Wipe sweat
* - Look around
* - Scratch head
* - Adjust clothes
*/
export class NPCIdleBehavior {
constructor(scene, sprite, npcType) {
this.scene = scene;
this.sprite = sprite;
this.npcType = npcType;
// Idle animation timing
this.idleTimer = 0;
this.nextIdleTime = Phaser.Math.Between(3000, 8000);
// Available idle animations
this.idleAnimations = this.getIdleAnimations();
this.currentIdle = null;
}
getIdleAnimations() {
// Different NPCs have different idle behaviors
const genericIdles = [
'fix_hat',
'wipe_sweat',
'look_around',
'scratch_head',
'adjust_clothes',
'yawn',
'stretch'
];
const npcSpecific = {
'gronk': [...genericIdles, 'adjust_apron', 'count_coins', 'polish_item'],
'farmer': [...genericIdles, 'lean_on_fence', 'check_watch', 'spit'],
'merchant': [...genericIdles, 'fix_glasses', 'write_note', 'arrange_goods'],
'default': genericIdles
};
return npcSpecific[this.npcType] || npcSpecific.default;
}
update(time, delta) {
if (!this.sprite.active) return;
this.idleTimer += delta;
// Trigger random idle animation
if (this.idleTimer >= this.nextIdleTime && !this.currentIdle) {
this.playRandomIdle();
this.idleTimer = 0;
this.nextIdleTime = Phaser.Math.Between(3000, 8000);
}
}
playRandomIdle() {
const idle = Phaser.Utils.Array.GetRandom(this.idleAnimations);
this.currentIdle = idle;
// Play animation if it exists
const animKey = `${this.npcType}_${idle}`;
if (this.sprite.anims && this.sprite.anims.exists(animKey)) {
this.sprite.play(animKey);
// Return to default idle after animation
this.sprite.on('animationcomplete', () => {
this.sprite.play(`${this.npcType}_idle`);
this.currentIdle = null;
}, this);
} else {
// Fallback: simple visual effects for idle
this.playFallbackIdle(idle);
}
}
playFallbackIdle(idleType) {
// Create simple tween-based idle behaviors as fallback
switch (idleType) {
case 'fix_hat':
this.scene.tweens.add({
targets: this.sprite,
angle: -5,
duration: 200,
yoyo: true,
onComplete: () => {
this.currentIdle = null;
}
});
break;
case 'wipe_sweat':
this.scene.tweens.add({
targets: this.sprite,
scaleY: 0.98,
duration: 300,
yoyo: true,
onComplete: () => {
this.currentIdle = null;
}
});
break;
case 'look_around':
this.scene.tweens.add({
targets: this.sprite,
scaleX: -Math.abs(this.sprite.scaleX),
duration: 400,
yoyo: true,
onComplete: () => {
this.currentIdle = null;
}
});
break;
case 'scratch_head':
this.scene.tweens.add({
targets: this.sprite,
y: this.sprite.y - 5,
duration: 150,
yoyo: true,
repeat: 2,
onComplete: () => {
this.currentIdle = null;
}
});
break;
default:
// Generic bob
this.scene.tweens.add({
targets: this.sprite,
y: this.sprite.y - 3,
duration: 200,
yoyo: true,
onComplete: () => {
this.currentIdle = null;
}
});
}
}
destroy() {
this.sprite = null;
}
}