// NPC module - NPC entity definitions and AI systems // Implements D-024: 10-axis NPC model + CombatCapability component // Background tier state machines for schedule, mood, relationships, job pub mod relationships; pub mod routine; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_ecs::schedule::IntoScheduleConfigs; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use crate::knowledge::types::{FactId, KnowledgeConfidence, StableId}; use crate::simulation::movement::TilePosition; use crate::simulation::time::DayPhase; /// NPC plugin: initializes NPC-related resources and systems. pub struct NpcPlugin; impl Plugin for NpcPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .add_systems( Update, routine::check_phase_transition .before(crate::simulation::pathfinding::compute_paths), ); tracing::debug!("NpcPlugin initialized"); } } #[derive(Component, Debug)] pub struct Npc; // --------------------------------------------------------------------------- // Axis 1: Want (D-024) // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum WantKind { Wealth, Safety, Knowledge, Connection, Power, Freedom, Justice, Revenge, } #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct Want { pub primary: WantKind, pub intensity: u8, // 1-10, integer for determinism (D-010) pub description: String, } // --------------------------------------------------------------------------- // Axis 2: Secret / vulnerability (D-024) // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum SecretSeverity { Minor, // Social embarrassment Moderate, // Career-threatening Major, // Criminal / life-threatening } #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct Secret { pub description: String, pub severity: SecretSeverity, pub known_by: Vec, } // --------------------------------------------------------------------------- // Axis 3: Relationships 1-3 (D-024) // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum RelationshipKind { Colleague, Friend, Rival, Romantic, Family, Superior, Subordinate, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RelationshipEvent { pub tick: u64, pub description: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Relationship { pub target_id: StableId, pub kind: RelationshipKind, pub trust_level: i8, // -10..+10, integer for determinism (D-010) pub history: Vec, } /// Per-NPC relationship slots. D-024: 3 key relationships for Active-tier. pub const MAX_KEY_RELATIONSHIPS: usize = 3; #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct Relationships { pub entries: Vec, } // --------------------------------------------------------------------------- // Axis 4: Tolerance threshold (D-024) // --------------------------------------------------------------------------- #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct ToleranceThreshold { pub current_stress: i16, // 0-100, integer for determinism (D-010) pub threshold: i16, } // --------------------------------------------------------------------------- // Axis 5: Daily routine (D-024, D-031) // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RoutineEntry { pub phase: DayPhase, pub location: TilePosition, pub activity: String, } #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct DailyRoutine { pub entries: Vec, pub description: String, } impl DailyRoutine { /// Get the routine entry for a given day phase. pub fn entry_for_phase(&self, phase: DayPhase) -> Option<&RoutineEntry> { self.entries.iter().find(|e| e.phase == phase) } /// Get the expected location for a given day phase. pub fn expected_location(&self, phase: DayPhase) -> Option { self.entry_for_phase(phase).map(|e| e.location) } } // --------------------------------------------------------------------------- // Axis 6: Information inventory (D-024) // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KnownFact { pub fact_id: FactId, pub confidence: KnowledgeConfidence, } #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct InformationInventory { pub facts: Vec, } // --------------------------------------------------------------------------- // Axis 7: Contentment (D-024, Gore's thematic axis) // --------------------------------------------------------------------------- #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct Contentment { pub level: i16, // -100..+100, integer for determinism (D-010) } // --------------------------------------------------------------------------- // Supporting axis 1: Personality traits (D-024) // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum PersonalityTrait { Cautious, Bold, Honest, Deceptive, Compassionate, Ruthless, Curious, Incurious, Social, Reclusive, } #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct PersonalityTraits { pub traits: Vec, } // --------------------------------------------------------------------------- // Supporting axis 2: Tell system (D-024) // --------------------------------------------------------------------------- #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum TellTrigger { StressAboveThreshold, NearSpecificEntity(StableId), DuringActivity(String), TimeOfDay(DayPhase), Always, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tell { pub trigger: TellTrigger, pub behavior: String, } #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct TellSystem { pub tells: Vec, } // --------------------------------------------------------------------------- // Supporting axis 3: Skill set + combat component (D-024) // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum Skill { Combat, Intimidation, Medical, Observation, Persuasion, Piloting, Stealth, Technical, } #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct SkillSet { pub skills: BTreeMap, // Skill -> proficiency (1-10), BTreeMap for determinism pub combat_trained: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum CombatStyle { Ranged, Melee, Evasive, Defensive, } #[derive(Component, Debug, Clone, Serialize, Deserialize)] pub struct CombatCapability { pub weapon_proficiency: u8, // 1-10 pub combat_style: CombatStyle, } #[cfg(test)] mod tests { use super::*; #[test] fn daily_routine_entry_for_phase() { let routine = DailyRoutine { entries: vec![ RoutineEntry { phase: DayPhase::Morning, location: TilePosition::new(5, 5, 0), activity: "Work".into(), }, RoutineEntry { phase: DayPhase::Evening, location: TilePosition::new(10, 10, 0), activity: "Bar".into(), }, ], description: "Test routine".into(), }; assert_eq!( routine.expected_location(DayPhase::Morning), Some(TilePosition::new(5, 5, 0)) ); assert_eq!( routine.expected_location(DayPhase::Evening), Some(TilePosition::new(10, 10, 0)) ); assert_eq!(routine.expected_location(DayPhase::Afternoon), None); assert_eq!(routine.expected_location(DayPhase::Night), None); } #[test] fn skill_set_btreemap_deterministic() { let mut skills1 = BTreeMap::new(); skills1.insert(Skill::Combat, 5); skills1.insert(Skill::Stealth, 3); skills1.insert(Skill::Persuasion, 7); let mut skills2 = BTreeMap::new(); skills2.insert(Skill::Persuasion, 7); skills2.insert(Skill::Combat, 5); skills2.insert(Skill::Stealth, 3); // Insertion order doesn't matter — iteration is deterministic let keys1: Vec<_> = skills1.keys().collect(); let keys2: Vec<_> = skills2.keys().collect(); assert_eq!(keys1, keys2); } #[test] fn relationship_max_entries() { let rels = Relationships { entries: vec![ Relationship { target_id: StableId(1), kind: RelationshipKind::Friend, trust_level: 5, history: vec![], }, Relationship { target_id: StableId(2), kind: RelationshipKind::Colleague, trust_level: 2, history: vec![], }, Relationship { target_id: StableId(3), kind: RelationshipKind::Rival, trust_level: -3, history: vec![], }, ], }; assert_eq!(rels.entries.len(), MAX_KEY_RELATIONSHIPS); } }