From a6538306b08f8b1748ea1046971f8f6f35b9c090 Mon Sep 17 00:00:00 2001 From: M1thieu <18742831+M1thieu@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:31:02 +0100 Subject: [PATCH 01/15] Document systems aggregator --- crates/systems/README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/systems/README.md b/crates/systems/README.md index 0a111f9..8ae4daf 100644 --- a/crates/systems/README.md +++ b/crates/systems/README.md @@ -1,3 +1,32 @@ # Systems -High-level systems including AI, physics solvers, acoustics, and save systems. \ No newline at end of file +The `systems` workspace crate bundles LP's high-level simulation layers +(decision-making, acoustics, future MPM integration, and persistence) so they +can be added to a Bevy app with a single plugin. It sits on top of the domain +crates (`energy`, `forces`, `information`, `matter`) and takes care of wiring and +scheduling. + +## Modules + +- `ai` - utility-driven agency used by LP's creatures and actors. +- `acoustics` - scaffold for physics-based sound that will hook into matter and + wave simulation. +- `mpm` - placeholder for the upcoming Material Point Method solver. +- `save_system` - shared save / load infrastructure. + +Each module exposes a `prelude` for selective use, while +`systems::SystemsPlugin` pulls everything together. + +## Quick start + +```rust +use bevy::prelude::*; +use systems::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(SystemsPlugin::default()) + .run(); +} +``` From 5fbd3e7794ac66de6b3a7633e055352ae9345ffd Mon Sep 17 00:00:00 2001 From: M1thieu <18742831+M1thieu@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:42:03 +0100 Subject: [PATCH 02/15] Rename pbmpm systems crate to mpm --- crates/systems/Cargo.toml | 2 +- crates/systems/{pbmpm => mpm}/Cargo.toml | 2 +- crates/systems/{pbmpm => mpm}/src/grid.rs | 0 crates/systems/{pbmpm => mpm}/src/lib.rs | 12 ++++++------ crates/systems/{pbmpm => mpm}/src/particle.rs | 0 crates/systems/{pbmpm => mpm}/src/solver.rs | 0 crates/systems/{pbmpm => mpm}/src/transfer.rs | 0 crates/systems/src/lib.rs | 6 +++--- 8 files changed, 11 insertions(+), 11 deletions(-) rename crates/systems/{pbmpm => mpm}/Cargo.toml (91%) rename crates/systems/{pbmpm => mpm}/src/grid.rs (100%) rename crates/systems/{pbmpm => mpm}/src/lib.rs (68%) rename crates/systems/{pbmpm => mpm}/src/particle.rs (100%) rename crates/systems/{pbmpm => mpm}/src/solver.rs (100%) rename crates/systems/{pbmpm => mpm}/src/transfer.rs (100%) diff --git a/crates/systems/Cargo.toml b/crates/systems/Cargo.toml index a335f94..33cf35d 100644 --- a/crates/systems/Cargo.toml +++ b/crates/systems/Cargo.toml @@ -7,6 +7,6 @@ description = "Core simulation infrastructure components that manage and support [dependencies] bevy = "0.17" save_system = { path = "./save_system" } -pbmpm = { path = "./pbmpm" } +mpm = { path = "./mpm" } acoustics = { path = "./acoustics" } ai = { path = "./ai" } diff --git a/crates/systems/pbmpm/Cargo.toml b/crates/systems/mpm/Cargo.toml similarity index 91% rename from crates/systems/pbmpm/Cargo.toml rename to crates/systems/mpm/Cargo.toml index 989ab5e..ed8f3c1 100644 --- a/crates/systems/pbmpm/Cargo.toml +++ b/crates/systems/mpm/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "pbmpm" +name = "mpm" version = "0.1.0" edition = "2024" description = "Material Point Method implementation for simulating continuous materials" diff --git a/crates/systems/pbmpm/src/grid.rs b/crates/systems/mpm/src/grid.rs similarity index 100% rename from crates/systems/pbmpm/src/grid.rs rename to crates/systems/mpm/src/grid.rs diff --git a/crates/systems/pbmpm/src/lib.rs b/crates/systems/mpm/src/lib.rs similarity index 68% rename from crates/systems/pbmpm/src/lib.rs rename to crates/systems/mpm/src/lib.rs index 6f01360..2b7df7c 100644 --- a/crates/systems/pbmpm/src/lib.rs +++ b/crates/systems/mpm/src/lib.rs @@ -5,22 +5,22 @@ pub mod particle; pub mod solver; pub mod transfer; -/// Plugin for Point-Based Material Point Method physics solver +/// Plugin for the Material Point Method physics solver #[derive(Default)] -pub struct PBMPMPlugin; +pub struct MPMPlugin; -impl Plugin for PBMPMPlugin { +impl Plugin for MPMPlugin { fn build(&self, _app: &mut App) { - // TODO: Will integrate PBMPM systems when implementation is ready + // TODO: Will integrate MPM systems when implementation is ready // Plugin structure prepared for upcoming development } } -/// The PBMPM prelude. +/// The MPM prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { - pub use super::PBMPMPlugin; + pub use super::MPMPlugin; // TODO: Export main types when implementation is complete // pub use crate::grid::*; diff --git a/crates/systems/pbmpm/src/particle.rs b/crates/systems/mpm/src/particle.rs similarity index 100% rename from crates/systems/pbmpm/src/particle.rs rename to crates/systems/mpm/src/particle.rs diff --git a/crates/systems/pbmpm/src/solver.rs b/crates/systems/mpm/src/solver.rs similarity index 100% rename from crates/systems/pbmpm/src/solver.rs rename to crates/systems/mpm/src/solver.rs diff --git a/crates/systems/pbmpm/src/transfer.rs b/crates/systems/mpm/src/transfer.rs similarity index 100% rename from crates/systems/pbmpm/src/transfer.rs rename to crates/systems/mpm/src/transfer.rs diff --git a/crates/systems/src/lib.rs b/crates/systems/src/lib.rs index 9b34f09..224207e 100644 --- a/crates/systems/src/lib.rs +++ b/crates/systems/src/lib.rs @@ -1,6 +1,6 @@ pub use acoustics; pub use ai; -pub use pbmpm; +pub use mpm; pub use save_system; use bevy::prelude::*; @@ -14,7 +14,7 @@ impl Plugin for SystemsPlugin { app.add_plugins(( acoustics::AcousticsPlugin, ai::LPAIPlugin::default(), - pbmpm::PBMPMPlugin, + mpm::MPMPlugin, save_system::SaveSystemPlugin::default(), )); } @@ -27,6 +27,6 @@ pub mod prelude { // Re-export all sub-crate preludes pub use acoustics::prelude::*; pub use ai::prelude::*; - pub use pbmpm::prelude::*; + pub use mpm::prelude::*; pub use save_system::prelude::*; } From 63b3b6f574dcbb9a80e3be69a1a65cca543b9289 Mon Sep 17 00:00:00 2001 From: M1thieu <18742831+M1thieu@users.noreply.github.com> Date: Wed, 29 Oct 2025 02:10:24 +0100 Subject: [PATCH 03/15] Make gravitational constant configurable --- crates/forces/src/core/gravity.rs | 58 ++++++++++++++++++++++--------- crates/forces/src/core/mod.rs | 6 ++-- examples/basic_forces.rs | 2 +- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/crates/forces/src/core/gravity.rs b/crates/forces/src/core/gravity.rs index 378b9ac..33a979c 100644 --- a/crates/forces/src/core/gravity.rs +++ b/crates/forces/src/core/gravity.rs @@ -2,18 +2,35 @@ use super::newton_laws::{AppliedForce, Mass}; use bevy::prelude::*; // Simulation constants -pub const GRAVITATIONAL_CONSTANT: f32 = 0.1; +pub const DEFAULT_GRAVITATIONAL_CONSTANT: f32 = 0.1; /// Resource for gravity simulation parameters #[derive(Resource, Clone, Debug)] pub struct GravityParams { /// Softening parameter to prevent singularities pub softening: f32, + /// Gravitational constant controlling attraction strength + pub gravitational_constant: f32, } impl Default for GravityParams { fn default() -> Self { - Self { softening: 5.0 } + Self { + softening: 5.0, + gravitational_constant: DEFAULT_GRAVITATIONAL_CONSTANT, + } + } +} + +impl GravityParams { + pub fn with_softening(mut self, softening: f32) -> Self { + self.softening = softening; + self + } + + pub fn with_gravitational_constant(mut self, gravitational_constant: f32) -> Self { + self.gravitational_constant = gravitational_constant; + self } } @@ -268,6 +285,7 @@ pub fn calculate_gravitational_attraction( >, ) { let softening_squared = gravity_params.softening * gravity_params.softening; + let gravitational_constant = gravity_params.gravitational_constant; let sources: Vec<(Entity, Vec3, f32)> = query .iter() @@ -286,7 +304,7 @@ pub fn calculate_gravitational_attraction( let direction = source_pos - affected_pos; let distance_squared = direction.length_squared(); let softened_distance_squared = distance_squared + softening_squared; - let force_magnitude = GRAVITATIONAL_CONSTANT * source_mass * affected_mass.value + let force_magnitude = gravitational_constant * source_mass * affected_mass.value / softened_distance_squared; force.force += direction.normalize() * force_magnitude; } @@ -315,6 +333,8 @@ pub fn calculate_barnes_hut_attraction( .collect(); let quadtree = spatial::Quadtree::from_bodies(&bodies); + let softening = gravity_params.softening; + let gravitational_constant = gravity_params.gravitational_constant; affected_query .par_iter_mut() @@ -329,7 +349,8 @@ pub fn calculate_barnes_hut_attraction( position, &quadtree.root, theta, - gravity_params.softening, + softening, + gravitational_constant, ); force.force += force_vector; @@ -341,6 +362,7 @@ pub fn calculate_barnes_hut_force( node: &spatial::QuadtreeNode, theta: f32, softening: f32, + gravitational_constant: f32, ) -> Vec3 { let softening_squared = softening * softening; @@ -348,8 +370,8 @@ pub fn calculate_barnes_hut_force( let direction = node.mass_properties.center_of_mass - affected_position; let distance_squared = direction.length_squared(); let softened_distance_squared = distance_squared + softening_squared; - let force_magnitude = - GRAVITATIONAL_CONSTANT * node.mass_properties.total_mass / softened_distance_squared; + let force_magnitude = gravitational_constant * node.mass_properties.total_mass + / softened_distance_squared; return direction.normalize() * force_magnitude; } @@ -364,12 +386,10 @@ pub fn calculate_barnes_hut_force( continue; } - total_force += { - let distance_squared = direction.length_squared(); - let softened_distance_squared = distance_squared + softening_squared; - let force_magnitude = GRAVITATIONAL_CONSTANT * mass / softened_distance_squared; - direction.normalize() * force_magnitude - }; + let softened_distance_squared = distance_squared + softening_squared; + let force_magnitude = + gravitational_constant * mass / softened_distance_squared; + total_force += direction.normalize() * force_magnitude; } return total_force; @@ -377,14 +397,20 @@ pub fn calculate_barnes_hut_force( let mut total_force = Vec3::ZERO; for child_node in node.children.iter().flatten() { - total_force += calculate_barnes_hut_force(affected_position, child_node, theta, softening); + total_force += calculate_barnes_hut_force( + affected_position, + child_node, + theta, + softening, + gravitational_constant, + ); } total_force } pub fn calculate_orbital_velocity(central_mass: f32, orbit_radius: f32) -> f32 { - (GRAVITATIONAL_CONSTANT * central_mass / orbit_radius).sqrt() + (DEFAULT_GRAVITATIONAL_CONSTANT * central_mass / orbit_radius).sqrt() } pub fn calculate_elliptical_orbit_velocity( @@ -393,13 +419,13 @@ pub fn calculate_elliptical_orbit_velocity( eccentricity: f32, is_periapsis: bool, ) -> f32 { - let mu = GRAVITATIONAL_CONSTANT * central_mass; + let mu = DEFAULT_GRAVITATIONAL_CONSTANT * central_mass; let semimajor_axis = distance / (1.0 - eccentricity * if is_periapsis { 1.0 } else { -1.0 }); (mu * (2.0 / distance - 1.0 / semimajor_axis)).sqrt() } pub fn calculate_escape_velocity(central_mass: f32, distance: f32) -> f32 { - (2.0 * GRAVITATIONAL_CONSTANT * central_mass / distance).sqrt() + (2.0 * DEFAULT_GRAVITATIONAL_CONSTANT * central_mass / distance).sqrt() } #[derive(Default)] diff --git a/crates/forces/src/core/mod.rs b/crates/forces/src/core/mod.rs index 3c9daa9..7fdae7e 100644 --- a/crates/forces/src/core/mod.rs +++ b/crates/forces/src/core/mod.rs @@ -7,9 +7,9 @@ pub mod newton_laws; pub mod prelude { // Re-export from gravity module pub use crate::core::gravity::{ - GRAVITATIONAL_CONSTANT, GravityAffected, GravityParams, GravitySource, MassiveBody, - UniformGravity, calculate_elliptical_orbit_velocity, calculate_escape_velocity, - calculate_gravitational_attraction, calculate_orbital_velocity, + DEFAULT_GRAVITATIONAL_CONSTANT, GravityAffected, GravityParams, GravitySource, + MassiveBody, UniformGravity, calculate_elliptical_orbit_velocity, + calculate_escape_velocity, calculate_gravitational_attraction, calculate_orbital_velocity, }; // Re-export from newton_laws module diff --git a/examples/basic_forces.rs b/examples/basic_forces.rs index 70fc2f2..079bb34 100644 --- a/examples/basic_forces.rs +++ b/examples/basic_forces.rs @@ -11,7 +11,7 @@ fn main() { ..default() })) .insert_resource(ClearColor(Color::srgb(0.0, 0.0, 0.1))) - .insert_resource(GravityParams { softening: 10.0 }) // Better softening value for stability + .insert_resource(GravityParams::default().with_softening(10.0)) // Better softening value for stability .add_systems(Startup, setup) .add_systems( Update, From 5472e03239e111fb534bbf51a99dc8c17f3dd54f Mon Sep 17 00:00:00 2001 From: M1thieu <18742831+M1thieu@users.noreply.github.com> Date: Wed, 29 Oct 2025 02:34:45 +0100 Subject: [PATCH 04/15] Make SystemsPlugin configurable --- crates/systems/src/lib.rs | 76 ++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/crates/systems/src/lib.rs b/crates/systems/src/lib.rs index 224207e..42fb309 100644 --- a/crates/systems/src/lib.rs +++ b/crates/systems/src/lib.rs @@ -4,19 +4,79 @@ pub use mpm; pub use save_system; use bevy::prelude::*; +use save_system::save_system::SaveSettings; /// Systems domain plugin -#[derive(Default)] -pub struct SystemsPlugin; +#[derive(Clone, Debug)] +pub struct SystemsPlugin { + include_ai: bool, + include_acoustics: bool, + include_mpm: bool, + include_save: bool, + save_settings: SaveSettings, +} + +impl Default for SystemsPlugin { + fn default() -> Self { + Self { + include_ai: true, + include_acoustics: true, + include_mpm: true, + include_save: true, + save_settings: SaveSettings::default(), + } + } +} + +impl SystemsPlugin { + /// Enable or disable the AI systems. + pub fn with_ai(mut self, enabled: bool) -> Self { + self.include_ai = enabled; + self + } + + /// Enable or disable acoustics systems. + pub fn with_acoustics(mut self, enabled: bool) -> Self { + self.include_acoustics = enabled; + self + } + + /// Enable or disable the Material Point Method placeholder systems. + pub fn with_mpm(mut self, enabled: bool) -> Self { + self.include_mpm = enabled; + self + } + + /// Enable or disable the save-system integration. + pub fn with_save_system(mut self, enabled: bool) -> Self { + self.include_save = enabled; + self + } + + /// Provide custom save settings that will be forwarded to the save-system plugin. + pub fn with_save_settings(mut self, settings: SaveSettings) -> Self { + self.save_settings = settings; + self + } +} impl Plugin for SystemsPlugin { fn build(&self, app: &mut App) { - app.add_plugins(( - acoustics::AcousticsPlugin, - ai::LPAIPlugin::default(), - mpm::MPMPlugin, - save_system::SaveSystemPlugin::default(), - )); + if self.include_acoustics { + app.add_plugins(acoustics::AcousticsPlugin); + } + + if self.include_ai { + app.add_plugins(ai::LPAIPlugin::default()); + } + + if self.include_mpm { + app.add_plugins(mpm::MPMPlugin); + } + + if self.include_save { + app.add_plugins(save_system::SaveSystemPlugin::new(self.save_settings.clone())); + } } } From 5bc35d744008e2323a1f05d80258c9b8e4fae566 Mon Sep 17 00:00:00 2001 From: M1thieu <18742831+M1thieu@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:42:01 +0100 Subject: [PATCH 05/15] Enhance AI relationship context handling Preserve social history instead of overwriting, expose configurable personality inputs, embed fractal templates, and document SystemsPlugin configuration. --- .../information/src/fractals/data_loader.rs | 6 +- crates/systems/README.md | 15 +++++ crates/systems/ai/src/lib.rs | 3 +- crates/systems/ai/src/personality/mod.rs | 8 ++- crates/systems/ai/src/personality/traits.rs | 57 ++++++++++++++----- crates/systems/ai/src/relationships/social.rs | 48 +++++++++------- 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/crates/information/src/fractals/data_loader.rs b/crates/information/src/fractals/data_loader.rs index 7d5c89d..3fd4009 100644 --- a/crates/information/src/fractals/data_loader.rs +++ b/crates/information/src/fractals/data_loader.rs @@ -1,5 +1,4 @@ use serde::Deserialize; -use std::fs; #[derive(Deserialize, Debug)] pub struct Parameters { @@ -25,10 +24,7 @@ pub struct Template { /// Load a template from the fractals.json file pub fn load_template(template_name: &str) -> Result { - let file_content = fs::read_to_string("crates/information/src/fractals/fractals.json") - .map_err(|_| "Error: Could not read fractals.json".to_string())?; - - let json: serde_json::Value = serde_json::from_str(&file_content) + let json: serde_json::Value = serde_json::from_str(include_str!("fractals.json")) .map_err(|_| "Error: Invalid JSON format in fractals.json".to_string())?; json["templates"] diff --git a/crates/systems/README.md b/crates/systems/README.md index 8ae4daf..ca6bfa7 100644 --- a/crates/systems/README.md +++ b/crates/systems/README.md @@ -30,3 +30,18 @@ fn main() { .run(); } ``` + +### Configuration + +`SystemsPlugin` can be customised before registration: + +```rust +use systems::save_system::prelude::SaveSettings; + +let systems = SystemsPlugin::default() + .with_ai(true) + .with_acoustics(false) + .with_save_settings(SaveSettings::default().with_default_file("saves/slot_a.json")); + +app.add_plugins(systems); +``` diff --git a/crates/systems/ai/src/lib.rs b/crates/systems/ai/src/lib.rs index 6252717..5cbb17e 100644 --- a/crates/systems/ai/src/lib.rs +++ b/crates/systems/ai/src/lib.rs @@ -30,7 +30,8 @@ pub mod prelude { // Context-aware personality system pub use crate::personality::traits::{ - ContextAwareUtilities, update_collective_influence, update_context_aware_utilities, + ContextAwareUtilities, PersonalityContextInputs, update_collective_influence, + update_context_aware_utilities, }; } diff --git a/crates/systems/ai/src/personality/mod.rs b/crates/systems/ai/src/personality/mod.rs index ecb32a3..7f2095c 100644 --- a/crates/systems/ai/src/personality/mod.rs +++ b/crates/systems/ai/src/personality/mod.rs @@ -9,7 +9,9 @@ pub struct PersonalityPlugin; impl Plugin for PersonalityPlugin { fn build(&self, app: &mut App) { app.register_type::() - .register_type::(); + .register_type::() + .register_type::() + .register_type::(); } } @@ -18,5 +20,7 @@ impl Plugin for PersonalityPlugin { /// This includes personality traits and related components. pub mod prelude { pub use crate::personality::PersonalityPlugin; - pub use crate::personality::traits::{Altruistic, Personality}; + pub use crate::personality::traits::{ + Altruistic, ContextAwareUtilities, Personality, PersonalityContextInputs, + }; } diff --git a/crates/systems/ai/src/personality/traits.rs b/crates/systems/ai/src/personality/traits.rs index 5f53961..df78607 100644 --- a/crates/systems/ai/src/personality/traits.rs +++ b/crates/systems/ai/src/personality/traits.rs @@ -155,30 +155,57 @@ impl Default for ContextAwareUtilities { } } +/// Optional context inputs that gameplay can supply to influence personality-driven utilities. +#[derive(Component, Debug, Clone, Reflect)] +pub struct PersonalityContextInputs { + /// Current energy or stamina level (0.0-1.0) + pub energy_level: f32, + /// Recent success metric (-1.0..=1.0) influencing confidence + pub recent_success: f32, + /// Environmental stress measure (0.0-1.0) + pub environmental_stress: f32, +} + +impl Default for PersonalityContextInputs { + fn default() -> Self { + Self { + energy_level: 0.5, + recent_success: 0.0, + environmental_stress: 0.0, + } + } +} + /// System that updates personality utilities based on generic resource and environmental state pub fn update_context_aware_utilities( mut query: Query<( &Personality, &mut ContextAwareUtilities, - // TODO: Replace with trait-based resource system when available - // For now, use simple f32 values that can be populated by game-level integration + Option<&PersonalityContextInputs>, )>, ) { - for (personality, mut utilities) in &mut query { - // Default values - will be replaced by proper resource tracking - let energy_level = 0.5; // Default moderate energy - let recent_success = 0.0; // Default neutral success - let environmental_stress = 0.0; // Default no stress + for (personality, mut utilities, context_inputs) in &mut query { + let default_context = PersonalityContextInputs::default(); + let context = context_inputs.unwrap_or(&default_context); // Update utilities with default context (to be enhanced later) - utilities.resource_competition = - calculate_contextual_resource_competition(personality, energy_level, recent_success); - - utilities.competitive_behavior = - calculate_contextual_competitive_strength(personality, energy_level, recent_success); - - utilities.stress_retreat = - calculate_contextual_stress_retreat(personality, energy_level, environmental_stress); + utilities.resource_competition = calculate_contextual_resource_competition( + personality, + context.energy_level, + context.recent_success, + ); + + utilities.competitive_behavior = calculate_contextual_competitive_strength( + personality, + context.energy_level, + context.recent_success, + ); + + utilities.stress_retreat = calculate_contextual_stress_retreat( + personality, + context.energy_level, + context.environmental_stress, + ); utilities.cooperation = Score::new(personality.social_utility()); diff --git a/crates/systems/ai/src/relationships/social.rs b/crates/systems/ai/src/relationships/social.rs index 69ea3f6..890b79f 100644 --- a/crates/systems/ai/src/relationships/social.rs +++ b/crates/systems/ai/src/relationships/social.rs @@ -70,29 +70,35 @@ impl SocialNetwork { strength: f32, current_tick: u64, ) { - let mut relationship = EntityRelationship { - strength: RelationshipStrength::new(strength), - relationship_type, - last_interaction_tick: current_tick, - }; + let clamped_strength = Score::clamp_trait_value(strength); + let relationships = self.relationships.entry(target).or_default(); + + match relationships.entry(relationship_type) { + std::collections::hash_map::Entry::Occupied(mut existing_entry) => { + let relationship = existing_entry.get_mut(); + let previous_strength = relationship.strength.value(); + let time_since_last = + current_tick.saturating_sub(relationship.last_interaction_tick) as f32; + + // Lightly decay stale relationships before applying the new observation. + if time_since_last > 0.0 { + let decay = (time_since_last / 1000.0).min(0.25); + relationship.strength.adjust(-decay); + } - // If relationship already exists, update based on existing interaction history - if let Some(existing_relationship) = self - .relationships - .entry(target) - .or_default() - .get_mut(&relationship_type) - { - // Modify strength based on interaction frequency - relationship.strength.adjust( - (current_tick - existing_relationship.last_interaction_tick) as f32 / 1000.0, - ); + // Blend prior strength with the latest observation to preserve history. + let blended_strength = previous_strength * 0.7 + clamped_strength * 0.3; + relationship.strength = RelationshipStrength::new(blended_strength); + relationship.last_interaction_tick = current_tick; + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(EntityRelationship { + strength: RelationshipStrength::new(clamped_strength), + relationship_type, + last_interaction_tick: current_tick, + }); + } } - - self.relationships - .entry(target) - .or_default() - .insert(relationship_type, relationship); } /// Query relationships with flexible filtering From 7889f49fe3d8c7ca2996743eec58ac588f956afc Mon Sep 17 00:00:00 2001 From: M1thieu <18742831+M1thieu@users.noreply.github.com> Date: Thu, 6 Nov 2025 01:56:22 +0100 Subject: [PATCH 06/15] Add Utility smoothing & Threat Tracking to AI - UtilitySmoothing: Component that smooths scorer outputs over time with continuation bonuses to prevent rapid action switching (thrashing) - ThreatTracker: Tracks visible threats with distance-based severity, natural decay, and exponential panic calculation for fear-driven behaviors Both components integrate seamlessly with existing scorer and perception systems. --- crates/systems/ai/src/core/mod.rs | 4 +- crates/systems/ai/src/core/scorers.rs | 76 ++++++++++ crates/systems/ai/src/trackers/mod.rs | 8 +- .../systems/ai/src/trackers/threat_tracker.rs | 141 ++++++++++++++++++ 4 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 crates/systems/ai/src/trackers/threat_tracker.rs diff --git a/crates/systems/ai/src/core/mod.rs b/crates/systems/ai/src/core/mod.rs index 488d7e8..5acb995 100644 --- a/crates/systems/ai/src/core/mod.rs +++ b/crates/systems/ai/src/core/mod.rs @@ -24,7 +24,7 @@ pub mod prelude { // Scorers pub use crate::core::scorers::{ AllOrNothing, EvaluatingScorer, FixedScore, MeasuredScorer, ProductOfScorers, Score, - ScorerBuilder, SumOfScorers, WinningScorer, + ScorerBuilder, SumOfScorers, UtilitySmoothing, WinningScorer, }; // Thinkers (includes Action, Actor, etc.) @@ -102,6 +102,7 @@ impl Plugin for LPAIPlugin { self.schedule.intern(), (AISet::Scorers, AISet::Thinkers, AISet::Actions).chain(), ) + .register_type::() .configure_sets(self.cleanup_schedule.intern(), AISet::Cleanup) // Add scorer systems .add_systems( @@ -114,6 +115,7 @@ impl Plugin for LPAIPlugin { scorers::product_of_scorers_system, scorers::winning_scorer_system, scorers::evaluating_scorer_system, + scorers::utility_smoothing_system, ) .in_set(AISet::Scorers), ) diff --git a/crates/systems/ai/src/core/scorers.rs b/crates/systems/ai/src/core/scorers.rs index f055cd8..bbfff6d 100644 --- a/crates/systems/ai/src/core/scorers.rs +++ b/crates/systems/ai/src/core/scorers.rs @@ -913,3 +913,79 @@ impl ScorerBuilder for MeasuredScorerBuilder { }); } } + +/// Component that smooths a scorer's output and applies a continuation bonus to reduce thrashing. +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component)] +pub struct UtilitySmoothing { + /// How quickly the smoothed value should respond to new inputs (0.0-1.0 per second). + pub response_rate: f32, + /// Additional weight applied based on recent history to favour continuing actions (0.0-1.0). + pub continuation_bonus: f32, + /// How fast the continuation effect decays per second (0.0-1.0). + pub continuation_decay: f32, + #[reflect(ignore)] + smoothed_value: f32, + #[reflect(ignore)] + momentum: f32, + #[reflect(ignore)] + initialised: bool, +} + +impl UtilitySmoothing { + pub fn new(response_rate: f32, continuation_bonus: f32, continuation_decay: f32) -> Self { + Self { + response_rate: response_rate.clamp(0.0, 1.0), + continuation_bonus: continuation_bonus.clamp(0.0, 1.0), + continuation_decay: continuation_decay.clamp(0.0, 1.0), + smoothed_value: 0.0, + momentum: 0.0, + initialised: false, + } + } + + fn response_alpha(&self, delta_secs: f32) -> f32 { + if self.response_rate <= 0.0 { + 0.0 + } else { + 1.0 - (1.0 - self.response_rate).powf(delta_secs.max(0.0).min(1.0)) + } + } + + fn continuation_alpha(&self, delta_secs: f32) -> f32 { + if self.continuation_decay <= 0.0 { + 0.0 + } else { + 1.0 - (1.0 - self.continuation_decay).powf(delta_secs.max(0.0).min(1.0)) + } + } +} + +/// System that applies smoothing to any scorer tagged with [`UtilitySmoothing`]. +pub fn utility_smoothing_system( + time: Res