diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..fcf410f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,54 @@
+name: Bug report
+description: Report a bug in LP
+body:
+
+- type: markdown
+ attributes:
+ value: |
+ When reporting bugs, please follow the guidelines in this template. This helps identify the problem precisely and thus enables contributors to fix it faster.
+ - Write a descriptive issue title above.
+ - The golden rule is to **always open *one* issue for *one* bug**. If you notice several bugs and want to report them, make sure to create one new issue for each of them.
+ - Search [open issues](https://github.com/erematorg/LP/issues) and [closed issues](https://github.com/erematorg/LP/issues?q=is%3Aissue+is%3Aclosed) to ensure it has not already been reported. If you don't find a relevant match or if you're unsure, don't hesitate to **open a new issue**. The bugsquad will handle it from there if it's a duplicate.
+ - Please always check if your issue is reproducible in the latest version – it may already have been fixed!
+
+- type: textarea
+ attributes:
+ label: Tested versions
+ description: |
+ To properly fix a bug, we need to identify if the bug was recently introduced in the LP project or if it has been present from the beginning.
+ - Specify the LP version you found the issue in. If you are using a specific build, include relevant details.
+ - If you can, **please test earlier LP versions** and, if applicable, newer versions. Mention whether the bug is reproducible or not in the versions you tested.
+ placeholder: |
+
+ - Reproducible in: v1.0, v1.1
+ - Not reproducible in: v0.9
+ validations:
+ required: true
+
+- type: input
+ attributes:
+ label: System information
+ description: |
+ - Specify the OS version, and when relevant, hardware information.
+ - For issues that are likely OS-specific and/or graphics-related, please specify the CPU model and architecture.
+ placeholder: Windows 10 - LP v1.0 - dedicated GPU NVIDIA GeForce GTX 970 - Intel Core i7-10700KF CPU @ 3.80GHz (16 Threads)
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: Issue description
+ description: |
+ Describe your issue briefly. What doesn't work, and how do you expect it to work instead?
+ You can include images or videos with drag and drop, and format code blocks or logs with ``` tags.
+ validations:
+ required: true
+
+- type: textarea
+ attributes:
+ label: Steps to reproduce
+ description: |
+ List the steps or sample code that reproduces the issue. Having reproducible issues is a prerequisite for contributors to be able to solve them.
+ If you include a minimal reproduction project below, detail how to use it here.
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..5b1e3da
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,36 @@
+name: Feature request
+description: Suggest a new feature for LP
+body:
+
+- type: markdown
+ attributes:
+ value: |
+ When suggesting new features, please follow the guidelines in this template. This helps provide clear and actionable suggestions.
+ - Write a descriptive title above.
+ - Clearly describe the problem or need that this feature aims to address.
+ - If you have multiple feature suggestions, create a new request for each to maintain clarity.
+ - Search [open issues](https://github.com/erematorg/LP/issues) to ensure a similar feature request has not already been submitted. If you don't find a relevant match or if you're unsure, feel free to **open a new request**.
+
+- type: textarea
+ attributes:
+ label: Problem Description
+ description: |
+ A clear and concise description of the problem or need that this feature aims to address.
+
+- type: textarea
+ attributes:
+ label: Proposed Solution
+ description: |
+ A clear and concise description of the solution or functionality you would like to see implemented.
+
+- type: textarea
+ attributes:
+ label: Alternatives Considered
+ description: |
+ Describe any alternative solutions or features you have considered or think might be viable.
+
+- type: textarea
+ attributes:
+ label: Additional Context
+ description: |
+ Add any other context, examples, or use cases that provide additional information about the feature request.
diff --git a/.github/code_of_conduct.md b/.github/code_of_conduct.md
new file mode 100644
index 0000000..e660ddf
--- /dev/null
+++ b/.github/code_of_conduct.md
@@ -0,0 +1,129 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+contact@eremat.org.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
+
diff --git a/.github/funding.yml b/.github/funding.yml
new file mode 100644
index 0000000..0f775a8
--- /dev/null
+++ b/.github/funding.yml
@@ -0,0 +1,14 @@
+# These are supported funding model platforms
+
+github: [erematorg]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: eremat
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+
diff --git a/crates/energy/src/electromagnetism/interactions.rs b/crates/energy/src/electromagnetism/interactions.rs
index 82bd4be..ff3e8b1 100644
--- a/crates/energy/src/electromagnetism/interactions.rs
+++ b/crates/energy/src/electromagnetism/interactions.rs
@@ -1,6 +1,7 @@
-use super::fields::{ElectricField, MagneticField};
use bevy::prelude::*;
+use super::fields::{ElectricField, MagneticField};
+
// Speed of light (in m/s) constant physical value
//TODO: Making this cleaner later on to make units of measure dynamic rather than admiting 1 meter = 1 meter, same for seconds and much more
const C: f32 = 299_792_458.0;
diff --git a/crates/energy/src/waves/mod.rs b/crates/energy/src/waves/mod.rs
index 8b1e76c..c178b8d 100644
--- a/crates/energy/src/waves/mod.rs
+++ b/crates/energy/src/waves/mod.rs
@@ -1,5 +1,17 @@
pub mod oscillation;
pub mod propagation;
+
+use bevy::prelude::Vec2;
+
+/// Helper function to normalize a vector or return a fallback if the vector is too small
+#[inline]
+pub(crate) fn normalize_or(vec: Vec2, fallback: Vec2) -> Vec2 {
+ if vec.length_squared() > f32::EPSILON {
+ vec.normalize()
+ } else {
+ fallback
+ }
+}
pub mod superposition;
pub mod wave_equation;
diff --git a/crates/energy/src/waves/oscillation.rs b/crates/energy/src/waves/oscillation.rs
index 89f63bf..153cc8b 100644
--- a/crates/energy/src/waves/oscillation.rs
+++ b/crates/energy/src/waves/oscillation.rs
@@ -1,13 +1,6 @@
use bevy::prelude::*;
-#[inline]
-fn normalize_or(vec: Vec2, fallback: Vec2) -> Vec2 {
- if vec.length_squared() > f32::EPSILON {
- vec.normalize()
- } else {
- fallback
- }
-}
+use super::normalize_or;
/// Wave parameters for configuring wave behavior
#[derive(Component, Debug, Clone, Copy, Reflect)]
@@ -47,11 +40,6 @@ impl Default for WaveParameters {
}
impl WaveParameters {
- /// Builder-style method for creating custom wave parameters
- pub fn new() -> Self {
- Self::default()
- }
-
/// Fluent interface for setting speed
pub fn with_speed(mut self, speed: f32) -> Self {
self.speed = speed;
diff --git a/crates/energy/src/waves/propagation.rs b/crates/energy/src/waves/propagation.rs
index 7dee9ec..578e9b8 100644
--- a/crates/energy/src/waves/propagation.rs
+++ b/crates/energy/src/waves/propagation.rs
@@ -1,14 +1,7 @@
-use super::oscillation::{WaveParameters, angular_frequency, wave_number};
use bevy::prelude::*;
-#[inline]
-fn normalize_or(vec: Vec2, fallback: Vec2) -> Vec2 {
- if vec.length_squared() > f32::EPSILON {
- vec.normalize()
- } else {
- fallback
- }
-}
+use super::normalize_or;
+use super::oscillation::{WaveParameters, angular_frequency, wave_number};
// Calculate modified angular frequency with dispersion
#[inline]
diff --git a/crates/forces/src/core/gravity.rs b/crates/forces/src/core/gravity.rs
index 378b9ac..0b3abe6 100644
--- a/crates/forces/src/core/gravity.rs
+++ b/crates/forces/src/core/gravity.rs
@@ -2,18 +2,47 @@ 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,
+ /// Maximum depth for Barnes-Hut octree spatial partitioning
+ pub barnes_hut_max_depth: usize,
+ /// Maximum bodies per node before subdivision in Barnes-Hut algorithm
+ pub barnes_hut_max_bodies_per_node: usize,
}
impl Default for GravityParams {
fn default() -> Self {
- Self { softening: 5.0 }
+ Self {
+ softening: 5.0,
+ gravitational_constant: DEFAULT_GRAVITATIONAL_CONSTANT,
+ barnes_hut_max_depth: 8,
+ barnes_hut_max_bodies_per_node: 8,
+ }
+ }
+}
+
+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
+ }
+
+ pub fn with_barnes_hut_params(mut self, max_depth: usize, max_bodies_per_node: usize) -> Self {
+ self.barnes_hut_max_depth = max_depth.max(1);
+ self.barnes_hut_max_bodies_per_node = max_bodies_per_node.max(1);
+ self
}
}
@@ -55,10 +84,6 @@ pub struct MassiveBody;
mod spatial {
use bevy::prelude::*;
- // Algorithm parameters
- const MAX_DEPTH: usize = 8;
- const MAX_BODIES_PER_NODE: usize = 8;
-
#[derive(Clone, Debug)]
pub struct AABB {
pub center: Vec2,
@@ -131,16 +156,20 @@ mod spatial {
pub mass_properties: MassProperties,
pub bodies: Vec<(Entity, Vec3, f32)>,
pub children: [Option>; 4],
+ pub max_depth: usize,
+ pub max_bodies_per_node: usize,
}
impl QuadtreeNode {
- pub fn new(aabb: AABB, depth: usize) -> Self {
+ pub fn new(aabb: AABB, depth: usize, max_depth: usize, max_bodies_per_node: usize) -> Self {
Self {
aabb,
depth,
mass_properties: MassProperties::new(),
bodies: Vec::new(),
children: [None, None, None, None],
+ max_depth,
+ max_bodies_per_node,
}
}
@@ -159,8 +188,8 @@ mod spatial {
pub fn insert(&mut self, entity: Entity, position: Vec3, mass: f32) {
self.mass_properties.add_body(position, mass);
- if self.depth >= MAX_DEPTH
- || (self.bodies.len() < MAX_BODIES_PER_NODE && self.children[0].is_none())
+ if self.depth >= self.max_depth
+ || (self.bodies.len() < self.max_bodies_per_node && self.children[0].is_none())
{
self.bodies.push((entity, position, mass));
return;
@@ -171,6 +200,8 @@ mod spatial {
self.children[i] = Some(Box::new(QuadtreeNode::new(
self.aabb.get_quadrant_aabb(i),
self.depth + 1,
+ self.max_depth,
+ self.max_bodies_per_node,
)));
}
@@ -196,15 +227,23 @@ mod spatial {
}
impl Quadtree {
- pub fn new(bounds: AABB) -> Self {
+ pub fn new(bounds: AABB, max_depth: usize, max_bodies_per_node: usize) -> Self {
Self {
- root: QuadtreeNode::new(bounds, 0),
+ root: QuadtreeNode::new(bounds, 0, max_depth, max_bodies_per_node),
}
}
- pub fn from_bodies(bodies: &[(Entity, Vec3, f32)]) -> Self {
+ pub fn from_bodies(
+ bodies: &[(Entity, Vec3, f32)],
+ max_depth: usize,
+ max_bodies_per_node: usize,
+ ) -> Self {
if bodies.is_empty() {
- return Self::new(AABB::new(Vec2::ZERO, Vec2::new(1000.0, 1000.0)));
+ return Self::new(
+ AABB::new(Vec2::ZERO, Vec2::new(1000.0, 1000.0)),
+ max_depth,
+ max_bodies_per_node,
+ );
}
let mut min_x = f32::MAX;
@@ -229,7 +268,11 @@ mod spatial {
let half_size = Vec2::new((max_x - min_x) * 0.5, (max_y - min_y) * 0.5);
let max_half_size = half_size.x.max(half_size.y);
- let mut tree = Self::new(AABB::new(center, Vec2::splat(max_half_size)));
+ let mut tree = Self::new(
+ AABB::new(center, Vec2::splat(max_half_size)),
+ max_depth,
+ max_bodies_per_node,
+ );
for &(entity, position, mass) in bodies {
tree.insert(entity, position, mass);
@@ -268,6 +311,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 +330,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;
}
@@ -314,7 +358,13 @@ pub fn calculate_barnes_hut_attraction(
.map(|(e, t, m)| (e, t.translation, m.value))
.collect();
- let quadtree = spatial::Quadtree::from_bodies(&bodies);
+ let quadtree = spatial::Quadtree::from_bodies(
+ &bodies,
+ gravity_params.barnes_hut_max_depth,
+ gravity_params.barnes_hut_max_bodies_per_node,
+ );
+ let softening = gravity_params.softening;
+ let gravitational_constant = gravity_params.gravitational_constant;
affected_query
.par_iter_mut()
@@ -329,7 +379,8 @@ pub fn calculate_barnes_hut_attraction(
position,
&quadtree.root,
theta,
- gravity_params.softening,
+ softening,
+ gravitational_constant,
);
force.force += force_vector;
@@ -341,6 +392,7 @@ pub fn calculate_barnes_hut_force(
node: &spatial::QuadtreeNode,
theta: f32,
softening: f32,
+ gravitational_constant: f32,
) -> Vec3 {
let softening_squared = softening * softening;
@@ -349,7 +401,7 @@ pub fn calculate_barnes_hut_force(
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;
+ gravitational_constant * node.mass_properties.total_mass / softened_distance_squared;
return direction.normalize() * force_magnitude;
}
@@ -364,12 +416,9 @@ 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 +426,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 +448,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..2cc15a3 100644
--- a/crates/forces/src/core/mod.rs
+++ b/crates/forces/src/core/mod.rs
@@ -7,7 +7,7 @@ pub mod newton_laws;
pub mod prelude {
// Re-export from gravity module
pub use crate::core::gravity::{
- GRAVITATIONAL_CONSTANT, GravityAffected, GravityParams, GravitySource, MassiveBody,
+ DEFAULT_GRAVITATIONAL_CONSTANT, GravityAffected, GravityParams, GravitySource, MassiveBody,
UniformGravity, calculate_elliptical_orbit_velocity, calculate_escape_velocity,
calculate_gravitational_attraction, calculate_orbital_velocity,
};
diff --git a/crates/information/src/fractals/core.rs b/crates/information/src/fractals/core.rs
index 422f084..b20dfb2 100644
--- a/crates/information/src/fractals/core.rs
+++ b/crates/information/src/fractals/core.rs
@@ -1,6 +1,7 @@
-use super::generator;
use std::collections::HashMap;
+use super::generator;
+
/// Handles rule management for L-Systems.
pub struct RuleManager<'a> {
rules: HashMap,
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/information/src/fractals/renderer.rs b/crates/information/src/fractals/renderer.rs
index 5d13346..258cc2c 100644
--- a/crates/information/src/fractals/renderer.rs
+++ b/crates/information/src/fractals/renderer.rs
@@ -4,7 +4,6 @@ use bevy::prelude::*;
// Components
/// Component for an L-System branch
#[derive(Component)]
-#[allow(dead_code)]
struct Branch {
/// Type of this branch segment
symbol_type: SymbolType,
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/README.md b/crates/systems/README.md
index 0a111f9..ca6bfa7 100644
--- a/crates/systems/README.md
+++ b/crates/systems/README.md
@@ -1,3 +1,47 @@
# 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();
+}
+```
+
+### 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/README.md b/crates/systems/ai/README.md
new file mode 100644
index 0000000..4eae3aa
--- /dev/null
+++ b/crates/systems/ai/README.md
@@ -0,0 +1,65 @@
+# LP AI Architecture
+
+This crate provides the agent decision loop used across Life's Progress. Every organism—plant, fungus, animal, machine—shares the same ECS-driven pipeline:
+
+```
+Perception → Trackers & Drives → State Layers (memory/emotion/social) → Utility Arbiter → Behavior Executors
+```
+
+## Key Concepts
+
+### AIModule
+Any component implementing `AIModule` exposes `update()` and `utility()` (0-1). Existing modules:
+- **Perception** - sensory cache (vision/chemical/etc.)
+- **Threat / Prey trackers** - context-specific evaluation
+- **Needs tracker** - energy/homeostasis drives
+- **Memory events** - short-term context
+- **Personality / Social graphs** - trait-based biasing
+
+Modules can publish `IntentContribution` events to compete for control (see Arbiter below).
+
+### Utility Arbiter
+`arbiter::UtilityArbiterPlugin` resets each agent’s `IntentSelection` every frame, reads `IntentContribution` events, applies continuation bias/hysteresis, and records the winning module & score. Only one intent wins per frame, providing Rain-World-style “strongest drive wins” behavior.
+
+### Behavior Executors
+Systems downstream of the arbiter read `IntentSelection` and perform actions (movement, growth, interaction). Executors will be built per intent (e.g., flee, forage, rest) and consume data from trackers/drives to apply forces or state changes.
+
+## Adding a New Decision Module
+1. Create a component implementing `AIModule` (e.g., `#[derive(Component)] struct HungerTracker`).
+2. In your system, update the module’s internal state and call `IntentContribution` to report a utility:
+```rust
+fn hunger_intent_system(
+ query: Query<(Entity, &HungerTracker)>,
+ mut writer: MessageWriter,
+) {
+ for (entity, tracker) in &query {
+ writer.write(IntentContribution {
+ entity,
+ module: "hunger",
+ utility: tracker.utility(),
+ });
+ }
+}
+```
+3. The arbiter will compare utilities (with continuation bias if the same module won last frame) and set `IntentSelection`.
+
+## Behavior Executors (WIP)
+Executors subscribe to `IntentSelection`:
+```rust
+fn flee_executor(
+ query: Query<(Entity, &IntentSelection, &mut Velocity), With>,
+) {
+ // if `selection.winner == Some("flee")`, apply movement logic
+}
+```
+
+### End-to-end example
+See `examples/basic_ai.rs` for a minimal wiring of the full loop:
+1. Each creature owns `Perception`, `EntityTracker`, `NeedsTracker`, `ThreatTracker`, `PreyTracker`, and `IntentSelection`.
+2. Sensors fill the `EntityTracker`; tracker systems evaluate utilities and emit `IntentContribution` events via the arbiter.
+3. A behavior executor reads `IntentResolved` events, maps winners (`"threat"`, `"prey"`, `"needs"`) to locomotion commands, and world systems react to the actions.
+
+## Philosophy
+- **No DSL / behavior trees** - everything is ECS data.
+- **Species-agnostic** - modules work for plants/animals alike; species differences are just component presets.
+- **Emergence-first** - utilities are derived from physics-aware trackers/drives, not hand-authored scripts.
diff --git a/crates/systems/ai/src/arbiter/mod.rs b/crates/systems/ai/src/arbiter/mod.rs
new file mode 100644
index 0000000..b9770dc
--- /dev/null
+++ b/crates/systems/ai/src/arbiter/mod.rs
@@ -0,0 +1,210 @@
+use std::collections::HashMap;
+
+use bevy::prelude::*;
+
+use crate::AIModule;
+use crate::trackers::needs_tracker::NeedsTracker;
+use crate::trackers::prey_tracker::PreyTracker;
+use crate::trackers::threat_tracker::ThreatTracker;
+
+/// Schedule sets that ensure the arbiter runs in a predictable order each frame.
+#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
+pub enum ArbiterSet {
+ Reset,
+ Gather,
+ Evaluate,
+ Broadcast,
+}
+
+/// Component describing the intent chosen for an agent this frame.
+#[derive(Component, Debug, Default, Reflect)]
+#[reflect(Component)]
+pub struct IntentSelection {
+ /// Winner chosen this frame.
+ pub winner: Option<&'static str>,
+ /// Utility returned by the winning module (biased).
+ pub utility: f32,
+ /// Winner from the previous frame (used for continuation bias).
+ pub last_winner: Option<&'static str>,
+}
+
+impl IntentSelection {
+ fn reset_for_frame(&mut self) {
+ self.last_winner = self.winner;
+ self.winner = None;
+ self.utility = 0.0;
+ }
+}
+
+/// Resource storing arbiter tuning parameters.
+#[derive(Resource, Debug, Clone, Copy, Reflect)]
+#[reflect(Resource)]
+pub struct ArbiterConfig {
+ /// How much advantage we give to the previous winner when scores are similar.
+ pub continuation_bias: f32,
+}
+
+impl Default for ArbiterConfig {
+ fn default() -> Self {
+ Self {
+ continuation_bias: 0.05,
+ }
+ }
+}
+
+/// Event emitted by modules competing for control.
+#[derive(Message, Clone, Debug)]
+pub struct IntentContribution {
+ pub entity: Entity,
+ pub module: &'static str,
+ pub utility: f32,
+}
+
+/// Event emitted after the arbiter resolves the winning intent.
+#[derive(Message, Clone, Debug)]
+pub struct IntentResolved {
+ pub entity: Entity,
+ pub winner: Option<&'static str>,
+ pub utility: f32,
+}
+
+fn reset_intentions(mut query: Query<&mut IntentSelection>) {
+ for mut selection in &mut query {
+ selection.reset_for_frame();
+ }
+}
+
+fn gather_need_intents(
+ query: Query<(Entity, &NeedsTracker)>,
+ mut writer: MessageWriter,
+) {
+ for (entity, tracker) in &query {
+ let utility = tracker.utility();
+ if utility > 0.0 {
+ writer.write(IntentContribution {
+ entity,
+ module: "needs",
+ utility,
+ });
+ }
+ }
+}
+
+fn gather_threat_intents(
+ query: Query<(Entity, &ThreatTracker)>,
+ mut writer: MessageWriter,
+) {
+ for (entity, tracker) in &query {
+ let utility = tracker.utility();
+ if utility > 0.0 {
+ writer.write(IntentContribution {
+ entity,
+ module: "threat",
+ utility,
+ });
+ }
+ }
+}
+
+fn gather_prey_intents(
+ query: Query<(Entity, &PreyTracker)>,
+ mut writer: MessageWriter,
+) {
+ for (entity, tracker) in &query {
+ let utility = tracker.utility();
+ if utility > 0.0 {
+ writer.write(IntentContribution {
+ entity,
+ module: "prey",
+ utility,
+ });
+ }
+ }
+}
+
+fn evaluate_intentions(
+ config: Res,
+ mut contributions: MessageReader,
+ mut selections: Query<&mut IntentSelection>,
+) {
+ // Track the best score per entity within this frame.
+ let mut best_scores: HashMap = HashMap::default();
+
+ for contribution in contributions.read() {
+ if let Ok(mut selection) = selections.get_mut(contribution.entity) {
+ let bias = if selection.last_winner == Some(contribution.module) {
+ config.continuation_bias
+ } else {
+ 0.0
+ };
+
+ let adjusted = (contribution.utility + bias).clamp(0.0, 1.0);
+ let current_best = best_scores.entry(contribution.entity).or_insert(0.0);
+
+ if adjusted > *current_best {
+ *current_best = adjusted;
+ selection.utility = adjusted;
+ selection.winner = Some(contribution.module);
+ }
+ }
+ }
+}
+
+fn broadcast_intent_selections(
+ query: Query<(Entity, &IntentSelection)>,
+ mut writer: MessageWriter,
+) {
+ for (entity, selection) in &query {
+ writer.write(IntentResolved {
+ entity,
+ winner: selection.winner,
+ utility: selection.utility,
+ });
+ }
+}
+
+/// Plugin wiring the utility arbiter into the Bevy schedule.
+#[derive(Default)]
+pub struct UtilityArbiterPlugin;
+
+impl Plugin for UtilityArbiterPlugin {
+ fn build(&self, app: &mut App) {
+ app.init_resource::()
+ .register_type::()
+ .register_type::()
+ .add_message::()
+ .add_message::()
+ .configure_sets(
+ Update,
+ (
+ ArbiterSet::Reset,
+ ArbiterSet::Gather,
+ ArbiterSet::Evaluate,
+ ArbiterSet::Broadcast,
+ )
+ .chain(),
+ )
+ .add_systems(Update, reset_intentions.in_set(ArbiterSet::Reset))
+ .add_systems(
+ Update,
+ (
+ gather_need_intents,
+ gather_threat_intents,
+ gather_prey_intents,
+ )
+ .in_set(ArbiterSet::Gather),
+ )
+ .add_systems(Update, evaluate_intentions.in_set(ArbiterSet::Evaluate))
+ .add_systems(
+ Update,
+ broadcast_intent_selections.in_set(ArbiterSet::Broadcast),
+ );
+ }
+}
+
+pub mod prelude {
+ pub use super::{
+ ArbiterConfig, ArbiterSet, IntentContribution, IntentResolved, IntentSelection,
+ UtilityArbiterPlugin,
+ };
+}
diff --git a/crates/systems/ai/src/core/actions.rs b/crates/systems/ai/src/core/actions.rs
deleted file mode 100644
index 18395b3..0000000
--- a/crates/systems/ai/src/core/actions.rs
+++ /dev/null
@@ -1,484 +0,0 @@
-//! Defines Action-related functionality. This module includes the
-//! ActionBuilder trait and some Composite Actions for utility.
-use crate::core::thinkers::{Action, ActionSpan, Actor};
-use bevy::prelude::*;
-use std::sync::Arc;
-
-// Error message constants to reduce repetition
-const ACTION_STATE_ERROR: &str =
- "Failed to find action state component for concurrent action entity";
-
-/// The current state for an Action. These states are changed by a combination
-/// of the Thinker that spawned it, and the actual Action system executing the
-/// Action itself.
-#[derive(Debug, Clone, Component, Eq, PartialEq, Reflect)]
-#[component(storage = "Table")]
-pub enum ActionState {
- /// Initial state. No action should be performed.
- Init,
- /// Action requested. The Action-handling system should start executing this Action ASAP.
- Requested,
- /// The action has ongoing execution.
- Executing,
- /// An ongoing Action has been cancelled. **You must check whether the Cancelled state was set**
- /// and change to either Success or Failure. Thinkers will wait on Cancelled actions to do
- /// cleanup work, so this can hang your AI if you don't look for it.
- Cancelled,
- /// The Action was a success.
- Success,
- /// The Action failed.
- Failure,
-}
-
-impl Default for ActionState {
- fn default() -> Self {
- Self::Init
- }
-}
-
-impl ActionState {
- pub fn new() -> Self {
- Self::default()
- }
-}
-
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub(crate) struct ActionBuilderId;
-
-#[derive(Debug, Clone)]
-pub(crate) struct ActionBuilderWrapper(pub Arc, pub Arc);
-
-impl ActionBuilderWrapper {
- pub fn new(builder: Arc) -> Self {
- ActionBuilderWrapper(Arc::new(ActionBuilderId), builder)
- }
-}
-
-/// Trait that must be defined by types in order to be `ActionBuilder`s.
-/// The `build()` method MUST be implemented for any `ActionBuilder`s you want to define.
-#[reflect_trait]
-pub trait ActionBuilder: std::fmt::Debug + Send + Sync {
- /// MUST insert your concrete Action component into the Scorer [`Entity`],
- /// using `cmd`. You _may_ use `actor`, but it's perfectly normal to just ignore it.
- fn build(&self, cmd: &mut Commands, action: Entity, actor: Entity);
-
- fn label(&self) -> Option<&str> {
- None
- }
-}
-
-/// Spawns a new Action Component, using the given ActionBuilder.
-pub fn spawn_action(
- builder: &T,
- cmd: &mut Commands,
- actor: Entity,
-) -> Entity {
- let action_ent = Action(cmd.spawn_empty().id());
- let span = ActionSpan::new(action_ent.entity(), ActionBuilder::label(builder));
- let _guard = span.span().enter();
- debug!("New Action spawned.");
- cmd.entity(action_ent.entity())
- .insert(Name::new("Action"))
- .insert(ActionState::new())
- .insert(Actor(actor));
- builder.build(cmd, action_ent.entity(), actor);
- std::mem::drop(_guard);
- cmd.entity(action_ent.entity()).insert(span);
- action_ent.entity()
-}
-
-/// [`ActionBuilder`] for the [`Steps`] component.
-#[derive(Debug, Reflect)]
-#[reflect(ActionBuilder)]
-pub struct StepsBuilder {
- label: Option,
- steps_labels: Vec,
- #[reflect(ignore)]
- steps: Vec>,
-}
-
-impl StepsBuilder {
- pub fn label>(mut self, label: S) -> Self {
- self.label = Some(label.into());
- self
- }
-
- pub fn step(mut self, action_builder: impl ActionBuilder + 'static) -> Self {
- if let Some(label) = action_builder.label() {
- self.steps_labels.push(label.into());
- } else {
- self.steps_labels.push("Unlabeled Action".into());
- }
- self.steps.push(Arc::new(action_builder));
- self
- }
-}
-
-impl ActionBuilder for StepsBuilder {
- fn label(&self) -> Option<&str> {
- self.label.as_deref()
- }
-
- fn build(&self, cmd: &mut Commands, action: Entity, actor: Entity) {
- if let Some(step) = self.steps.first() {
- let child_action = spawn_action(step.as_ref(), cmd, actor);
- cmd.entity(action)
- .insert(Name::new("Steps Action"))
- .insert(Steps {
- active_step: 0,
- active_ent: Action(child_action),
- steps: self.steps.clone(),
- steps_labels: self.steps_labels.clone(),
- })
- .add_children(&[child_action]);
- }
- }
-}
-
-/// Composite Action that executes a series of steps in sequential order.
-#[derive(Component, Debug, Reflect)]
-pub struct Steps {
- #[reflect(ignore)]
- steps: Vec>,
- steps_labels: Vec,
- active_step: usize,
- active_ent: Action,
-}
-
-impl Steps {
- pub fn build() -> StepsBuilder {
- StepsBuilder {
- steps: Vec::new(),
- steps_labels: Vec::new(),
- label: None,
- }
- }
-}
-
-/// System that executes [`Steps`] Actions.
-pub fn steps_system(
- mut cmd: Commands,
- mut steps_q: Query<(Entity, &Actor, &mut Steps, &ActionSpan)>,
- mut states: Query<&mut ActionState>,
-) {
- use ActionState::*;
- for (seq_ent, Actor(actor), mut steps_action, _span) in steps_q.iter_mut() {
- let active_ent = steps_action.active_ent.entity();
- let current_state = states.get_mut(seq_ent).unwrap().clone();
- #[cfg(feature = "trace")]
- let _guard = _span.span().enter();
-
- match current_state {
- Requested => {
- #[cfg(feature = "trace")]
- trace!(
- "Initializing StepsAction and requesting first step: {:?}",
- active_ent
- );
- *states.get_mut(active_ent).unwrap() = Requested;
- *states.get_mut(seq_ent).unwrap() = Executing;
- }
- Executing => {
- let mut step_state = states.get_mut(active_ent).unwrap();
- match *step_state {
- Init => *step_state = Requested,
- Executing | Requested => {}
- Cancelled => {}
- Failure => {
- #[cfg(feature = "trace")]
- trace!("Step {:?} failed. Failing entire StepsAction.", active_ent);
- let step_state = step_state.clone();
- let mut seq_state = states
- .get_mut(seq_ent)
- .expect("Failed to find action state for sequence entity");
- *seq_state = step_state;
- if let Ok(mut ent) = cmd.get_entity(steps_action.active_ent.entity()) {
- ent.despawn();
- }
- }
- Success if steps_action.active_step == steps_action.steps.len() - 1 => {
- #[cfg(feature = "trace")]
- trace!("StepsAction completed all steps successfully.");
- let step_state = step_state.clone();
- let mut seq_state = states
- .get_mut(seq_ent)
- .expect("Failed to find action state for sequence entity");
- *seq_state = step_state;
- if let Ok(mut ent) = cmd.get_entity(steps_action.active_ent.entity()) {
- ent.despawn();
- }
- }
- Success => {
- #[cfg(feature = "trace")]
- trace!("Step succeeded, but there's more steps. Spawning next action.");
- if let Ok(mut ent) = cmd.get_entity(steps_action.active_ent.entity()) {
- ent.despawn();
- }
- steps_action.active_step += 1;
- let step_builder = steps_action.steps[steps_action.active_step].clone();
- let step_ent = spawn_action(step_builder.as_ref(), &mut cmd, *actor);
- #[cfg(feature = "trace")]
- trace!("Spawned next step: {:?}", step_ent);
- cmd.entity(seq_ent).add_children(&[step_ent]);
- steps_action.active_ent = Action(step_ent);
- }
- }
- }
- Cancelled => {
- #[cfg(feature = "trace")]
- trace!(
- "StepsAction has been cancelled. Cancelling current step {:?} before finalizing.",
- active_ent
- );
- let mut step_state = states
- .get_mut(active_ent)
- .expect("Failed to find action state for active step entity");
- if matches!(*step_state, Requested | Executing | Init) {
- *step_state = Cancelled;
- } else if matches!(*step_state, Failure | Success) {
- *states.get_mut(seq_ent).unwrap() = step_state.clone();
- }
- }
- Init | Success | Failure => {}
- }
- }
-}
-
-/// Configures what mode the [`Concurrently`] action will run in.
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect)]
-pub enum ConcurrentMode {
- Race,
- Join,
-}
-
-/// [`ActionBuilder`] for the [`Concurrently`] component.
-#[derive(Debug, Reflect)]
-pub struct ConcurrentlyBuilder {
- mode: ConcurrentMode,
- #[reflect(ignore)]
- actions: Vec>,
- action_labels: Vec,
- label: Option,
-}
-
-impl ConcurrentlyBuilder {
- pub fn label>(mut self, label: S) -> Self {
- self.label = Some(label.into());
- self
- }
-
- pub fn push(mut self, action_builder: impl ActionBuilder + 'static) -> Self {
- if let Some(label) = action_builder.label() {
- self.action_labels.push(label.into());
- } else {
- self.action_labels.push("Unnamed Action".into());
- }
- self.actions.push(Arc::new(action_builder));
- self
- }
-
- pub fn mode(mut self, mode: ConcurrentMode) -> Self {
- self.mode = mode;
- self
- }
-}
-
-impl ActionBuilder for ConcurrentlyBuilder {
- fn label(&self) -> Option<&str> {
- self.label.as_deref()
- }
-
- fn build(&self, cmd: &mut Commands, action: Entity, actor: Entity) {
- let children: Vec = self
- .actions
- .iter()
- .map(|action| spawn_action(action.as_ref(), cmd, actor))
- .collect();
- cmd.entity(action)
- .insert(Name::new("Concurrent Action"))
- .add_children(&children[..])
- .insert(Concurrently {
- actions: children.into_iter().map(Action).collect(),
- action_labels: self.action_labels.clone(),
- mode: self.mode,
- });
- }
-}
-
-/// Composite Action that executes a number of Actions concurrently.
-#[derive(Component, Debug, Reflect)]
-pub struct Concurrently {
- mode: ConcurrentMode,
- actions: Vec,
- action_labels: Vec,
-}
-
-impl Concurrently {
- pub fn build() -> ConcurrentlyBuilder {
- ConcurrentlyBuilder {
- actions: Vec::new(),
- action_labels: Vec::new(),
- mode: ConcurrentMode::Join,
- label: None,
- }
- }
-}
-
-/// System that executes [`Concurrently`] Actions.
-pub fn concurrent_system(
- concurrent_q: Query<(Entity, &Concurrently, &ActionSpan)>,
- mut states_q: Query<&mut ActionState>,
-) {
- use ActionState::*;
- for (seq_ent, concurrent_action, _span) in concurrent_q.iter() {
- let current_state = states_q
- .get_mut(seq_ent)
- .expect("Failed to find action state for concurrent sequence entity")
- .clone();
- #[cfg(feature = "trace")]
- let _guard = _span.span.enter();
-
- match current_state {
- Requested => {
- #[cfg(feature = "trace")]
- trace!(
- "Initializing Concurrently action with {} children.",
- concurrent_action.actions.len()
- );
- let mut current_state = states_q.get_mut(seq_ent).expect(ACTION_STATE_ERROR);
- *current_state = Executing;
- for action in concurrent_action.actions.iter() {
- let child_ent = action.entity();
- let mut child_state = states_q.get_mut(child_ent).expect(ACTION_STATE_ERROR);
- *child_state = Requested;
- }
- }
- Executing => match concurrent_action.mode {
- ConcurrentMode::Join => {
- let mut all_success = true;
- let mut failed_idx = None;
- for (idx, action) in concurrent_action.actions.iter().enumerate() {
- let child_ent = action.entity();
- let mut child_state =
- states_q.get_mut(child_ent).expect(ACTION_STATE_ERROR);
- match *child_state {
- Failure => {
- failed_idx = Some(idx);
- all_success = false;
- #[cfg(feature = "trace")]
- trace!(
- "Join action has failed. Cancelling all other actions that haven't completed yet."
- );
- }
- Success => {}
- _ => {
- all_success = false;
- if failed_idx.is_some() {
- *child_state = Cancelled;
- }
- }
- }
- }
- if all_success {
- *states_q.get_mut(seq_ent).expect(ACTION_STATE_ERROR) = Success;
- } else if let Some(idx) = failed_idx {
- for action in concurrent_action.actions.iter().take(idx) {
- let child_ent = action.entity();
- let mut child_state =
- states_q.get_mut(child_ent).expect(ACTION_STATE_ERROR);
- if !matches!(*child_state, Failure | Success) {
- *child_state = Cancelled;
- }
- }
- *states_q.get_mut(seq_ent).expect(ACTION_STATE_ERROR) = Failure;
- }
- }
- ConcurrentMode::Race => {
- let mut all_failure = true;
- let mut succeed_idx = None;
- for (idx, action) in concurrent_action.actions.iter().enumerate() {
- let child_ent = action.entity();
- let mut child_state =
- states_q.get_mut(child_ent).expect(ACTION_STATE_ERROR);
- match *child_state {
- Failure => {}
- Success => {
- succeed_idx = Some(idx);
- all_failure = false;
- #[cfg(feature = "trace")]
- trace!(
- "Race action has succeeded. Cancelling all other actions that haven't completed yet."
- );
- }
- _ => {
- all_failure = false;
- if succeed_idx.is_some() {
- *child_state = Cancelled;
- }
- }
- }
- }
- if all_failure {
- *states_q.get_mut(seq_ent).expect(ACTION_STATE_ERROR) = Failure;
- } else if let Some(idx) = succeed_idx {
- for action in concurrent_action.actions.iter().take(idx) {
- let child_ent = action.entity();
- let mut child_state =
- states_q.get_mut(child_ent).expect(ACTION_STATE_ERROR);
- if !matches!(*child_state, Failure | Success) {
- *child_state = Cancelled;
- }
- }
- *states_q.get_mut(seq_ent).expect(ACTION_STATE_ERROR) = Success;
- }
- }
- },
- Cancelled => {
- let mut all_done = true;
- let mut any_failed = false;
- let mut any_success = false;
- for action in concurrent_action.actions.iter() {
- let child_ent = action.entity();
- let mut child_state = states_q.get_mut(child_ent).expect(ACTION_STATE_ERROR);
- match *child_state {
- Init => {}
- Success => any_success = true,
- Failure => any_failed = true,
- _ => {
- all_done = false;
- *child_state = Cancelled;
- }
- }
- }
- if all_done {
- let mut state_var = states_q.get_mut(seq_ent).expect(ACTION_STATE_ERROR);
- match concurrent_action.mode {
- ConcurrentMode::Race => {
- if any_success {
- #[cfg(feature = "trace")]
- trace!("Race action has succeeded due to succeeded children.");
- *state_var = Success;
- } else {
- #[cfg(feature = "trace")]
- trace!("No race children has completed Successfully.");
- *state_var = Failure;
- }
- }
- ConcurrentMode::Join => {
- if any_failed {
- #[cfg(feature = "trace")]
- trace!("Join action has failed due to failed children.");
- *state_var = Failure;
- } else {
- #[cfg(feature = "trace")]
- trace!("All Join children have completed Successfully.");
- *state_var = Success;
- }
- }
- }
- }
- }
- Init | Success | Failure => {}
- }
- }
-}
diff --git a/crates/systems/ai/src/core/choices.rs b/crates/systems/ai/src/core/choices.rs
deleted file mode 100644
index 0909c09..0000000
--- a/crates/systems/ai/src/core/choices.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use std::sync::Arc;
-
-use bevy::prelude::*;
-
-use crate::core::{
- actions::{ActionBuilder, ActionBuilderWrapper},
- scorers::{self, Score, ScorerBuilder},
- thinkers::Scorer,
-};
-
-/// Contains different types of Considerations and Actions
-#[derive(Debug, Clone, Reflect)]
-#[reflect(from_reflect = false)]
-pub struct Choice {
- pub(crate) scorer: Scorer,
- #[reflect(ignore)]
- pub(crate) action: ActionBuilderWrapper,
- pub(crate) action_label: Option,
-}
-
-impl Choice {
- pub fn calculate(&self, scores: &Query<&Score>) -> f32 {
- scores
- .get(self.scorer.0)
- .expect("Failed to find score component for choice scorer")
- .0
- }
-}
-
-/// Builds a new [`Choice`].
-#[derive(Clone, Debug, Reflect)]
-#[reflect(from_reflect = false)]
-pub struct ChoiceBuilder {
- when_label: Option,
- #[reflect(ignore)]
- pub when: Arc,
- then_label: Option,
- #[reflect(ignore)]
- pub then: Arc,
-}
-impl ChoiceBuilder {
- pub fn new(scorer: Arc, action: Arc) -> Self {
- Self {
- when_label: scorer.label().map(|s| s.into()),
- when: scorer,
- then_label: action.label().map(|s| s.into()),
- then: action,
- }
- }
-
- pub fn build(&self, cmd: &mut Commands, actor: Entity, parent: Entity) -> Choice {
- let scorer_ent = scorers::spawn_scorer(&*self.when, cmd, actor);
- cmd.entity(parent).add_child(scorer_ent);
- Choice {
- scorer: Scorer(scorer_ent),
- action_label: self.then.label().map(|s| s.into()),
- action: ActionBuilderWrapper::new(self.then.clone()),
- }
- }
-}
diff --git a/crates/systems/ai/src/core/evaluators.rs b/crates/systems/ai/src/core/evaluators.rs
deleted file mode 100644
index 06e9eac..0000000
--- a/crates/systems/ai/src/core/evaluators.rs
+++ /dev/null
@@ -1,166 +0,0 @@
-use bevy::prelude::*;
-
-/**
-Trait that any evaluators must implement. Must return an `f32` value between `0.0..=100.0`.
- */
-#[reflect_trait]
-pub trait Evaluator: std::fmt::Debug + Sync + Send {
- fn evaluate(&self, value: f32) -> f32;
-}
-
-/**
-[`Evaluator`] for linear values. That is, there's no curve to the value mapping.
- */
-#[derive(Debug, Clone, Reflect)]
-pub struct LinearEvaluator {
- xa: f32,
- ya: f32,
- yb: f32,
- dy_over_dx: f32,
-}
-
-impl LinearEvaluator {
- pub fn new() -> Self {
- Self::new_full(0.0, 0.0, 1.0, 1.0)
- }
- pub fn new_inversed() -> Self {
- Self::new_ranged(1.0, 0.0)
- }
- pub fn new_ranged(min: f32, max: f32) -> Self {
- Self::new_full(min, 0.0, max, 1.0)
- }
- fn new_full(xa: f32, ya: f32, xb: f32, yb: f32) -> Self {
- Self {
- xa,
- ya,
- yb,
- dy_over_dx: (yb - ya) / (xb - xa),
- }
- }
-}
-
-impl Default for LinearEvaluator {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl Evaluator for LinearEvaluator {
- fn evaluate(&self, value: f32) -> f32 {
- clamp(
- self.ya + self.dy_over_dx * (value - self.xa),
- self.ya,
- self.yb,
- )
- }
-}
-
-/**
-[`Evaluator`] with an exponent curve. The value will grow according to its `power` parameter.
- */
-#[derive(Debug, Clone, Reflect)]
-pub struct PowerEvaluator {
- xa: f32,
- ya: f32,
- xb: f32,
- power: f32,
- dy: f32,
-}
-
-impl PowerEvaluator {
- pub fn new(power: f32) -> Self {
- Self::new_full(power, 0.0, 0.0, 1.0, 1.0)
- }
- pub fn new_ranged(power: f32, min: f32, max: f32) -> Self {
- Self::new_full(power, min, 0.0, max, 1.0)
- }
- fn new_full(power: f32, xa: f32, ya: f32, xb: f32, yb: f32) -> Self {
- Self {
- power: clamp(power, 0.0, 10000.0),
- dy: yb - ya,
- xa,
- ya,
- xb,
- }
- }
-}
-
-impl Default for PowerEvaluator {
- fn default() -> Self {
- Self::new(2.0)
- }
-}
-
-impl Evaluator for PowerEvaluator {
- fn evaluate(&self, value: f32) -> f32 {
- let cx = clamp(value, self.xa, self.xb);
- self.dy * ((cx - self.xa) / (self.xb - self.xa)).powf(self.power) + self.ya
- }
-}
-
-/**
-[`Evaluator`] with a "Sigmoid", or "S-like" curve.
- */
-#[derive(Debug, Clone, Reflect)]
-pub struct SigmoidEvaluator {
- xa: f32,
- xb: f32,
- ya: f32,
- yb: f32,
- k: f32,
- two_over_dx: f32,
- x_mean: f32,
- y_mean: f32,
- dy_over_two: f32,
- one_minus_k: f32,
-}
-
-impl SigmoidEvaluator {
- pub fn new(k: f32) -> Self {
- Self::new_full(k, 0.0, 0.0, 1.0, 1.0)
- }
-
- pub fn new_ranged(k: f32, min: f32, max: f32) -> Self {
- Self::new_full(k, min, 0.0, max, 1.0)
- }
-
- fn new_full(k: f32, xa: f32, ya: f32, xb: f32, yb: f32) -> Self {
- let k = clamp(k, -0.99999, 0.99999);
- Self {
- xa,
- xb,
- ya,
- yb,
- two_over_dx: (2.0 / (xb - ya)).abs(),
- x_mean: (xa + xb) / 2.0,
- y_mean: (ya + yb) / 2.0,
- dy_over_two: (yb - ya) / 2.0,
- one_minus_k: 1.0 - k,
- k,
- }
- }
-}
-
-impl Evaluator for SigmoidEvaluator {
- fn evaluate(&self, x: f32) -> f32 {
- let cx_minus_x_mean = clamp(x, self.xa, self.xb) - self.x_mean;
- let numerator = self.two_over_dx * cx_minus_x_mean * self.one_minus_k;
- let denominator = self.k * (1.0 - 2.0 * (self.two_over_dx * cx_minus_x_mean)).abs() + 1.0;
- clamp(
- self.dy_over_two * (numerator / denominator) + self.y_mean,
- self.ya,
- self.yb,
- )
- }
-}
-
-impl Default for SigmoidEvaluator {
- fn default() -> Self {
- Self::new(-0.5)
- }
-}
-
-pub(crate) fn clamp(val: T, min: T, max: T) -> T {
- let val = if val > max { max } else { val };
- if val < min { min } else { val }
-}
diff --git a/crates/systems/ai/src/core/measures.rs b/crates/systems/ai/src/core/measures.rs
deleted file mode 100644
index 009366f..0000000
--- a/crates/systems/ai/src/core/measures.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-//! * A series of
-//! [Measures](https://en.wikipedia.org/wiki/Measure_(mathematics)) used to
-//! * weight score.
-
-use bevy::prelude::*;
-
-use crate::prelude::Score;
-
-/// A Measure trait describes a way to combine scores together.
-#[reflect_trait]
-pub trait Measure: std::fmt::Debug + Sync + Send {
- /// Calculates a score from the child scores
- fn calculate(&self, inputs: Vec<(&Score, f32)>) -> f32;
-}
-
-/// A measure that adds all the elements together and multiplies them by the
-/// weight.
-#[derive(Debug, Clone, Reflect)]
-pub struct WeightedSum;
-
-impl Measure for WeightedSum {
- fn calculate(&self, scores: Vec<(&Score, f32)>) -> f32 {
- scores
- .iter()
- .fold(0f32, |acc, (score, weight)| acc + score.0 * weight)
- }
-}
-
-/// A measure that multiplies all the elements together.
-#[derive(Debug, Clone, Reflect)]
-pub struct WeightedProduct;
-
-impl Measure for WeightedProduct {
- fn calculate(&self, scores: Vec<(&Score, f32)>) -> f32 {
- if scores.is_empty() {
- return 0.0;
- }
-
- scores
- .iter()
- .fold(1.0f32, |acc, (score, weight)| acc * score.0 * weight)
- }
-}
-
-/// A measure that returns the max of the weighted child scares based on the
-/// one-dimensional (Chebychev
-/// Distance)[https://en.wikipedia.org/wiki/Chebyshev_distance].
-#[derive(Debug, Clone, Reflect)]
-pub struct ChebyshevDistance;
-
-impl Measure for ChebyshevDistance {
- fn calculate(&self, scores: Vec<(&Score, f32)>) -> f32 {
- scores
- .iter()
- .fold(0f32, |best, (score, weight)| (score.0 * weight).max(best))
- }
-}
-
-/// The default measure which uses a weight to provide an intuitive curve.
-#[derive(Debug, Clone, Default, Reflect)]
-pub struct WeightedMeasure;
-
-impl Measure for WeightedMeasure {
- fn calculate(&self, scores: Vec<(&Score, f32)>) -> f32 {
- let wsum: f32 = scores.iter().map(|(_score, weight)| weight).sum();
-
- if wsum == 0.0 {
- 0.0
- } else {
- scores
- .iter()
- .map(|(score, weight)| weight / wsum * score.get().powf(2.0))
- .sum::()
- .powf(1.0 / 2.0)
- }
- }
-}
diff --git a/crates/systems/ai/src/core/mod.rs b/crates/systems/ai/src/core/mod.rs
deleted file mode 100644
index 488d7e8..0000000
--- a/crates/systems/ai/src/core/mod.rs
+++ /dev/null
@@ -1,154 +0,0 @@
-//! Core AI module for LP
-//!
-//! Provides a utility AI system with composable scorers, actions, and thinkers
-//! for sophisticated agent behavior in dynamic ecosystems.
-
-pub mod actions;
-pub mod choices;
-pub mod evaluators;
-pub mod measures;
-pub mod pickers;
-pub mod scorers;
-pub mod thinkers;
-
-/// Prelude for the core AI module.
-///
-/// This includes the most common types you'll need when working with LP's AI system.
-/// Use with `use crate::systems::ai::core::prelude::*;`
-pub mod prelude {
- // Actions (ActionBuilder and ActionState are in actions, but Action is in thinkers)
- pub use crate::core::actions::{
- ActionBuilder, ActionState, ConcurrentMode, Concurrently, Steps,
- };
-
- // Scorers
- pub use crate::core::scorers::{
- AllOrNothing, EvaluatingScorer, FixedScore, MeasuredScorer, ProductOfScorers, Score,
- ScorerBuilder, SumOfScorers, WinningScorer,
- };
-
- // Thinkers (includes Action, Actor, etc.)
- pub use crate::core::thinkers::{
- Action, ActionSpan, Actor, HasThinker, Scorer, ScorerSpan, Thinker, ThinkerBuilder,
- };
-
- // Evaluators
- pub use crate::core::evaluators::{
- Evaluator, LinearEvaluator, PowerEvaluator, SigmoidEvaluator,
- };
-
- // Measures
- pub use crate::core::measures::{ChebyshevDistance, Measure, WeightedProduct, WeightedSum};
-
- // Pickers
- pub use crate::core::pickers::{FirstToScore, Highest, HighestToScore, Picker};
-
- // Choices
- pub use crate::core::choices::{Choice, ChoiceBuilder};
-}
-
-use bevy::{
- ecs::{intern::Interned, schedule::ScheduleLabel},
- prelude::*,
-};
-
-/// Core plugin for LP's AI system. Provides utility AI functionality.
-///
-/// Add this plugin to enable AI behaviors in your app.
-///
-/// # Example
-/// ```rust
-/// # use bevy::prelude::*;
-/// # use ai::prelude::*;
-/// App::new()
-/// .add_plugins(DefaultPlugins)
-/// .add_plugins(LPAIPlugin::new(PreUpdate))
-/// .run();
-/// ```
-#[derive(Debug, Clone, Reflect)]
-#[reflect(from_reflect = false)]
-pub struct LPAIPlugin {
- #[reflect(ignore)]
- schedule: Interned,
- #[reflect(ignore)]
- cleanup_schedule: Interned,
-}
-
-impl Default for LPAIPlugin {
- fn default() -> Self {
- Self::new(Update)
- }
-}
-
-impl LPAIPlugin {
- /// Create the AI plugin which runs in the specified schedule
- pub fn new(schedule: impl ScheduleLabel) -> Self {
- Self {
- schedule: schedule.intern(),
- cleanup_schedule: Last.intern(),
- }
- }
-
- /// Set the schedule for cleanup tasks (default: Last)
- pub fn with_cleanup_schedule(mut self, cleanup_schedule: impl ScheduleLabel) -> Self {
- self.cleanup_schedule = cleanup_schedule.intern();
- self
- }
-}
-
-impl Plugin for LPAIPlugin {
- fn build(&self, app: &mut App) {
- app.configure_sets(
- self.schedule.intern(),
- (AISet::Scorers, AISet::Thinkers, AISet::Actions).chain(),
- )
- .configure_sets(self.cleanup_schedule.intern(), AISet::Cleanup)
- // Add scorer systems
- .add_systems(
- self.schedule.intern(),
- (
- scorers::fixed_score_system,
- scorers::measured_scorers_system,
- scorers::all_or_nothing_system,
- scorers::sum_of_scorers_system,
- scorers::product_of_scorers_system,
- scorers::winning_scorer_system,
- scorers::evaluating_scorer_system,
- )
- .in_set(AISet::Scorers),
- )
- // Add thinker systems
- .add_systems(
- self.schedule.intern(),
- thinkers::thinker_system.in_set(AISet::Thinkers),
- )
- // Add action systems
- .add_systems(
- self.schedule.intern(),
- (actions::steps_system, actions::concurrent_system).in_set(AISet::Actions),
- )
- // Add cleanup systems
- .add_systems(
- self.cleanup_schedule.intern(),
- (
- thinkers::thinker_component_attach_system,
- thinkers::thinker_component_detach_system,
- thinkers::actor_gone_cleanup,
- )
- .in_set(AISet::Cleanup),
- );
- }
-}
-
-/// System sets for organizing AI-related systems
-#[derive(Clone, Debug, Hash, Eq, PartialEq, SystemSet, Reflect)]
-pub enum AISet {
- /// Scorers evaluate world state
- Scorers,
- /// Thinkers make decisions based on scores
- Thinkers,
- /// Actions execute the chosen behaviors
- Actions,
- /// Cleanup tasks run last
- Cleanup,
-}
diff --git a/crates/systems/ai/src/core/pickers.rs b/crates/systems/ai/src/core/pickers.rs
deleted file mode 100644
index 383f1f6..0000000
--- a/crates/systems/ai/src/core/pickers.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-//! Pickers are used by Thinkers to determine which of its Scorers will "win".
-
-use bevy::prelude::*;
-
-use crate::core::{choices::Choice, scorers::Score};
-
-/// Required trait for Pickers. A Picker is given a slice of choices and a
-/// query that can be passed into `Choice::calculate`.
-///
-/// Implementations of `pick` must return `Some(Choice)` for the `Choice` that
-/// was picked, or `None`.
-#[reflect_trait]
-pub trait Picker: std::fmt::Debug + Sync + Send {
- fn pick<'a>(&self, choices: &'a [Choice], scores: &Query<&Score>) -> Option<&'a Choice>;
-}
-
-/// Picker that chooses the first `Choice` with a [`Score`] higher than its
-/// configured `threshold`.
-///
-/// ### Example
-///
-/// ```
-/// # use ai::prelude::*;
-/// # fn main() {
-/// Thinker::build()
-/// .picker(FirstToScore::new(0.8))
-/// // .when(...)
-/// # ;
-/// # }
-/// ```
-#[derive(Debug, Clone, Default)]
-pub struct FirstToScore {
- pub threshold: f32,
-}
-
-impl FirstToScore {
- pub fn new(threshold: f32) -> Self {
- Self { threshold }
- }
-}
-
-impl Picker for FirstToScore {
- fn pick<'a>(&self, choices: &'a [Choice], scores: &Query<&Score>) -> Option<&'a Choice> {
- for choice in choices {
- let value = choice.calculate(scores);
- if value >= self.threshold {
- return Some(choice);
- }
- }
- None
- }
-}
-
-/// Picker that chooses the `Choice` with the highest non-zero [`Score`], and the first highest in case of a tie.
-///
-/// ### Example
-///
-/// ```
-/// # use ai::prelude::*;
-/// # fn main() {
-/// Thinker::build()
-/// .picker(Highest)
-/// // .when(...)
-/// # ;
-/// # }
-/// ```
-#[derive(Debug, Clone, Default)]
-pub struct Highest;
-
-impl Picker for Highest {
- fn pick<'a>(&self, choices: &'a [Choice], scores: &Query<&Score>) -> Option<&'a Choice> {
- let mut max_score = 0f32;
-
- choices.iter().fold(None, |acc, choice| {
- let score = choice.calculate(scores);
-
- if score <= max_score || score <= 0.0 {
- return acc;
- }
-
- max_score = score;
- Some(choice)
- })
- }
-}
-
-/// Picker that chooses the highest `Choice` with a [`Score`] higher than its
-/// configured `threshold`.
-///
-/// ### Example
-///
-/// ```
-/// # use ai::prelude::*;
-/// # fn main() {
-/// Thinker::build()
-/// .picker(HighestToScore::new(0.8))
-/// // .when(...)
-/// # ;
-/// # }
-/// ```
-#[derive(Debug, Clone, Default)]
-pub struct HighestToScore {
- pub threshold: f32,
-}
-
-impl HighestToScore {
- pub fn new(threshold: f32) -> Self {
- Self { threshold }
- }
-}
-
-impl Picker for HighestToScore {
- fn pick<'a>(&self, choices: &'a [Choice], scores: &Query<&Score>) -> Option<&'a Choice> {
- let mut highest_score = 0f32;
-
- choices.iter().fold(None, |acc, choice| {
- let score = choice.calculate(scores);
-
- if score <= self.threshold || score <= highest_score {
- return acc;
- }
-
- highest_score = score;
- Some(choice)
- })
- }
-}
diff --git a/crates/systems/ai/src/core/scorers.rs b/crates/systems/ai/src/core/scorers.rs
deleted file mode 100644
index f055cd8..0000000
--- a/crates/systems/ai/src/core/scorers.rs
+++ /dev/null
@@ -1,915 +0,0 @@
-//! Scorers look at the world and boil down arbitrary characteristics into a
-//! range of 0.0..=1.0. This module includes the ScorerBuilder trait and some
-//! built-in Composite Scorers.
-
-use std::{cmp::Ordering, sync::Arc};
-
-use bevy::prelude::*;
-
-use crate::core::{
- evaluators::Evaluator,
- measures::{Measure, WeightedMeasure},
- thinkers::{Actor, Scorer, ScorerSpan},
-};
-
-/// Score value between `0.0..=1.0` associated with a Scorer.
-///
-/// This type represents normalized utility scores used throughout the AI system.
-/// It provides math operations for combining and transforming scores.
-#[derive(Clone, Copy, Component, Debug, Default, Reflect, PartialEq, PartialOrd)]
-pub struct Score(pub(crate) f32);
-
-impl Score {
- /// Common score constants
- pub const ZERO: Self = Self(0.0);
- pub const HALF: Self = Self(0.5);
- pub const MAX: Self = Self(1.0);
-
- /// Create a new score, clamping to valid range `0.0..=1.0`
- pub fn new(value: f32) -> Self {
- Self(value.clamp(0.0, 1.0))
- }
-
- /// Returns the `Score`'s current value.
- pub fn get(&self) -> f32 {
- self.0
- }
-
- /// Get the raw score value (alias for `get()`)
- pub fn value(&self) -> f32 {
- self.0
- }
-
- /// Set the `Score`'s value.
- ///
- /// Values outside `0.0..=1.0` will be automatically clamped to the valid range.
- pub fn set(&mut self, value: f32) {
- self.0 = value.clamp(0.0, 1.0);
- }
-
- /// Set the `Score`'s value. Allows values outside the range `0.0..=1.0`
- /// WARNING: `Scorer`s are significantly harder to compose when there
- /// isn't a set scale. Avoid using unless it's not feasible to rescale
- /// and use `set` instead.
- pub fn set_unchecked(&mut self, value: f32) {
- self.0 = value;
- }
-
- /// Helper function for clamping trait values to valid range
- pub fn clamp_trait_value(value: f32) -> f32 {
- value.clamp(0.0, 1.0)
- }
-
- /// Normalize a collection of scores to sum to 1.0
- pub fn normalize_scores(scores: &mut [Score]) {
- let total: f32 = scores.iter().map(|score| score.value()).sum();
- if total > 0.0 {
- for score in scores.iter_mut() {
- *score = Score::new(score.value() / total);
- }
- } else {
- // If all scores are 0, distribute evenly
- let equal_value = if scores.is_empty() {
- 0.0
- } else {
- 1.0 / scores.len() as f32
- };
- for score in scores.iter_mut() {
- *score = Score::new(equal_value);
- }
- }
- }
-
- /// Multiply the score by a factor (adjusts importance)
- pub fn multiply_by_factor(&self, factor: f32) -> Self {
- Self::new(self.0 * factor)
- }
-
- /// Combine two scores with AND logic (both must be true)
- /// Returns lower values as both scores must be high
- pub fn and_with(&self, other: &Self) -> Self {
- Self::new(self.0 * other.0)
- }
-
- /// Blend two scores with custom importance weights
- /// weight_a and weight_b should ideally sum to 1.0
- pub fn blend_weighted(&self, other: &Self, weight_a: f32, weight_b: f32) -> Self {
- Self::new(self.0 * weight_a + other.0 * weight_b)
- }
-
- /// Combine scores with OR logic (either can be true)
- /// P(A or B) = P(A) + P(B) - P(A and B)
- pub fn or_with(&self, other: &Self) -> Self {
- Self::new(self.0 + other.0 - (self.0 * other.0))
- }
-
- /// Get the opposite score (1 - score)
- /// Useful for negating or inverting priorities
- pub fn opposite(&self) -> Self {
- Self::new(1.0 - self.0)
- }
-
- /// Perform weighted random selection between multiple options
- pub fn weighted_select(
- options: &[(T, Score)],
- rng: &mut R,
- ) -> Option {
- let total_weight: f32 = options.iter().map(|(_, score)| score.value()).sum();
- if total_weight <= 0.0 || options.is_empty() {
- return None;
- }
- let random_point = rng.random_range(0.0..total_weight);
- let mut cumulative_weight = 0.0;
- for (option, score) in options {
- cumulative_weight += score.value();
- if random_point <= cumulative_weight {
- return Some(option.clone());
- }
- }
- // Fallback to last option (shouldn't happen with proper weights)
- options.last().map(|(opt, _)| opt.clone())
- }
-}
-
-/// Trait for building scorer components. Must implement `build()`.
-#[reflect_trait]
-pub trait ScorerBuilder: std::fmt::Debug + Sync + Send {
- /// MUST insert your concrete Scorer component into the Scorer [`Entity`],
- /// using `cmd`. You _may_ use `actor`, but it's perfectly normal to just
- /// ignore it.
- ///
- /// In most cases, your `ScorerBuilder` and `Scorer` can be the same type.
- /// The only requirement is that your struct implements `Debug`,
- /// `Component, `Clone`. You can then use the derive macro `ScorerBuilder`
- /// to turn your struct into a `ScorerBuilder`
- ///
- /// ### Example
- ///
- /// Using the derive macro (the easy way):
- ///
- /// ```
- /// # use bevy::prelude::*;
- /// # use ai::prelude::*;
- /// #[derive(Debug, Clone, Component, ScorerBuilder)]
- /// #[scorer_label = "MyScorerLabel"] // Optional. Defaults to type name.
- /// struct MyScorer;
- /// ```
- ///
- /// Implementing it manually:
- ///
- /// ```
- /// # use bevy::prelude::*;
- /// # use ai::prelude::*;
- /// #[derive(Debug)]
- /// struct MyBuilder;
- /// #[derive(Debug, Component)]
- /// struct MyScorer;
- ///
- /// impl ScorerBuilder for MyBuilder {
- /// fn build(&self, cmd: &mut Commands, scorer: Entity, _actor: Entity) {
- /// cmd.entity(scorer).insert(MyScorer);
- /// }
- /// }
- /// ```
- fn build(&self, cmd: &mut Commands, scorer: Entity, actor: Entity);
-
- /// Optional label for logging.
- fn label(&self) -> Option<&str> {
- None
- }
-}
-
-pub fn spawn_scorer(
- builder: &T,
- cmd: &mut Commands,
- actor: Entity,
-) -> Entity {
- let scorer_ent = cmd.spawn_empty().id();
- let span = ScorerSpan::new(scorer_ent, ScorerBuilder::label(builder));
- let _guard = span.span().enter();
- debug!("New Scorer spawned.");
- cmd.entity(scorer_ent)
- .insert(Name::new("Scorer"))
- .insert(Score::default())
- .insert(Actor(actor));
- builder.build(cmd, scorer_ent, actor);
- std::mem::drop(_guard);
- cmd.entity(scorer_ent).insert(span);
- scorer_ent
-}
-
-/// Scorer that always returns the same, fixed score.
-
-#[derive(Clone, Component, Debug, Reflect)]
-pub struct FixedScore(pub f32);
-
-impl FixedScore {
- pub fn build(score: f32) -> FixedScorerBuilder {
- FixedScorerBuilder { score, label: None }
- }
-}
-
-pub fn fixed_score_system(mut query: Query<(&FixedScore, &mut Score, &ScorerSpan)>) {
- for (FixedScore(fixed), mut score, _span) in query.iter_mut() {
- #[cfg(feature = "trace")]
- {
- let _guard = _span.span().enter();
- trace!("FixedScore: {}", fixed);
- }
- score.set(*fixed);
- }
-}
-
-#[derive(Debug, Reflect)]
-pub struct FixedScorerBuilder {
- score: f32,
- label: Option,
-}
-
-impl FixedScorerBuilder {
- pub fn label(mut self, label: impl Into) -> Self {
- self.label = Some(label.into());
- self
- }
-}
-
-impl ScorerBuilder for FixedScorerBuilder {
- fn build(&self, cmd: &mut Commands, scorer: Entity, _actor: Entity) {
- cmd.entity(scorer).insert(FixedScore(self.score));
- }
-
- fn label(&self) -> Option<&str> {
- self.label.as_deref().or(Some("FixedScore"))
- }
-}
-
-/// Composite Scorer: sums scores if all individual scores >= threshold.
-#[derive(Component, Debug, Reflect)]
-pub struct AllOrNothing {
- threshold: f32,
- scorers: Vec,
-}
-
-impl AllOrNothing {
- pub fn build(threshold: f32) -> AllOrNothingBuilder {
- AllOrNothingBuilder {
- threshold,
- scorers: Vec::new(),
- scorer_labels: Vec::new(),
- label: None,
- }
- }
-}
-
-pub fn all_or_nothing_system(
- query: Query<(Entity, &AllOrNothing, &ScorerSpan)>,
- mut scores: Query<&mut Score>,
-) {
- for (
- aon_ent,
- AllOrNothing {
- threshold,
- scorers: children,
- },
- _span,
- ) in query.iter()
- {
- let mut sum = 0.0;
- for Scorer(child) in children.iter() {
- let score = scores.get_mut(*child).expect("score missing");
- if score.0 < *threshold {
- sum = 0.0;
- break;
- } else {
- sum += score.0;
- }
- }
- let mut score = scores.get_mut(aon_ent).expect("score missing");
- score.set(crate::core::evaluators::clamp(sum, 0.0, 1.0));
- #[cfg(feature = "trace")]
- {
- let _guard = _span.span().enter();
- trace!("AllOrNothing score: {}", score.get());
- }
- }
-}
-
-#[derive(Debug, Clone, Reflect)]
-pub struct AllOrNothingBuilder {
- threshold: f32,
- #[reflect(ignore)]
- scorers: Vec>,
- scorer_labels: Vec,
- label: Option,
-}
-
-impl AllOrNothingBuilder {
- /// Add another Scorer to this [`ScorerBuilder`].
- pub fn push(mut self, scorer: impl ScorerBuilder + 'static) -> Self {
- if let Some(label) = scorer.label() {
- self.scorer_labels.push(label.into());
- } else {
- self.scorer_labels.push("Unnamed Scorer".into());
- }
- self.scorers.push(Arc::new(scorer));
- self
- }
-
- pub fn label(mut self, label: impl AsRef) -> Self {
- self.label = Some(label.as_ref().into());
- self
- }
-}
-
-impl ScorerBuilder for AllOrNothingBuilder {
- fn label(&self) -> Option<&str> {
- self.label.as_deref().or(Some("AllOrNothing"))
- }
-
- fn build(&self, cmd: &mut Commands, scorer: Entity, actor: Entity) {
- let scorers: Vec<_> = self
- .scorers
- .iter()
- .map(|scorer| spawn_scorer(&**scorer, cmd, actor))
- .collect();
- cmd.entity(scorer)
- .insert(Score::default())
- .add_children(&scorers[..])
- .insert(Name::new("Scorer"))
- .insert(AllOrNothing {
- threshold: self.threshold,
- scorers: scorers.into_iter().map(Scorer).collect(),
- });
- }
-}
-
-/// Composite Scorer: sums all scores, returns 0 if total < threshold.
-#[derive(Component, Debug, Reflect)]
-pub struct SumOfScorers {
- threshold: f32,
- scorers: Vec,
- scorer_labels: Vec,
-}
-
-impl SumOfScorers {
- pub fn build(threshold: f32) -> SumOfScorersBuilder {
- SumOfScorersBuilder {
- threshold,
- scorers: Vec::new(),
- scorer_labels: Vec::new(),
- label: None,
- }
- }
-}
-
-pub fn sum_of_scorers_system(
- query: Query<(Entity, &SumOfScorers, &ScorerSpan)>,
- mut scores: Query<&mut Score>,
-) {
- for (
- sos_ent,
- SumOfScorers {
- threshold,
- scorers: children,
- ..
- },
- _span,
- ) in query.iter()
- {
- let mut sum = 0.0;
- for Scorer(child) in children.iter() {
- let score = scores
- .get_mut(*child)
- .expect("Failed to find score component for SumOfScorers child");
- sum += score.0;
- }
- if sum < *threshold {
- sum = 0.0;
- }
- let mut score = scores
- .get_mut(sos_ent)
- .expect("Failed to find score component for SumOfScorers entity");
- score.set(crate::core::evaluators::clamp(sum, 0.0, 1.0));
- #[cfg(feature = "trace")]
- {
- let _guard = _span.span().enter();
- trace!(
- "SumOfScorers score: {}, from {} scores",
- score.get(),
- children.len()
- );
- }
- }
-}
-
-#[derive(Debug, Clone, Reflect)]
-pub struct SumOfScorersBuilder {
- threshold: f32,
- #[reflect(ignore)]
- scorers: Vec>,
- scorer_labels: Vec,
- label: Option,
-}
-
-impl SumOfScorersBuilder {
- /// Add a new Scorer to this [`SumOfScorersBuilder`].
- pub fn push(mut self, scorer: impl ScorerBuilder + 'static) -> Self {
- if let Some(label) = scorer.label() {
- self.scorer_labels.push(label.into());
- } else {
- self.scorer_labels.push("Unnamed Scorer".into());
- }
- self.scorers.push(Arc::new(scorer));
- self
- }
-
- /// Set a label for this Action.
- pub fn label(mut self, label: impl AsRef) -> Self {
- self.label = Some(label.as_ref().into());
- self
- }
-}
-
-impl ScorerBuilder for SumOfScorersBuilder {
- fn label(&self) -> Option<&str> {
- self.label.as_deref().or(Some("SumOfScorers"))
- }
-
- #[allow(clippy::needless_collect)]
- fn build(&self, cmd: &mut Commands, scorer: Entity, actor: Entity) {
- let scorers: Vec<_> = self
- .scorers
- .iter()
- .map(|scorer| spawn_scorer(&**scorer, cmd, actor))
- .collect();
- cmd.entity(scorer)
- .add_children(&scorers[..])
- .insert(SumOfScorers {
- threshold: self.threshold,
- scorers: scorers.into_iter().map(Scorer).collect(),
- scorer_labels: self.scorer_labels.clone(),
- });
- }
-}
-
-/// Composite Scorer: multiplies all scores with optional compensation factor.
-#[derive(Component, Debug, Reflect)]
-pub struct ProductOfScorers {
- threshold: f32,
- use_compensation: bool,
- scorers: Vec,
- scorer_labels: Vec,
-}
-
-impl ProductOfScorers {
- pub fn build(threshold: f32) -> ProductOfScorersBuilder {
- ProductOfScorersBuilder {
- threshold,
- use_compensation: false,
- scorers: Vec::new(),
- scorer_labels: Vec::new(),
- label: None,
- }
- }
-}
-
-pub fn product_of_scorers_system(
- query: Query<(Entity, &ProductOfScorers, &ScorerSpan)>,
- mut scores: Query<&mut Score>,
-) {
- for (
- sos_ent,
- ProductOfScorers {
- threshold,
- use_compensation,
- scorers: children,
- ..
- },
- _span,
- ) in query.iter()
- {
- let mut product = 1.0;
- let mut num_scorers = 0;
-
- for Scorer(child) in children.iter() {
- let score = scores
- .get_mut(*child)
- .expect("Failed to find score component for ProductOfScorers child");
- product *= score.0;
- num_scorers += 1;
- }
-
- // See for example
- // http://www.gdcvault.com/play/1021848/Building-a-Better-Centaur-AI
- if *use_compensation && product < 1.0 {
- let mod_factor = 1.0 - 1.0 / (num_scorers as f32);
- let makeup = (1.0 - product) * mod_factor;
- product += makeup * product;
- }
-
- if product < *threshold {
- product = 0.0;
- }
-
- let mut score = scores
- .get_mut(sos_ent)
- .expect("Failed to find score component for ProductOfScorers entity");
- score.set(product.clamp(0.0, 1.0));
- #[cfg(feature = "trace")]
- {
- let _guard = _span.span().enter();
- trace!(
- "ProductOfScorers score: {}, from {} scores",
- score.get(),
- children.len()
- );
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ProductOfScorersBuilder {
- threshold: f32,
- use_compensation: bool,
- scorers: Vec>,
- scorer_labels: Vec,
- label: Option,
-}
-
-impl ProductOfScorersBuilder {
- /// To account for the fact that the total score will be reduced for
- /// scores with more inputs, we can optionally apply a compensation factor
- /// by calling this and passing `true`
- pub fn use_compensation(mut self, use_compensation: bool) -> Self {
- self.use_compensation = use_compensation;
- self
- }
-
- /// Add a new scorer to this [`ProductOfScorersBuilder`].
- pub fn push(mut self, scorer: impl ScorerBuilder + 'static) -> Self {
- if let Some(label) = scorer.label() {
- self.scorer_labels.push(label.into());
- } else {
- self.scorer_labels.push("Unnamed Scorer".into());
- }
- self.scorers.push(Arc::new(scorer));
- self
- }
-
- /// Set a label for this Action.
- pub fn label(mut self, label: impl AsRef) -> Self {
- self.label = Some(label.as_ref().into());
- self
- }
-}
-
-impl ScorerBuilder for ProductOfScorersBuilder {
- fn label(&self) -> Option<&str> {
- self.label.as_deref().or(Some("ProductOfScorers"))
- }
-
- #[allow(clippy::needless_collect)]
- fn build(&self, cmd: &mut Commands, scorer: Entity, actor: Entity) {
- let scorers: Vec<_> = self
- .scorers
- .iter()
- .map(|scorer| spawn_scorer(&**scorer, cmd, actor))
- .collect();
- cmd.entity(scorer)
- .add_children(&scorers[..])
- .insert(ProductOfScorers {
- threshold: self.threshold,
- use_compensation: self.use_compensation,
- scorers: scorers.into_iter().map(Scorer).collect(),
- scorer_labels: self.scorer_labels.clone(),
- });
- }
-}
-
-/// Composite Scorer: returns highest score if any score >= threshold.
-#[derive(Component, Debug, Reflect)]
-pub struct WinningScorer {
- threshold: f32,
- scorers: Vec,
- scorer_labels: Vec,
-}
-
-impl WinningScorer {
- pub fn build(threshold: f32) -> WinningScorerBuilder {
- WinningScorerBuilder {
- threshold,
- scorers: Vec::new(),
- scorer_labels: Vec::new(),
- label: None,
- }
- }
-}
-
-pub fn winning_scorer_system(
- mut query: Query<(Entity, &mut WinningScorer, &ScorerSpan)>,
- mut scores: Query<&mut Score>,
-) {
- for (sos_ent, mut winning_scorer, _span) in query.iter_mut() {
- let (threshold, children) = (winning_scorer.threshold, &mut winning_scorer.scorers);
- let winning_score_or_zero = children
- .iter()
- .map(|Scorer(e)| {
- scores
- .get(*e)
- .expect("Failed to find score component for WinningScorer child")
- })
- .map(|score| score.get())
- .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
- .filter(|&score| score >= threshold)
- .unwrap_or(0.0);
- let mut score = scores
- .get_mut(sos_ent)
- .expect("Failed to find score component for WinningScorer entity");
- score.set(crate::core::evaluators::clamp(
- winning_score_or_zero,
- 0.0,
- 1.0,
- ));
- #[cfg(feature = "trace")]
- {
- let _guard = _span.span().enter();
- trace!(
- "WinningScorer score: {}, from {} scores",
- score.get(),
- children.len()
- );
- }
- }
-}
-
-#[derive(Debug, Clone, Reflect)]
-pub struct WinningScorerBuilder {
- threshold: f32,
- #[reflect(ignore)]
- scorers: Vec>,
- scorer_labels: Vec,
- label: Option,
-}
-
-impl WinningScorerBuilder {
- /// Add another Scorer to this [`WinningScorerBuilder`].
- pub fn push(mut self, scorer: impl ScorerBuilder + 'static) -> Self {
- if let Some(label) = scorer.label() {
- self.scorer_labels.push(label.into());
- } else {
- self.scorer_labels.push("Unnamed Scorer".into())
- }
- self.scorers.push(Arc::new(scorer));
- self
- }
-
- /// Set a label for this Action.
- pub fn label(mut self, label: impl AsRef) -> Self {
- self.label = Some(label.as_ref().into());
- self
- }
-}
-
-impl ScorerBuilder for WinningScorerBuilder {
- fn label(&self) -> Option<&str> {
- self.label.as_deref().or(Some("WinningScorer"))
- }
-
- #[allow(clippy::needless_collect)]
- fn build(&self, cmd: &mut Commands, scorer: Entity, actor: Entity) {
- let scorers: Vec<_> = self
- .scorers
- .iter()
- .map(|scorer| spawn_scorer(&**scorer, cmd, actor))
- .collect();
- cmd.entity(scorer)
- .add_children(&scorers[..])
- .insert(WinningScorer {
- threshold: self.threshold,
- scorers: scorers.into_iter().map(Scorer).collect(),
- scorer_labels: self.scorer_labels.clone(),
- });
- }
-}
-
-/// Applies an Evaluator to transform a single scorer's output.
-#[derive(Component, Debug, Reflect)]
-#[reflect(from_reflect = false)]
-pub struct EvaluatingScorer {
- scorer: Scorer,
- evaluator_string: String,
- #[reflect(ignore)]
- evaluator: Arc,
-}
-
-impl EvaluatingScorer {
- pub fn build(
- scorer: impl ScorerBuilder + 'static,
- evaluator: impl Evaluator + 'static,
- ) -> EvaluatingScorerBuilder {
- EvaluatingScorerBuilder {
- scorer_label: scorer.label().map(|s| s.into()),
- evaluator: Arc::new(evaluator),
- scorer: Arc::new(scorer),
- label: None,
- }
- }
-}
-
-pub fn evaluating_scorer_system(
- query: Query<(Entity, &EvaluatingScorer, &ScorerSpan)>,
- mut scores: Query<&mut Score>,
-) {
- for (sos_ent, eval_scorer, _span) in query.iter() {
- // Get the inner score
- let inner_score = scores
- .get(eval_scorer.scorer.0)
- .expect("Failed to find score component for TrustScorer inner scorer")
- .get();
- // Get composite score
- let mut score = scores
- .get_mut(sos_ent)
- .expect("Failed to find score component for TrustScorer entity");
- score.set(crate::core::evaluators::clamp(
- eval_scorer.evaluator.evaluate(inner_score),
- 0.0,
- 1.0,
- ));
- #[cfg(feature = "trace")]
- {
- let _guard = _span.span().enter();
- trace!(
- "EvaluatingScorer score: {}, from score: {}",
- score.get(),
- inner_score
- );
- }
- }
-}
-
-#[derive(Debug, Reflect)]
-#[reflect(from_reflect = false)]
-pub struct EvaluatingScorerBuilder {
- #[reflect(ignore)]
- scorer: Arc,
- scorer_label: Option,
- #[reflect(ignore)]
- evaluator: Arc,
- label: Option,
-}
-
-impl ScorerBuilder for EvaluatingScorerBuilder {
- fn label(&self) -> Option<&str> {
- self.label.as_deref().or(Some("EvaluatingScorer"))
- }
-
- fn build(&self, cmd: &mut Commands, scorer: Entity, actor: Entity) {
- let inner_scorer = spawn_scorer(&*self.scorer, cmd, actor);
- let scorers = [inner_scorer];
- cmd.entity(scorer)
- .add_children(&scorers[..])
- .insert(EvaluatingScorer {
- evaluator: self.evaluator.clone(),
- scorer: Scorer(inner_scorer),
- evaluator_string: format!("{:#?}", self.evaluator),
- });
- }
-}
-
-/// Composite Scorer: combines scores using customizable Measure (default: weighted).
-#[derive(Component, Debug, Reflect)]
-#[reflect(from_reflect = false)]
-pub struct MeasuredScorer {
- threshold: f32,
- #[reflect(ignore)]
- measure: Arc,
- measure_string: String,
- scorers: Vec<(Scorer, f32)>,
-}
-
-impl MeasuredScorer {
- pub fn build(threshold: f32) -> MeasuredScorerBuilder {
- MeasuredScorerBuilder {
- threshold,
- measure: Arc::new(WeightedMeasure),
- measure_string: format!("{WeightedMeasure:#?}"),
- scorers: Vec::new(),
- scorer_labels: Vec::new(),
- label: None,
- }
- }
-}
-
-pub fn measured_scorers_system(
- query: Query<(Entity, &MeasuredScorer, &ScorerSpan)>,
- mut scores: Query<&mut Score>,
-) {
- for (
- sos_ent,
- MeasuredScorer {
- threshold,
- measure,
- scorers: children,
- ..
- },
- _span,
- ) in query.iter()
- {
- let measured_score = measure.calculate(
- children
- .iter()
- .map(|(scorer, weight)| {
- (
- scores
- .get(scorer.0)
- .expect("Failed to find score component for WeightedScorer child"),
- *weight,
- )
- })
- .collect::>(),
- );
- let mut score = scores
- .get_mut(sos_ent)
- .expect("Failed to find score component for WeightedScorer entity");
-
- if measured_score < *threshold {
- score.set(0.0);
- } else {
- score.set(measured_score.clamp(0.0, 1.0));
- }
- #[cfg(feature = "trace")]
- {
- let _guard = _span.span().enter();
- trace!(
- "MeasuredScorer score: {}, from {} scores",
- score.get(),
- children.len()
- );
- }
- }
-}
-
-#[derive(Debug, Reflect)]
-#[reflect(from_reflect = false)]
-pub struct MeasuredScorerBuilder {
- threshold: f32,
- #[reflect(ignore)]
- measure: Arc,
- measure_string: String,
- #[reflect(ignore)]
- scorers: Vec<(Arc, f32)>,
- scorer_labels: Vec,
- label: Option,
-}
-
-impl MeasuredScorerBuilder {
- /// Sets the measure to be used to combine the child scorers
- pub fn measure(mut self, measure: impl Measure + 'static) -> Self {
- self.measure_string = format!("{measure:#?}");
- self.measure = Arc::new(measure);
- self
- }
-
- pub fn push(mut self, scorer: impl ScorerBuilder + 'static, weight: f32) -> Self {
- if let Some(label) = scorer.label() {
- self.scorer_labels.push(label.into());
- } else {
- self.scorer_labels.push("Unnamed Scorer".into());
- }
- self.scorers.push((Arc::new(scorer), weight));
- self
- }
-
- /// Set a label for this ScorerBuilder.
- pub fn label(mut self, label: impl AsRef) -> Self {
- self.label = Some(label.as_ref().into());
- self
- }
-}
-
-impl ScorerBuilder for MeasuredScorerBuilder {
- fn label(&self) -> Option<&str> {
- self.label.as_deref().or(Some("MeasuredScorer"))
- }
-
- #[allow(clippy::needless_collect)]
- fn build(&self, cmd: &mut Commands, scorer: Entity, actor: Entity) {
- let scorers: Vec<_> = self
- .scorers
- .iter()
- .map(|(scorer, _)| spawn_scorer(&**scorer, cmd, actor))
- .collect();
- cmd.entity(scorer)
- .add_children(&scorers[..])
- .insert(MeasuredScorer {
- threshold: self.threshold,
- measure: self.measure.clone(),
- scorers: scorers
- .into_iter()
- .map(Scorer)
- .zip(self.scorers.iter().map(|(_, weight)| *weight))
- .collect(),
- measure_string: self.measure_string.clone(),
- });
- }
-}
diff --git a/crates/systems/ai/src/core/thinkers.rs b/crates/systems/ai/src/core/thinkers.rs
deleted file mode 100644
index 0c7d694..0000000
--- a/crates/systems/ai/src/core/thinkers.rs
+++ /dev/null
@@ -1,548 +0,0 @@
-//! Thinkers are the "brain" of an entity. You attach Scorers to it, and the
-//! Thinker picks the right Action to run based on the resulting Scores.
-
-use std::{
- collections::VecDeque,
- sync::Arc,
- time::{Duration, Instant},
-};
-
-use bevy::{
- log::{
- Level,
- tracing::{Span, field, span},
- },
- prelude::*,
-};
-
-use crate::core::{
- actions::{self, ActionBuilder, ActionBuilderWrapper, ActionState},
- choices::{Choice, ChoiceBuilder},
- pickers::Picker,
- scorers::{Score, ScorerBuilder},
-};
-
-/// Wrapper for Actor entities. In terms of Scorers, Thinkers, and Actions,
-/// this is the Entity actually performing the action.
-#[derive(Debug, Clone, Component, Copy, Reflect)]
-pub struct Actor(pub Entity);
-
-#[derive(Debug, Clone, Copy, Reflect)]
-pub struct Action(pub Entity);
-
-impl Action {
- pub fn entity(&self) -> Entity {
- self.0
- }
-}
-
-#[derive(Debug, Clone, Component)]
-pub struct ActionSpan {
- pub(crate) span: Span,
-}
-
-impl ActionSpan {
- pub(crate) fn new(action: Entity, label: Option<&str>) -> Self {
- let span = span!(
- Level::DEBUG,
- "action",
- ent = ?action,
- label = field::Empty,
- );
- if let Some(label) = label {
- span.record("label", label);
- }
- Self { span }
- }
-
- pub fn span(&self) -> &Span {
- &self.span
- }
-}
-
-#[derive(Debug, Clone, Copy, Reflect)]
-pub struct Scorer(pub Entity);
-
-#[derive(Debug, Clone, Component)]
-pub struct ScorerSpan {
- pub(crate) span: Span,
-}
-
-impl ScorerSpan {
- pub(crate) fn new(scorer: Entity, label: Option<&str>) -> Self {
- let span = span!(
- Level::DEBUG,
- "scorer",
- ent = ?scorer,
- label = field::Empty,
- );
-
- if let Some(label) = label {
- span.record("label", label);
- }
- Self { span }
- }
-
- pub fn span(&self) -> &Span {
- &self.span
- }
-}
-
-/// The "brains" behind this whole operation. A `Thinker` is what glues
-/// together `Actions` and `Scorers` and shapes larger, intelligent-seeming
-/// systems.
-#[derive(Component, Debug, Reflect)]
-#[reflect(from_reflect = false)]
-pub struct Thinker {
- #[reflect(ignore)]
- picker: Arc,
- #[reflect(ignore)]
- otherwise: Option,
- #[reflect(ignore)]
- choices: Vec,
- #[reflect(ignore)]
- current_action: Option<(Action, ActionBuilderWrapper)>,
- current_action_label: Option