//! Villager components, spawning, needs, movement, speech bubbles. use bevy::prelude::*; use std::collections::VecDeque; use crate::persona::Persona; use crate::world::{WorldMap, GRID}; #[derive(Component)] pub struct Villager { pub persona: Persona, pub home: IVec2, } #[derive(Component, Clone, Copy)] pub struct Needs { pub hunger: f32, // 0..100, higher = hungrier pub energy: f32, // 0..100, lower = more tired pub social: f32, // 0..100, lower = lonelier } impl Default for Needs { fn default() -> Self { Self { hunger: 20.0, energy: 80.0, social: 50.0 } } } #[derive(Component)] pub struct Memory(pub VecDeque); impl Memory { pub fn push(&mut self, event: String) { if self.0.len() >= 24 { self.0.pop_front(); } self.0.push_back(event); } pub fn recent(&self, n: usize) -> impl Iterator { self.0.iter().rev().take(n).collect::>().into_iter().rev() } } #[derive(Component)] pub struct Brain { pub in_flight: bool, pub next_wake: f32, pub expect_reply: bool, } #[derive(Component)] pub struct Pathing { pub cells: Vec, // remaining cells; empty = not moving pub step_progress: f32, pub destination: String, // place name for arrival memory } impl Pathing { pub fn idle() -> Self { Self { cells: Vec::new(), step_progress: 0.0, destination: String::new() } } pub fn is_moving(&self) -> bool { !self.cells.is_empty() } } #[derive(Component)] pub struct Speech { pub text: String, pub expires: f32, } impl Default for Speech { fn default() -> Self { Self { text: String::new(), expires: 0.0 } } } #[derive(Component)] pub struct BubbleText; // marker for the Text2d child // --------------------------------------------------------------------------- // Body layer: runs at frame rate, no LLM involved // --------------------------------------------------------------------------- /// Walk the current path (grid BFS waypoints), 2 cells/sec. pub fn move_bodies( time: Res