diff --git a/immediate_stats/src/bevy.rs b/immediate_stats/src/bevy.rs --- a/immediate_stats/src/bevy.rs +++ b/immediate_stats/src/bevy.rs @@ -1,9 +1,6 @@ -#[cfg(feature = "bevy_butler")] -pub mod butler; - use crate::StatContainer; +use crate::modifier::Modifier; use crate::stat::Stat; -use crate::stat::modifier::Modifier; use bevy_app::{App, Plugin}; use bevy_ecs::component::Mutable; use bevy_ecs::prelude::ReflectComponent; @@ -38,74 +35,5 @@ pub fn reset_resource_modifiers(res: Option>) { if let Some(mut res) = res { res.reset_modifiers(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::stat::Stat; - use bevy_ecs::prelude::World; - use immediate_stats_macros::StatContainer; - - #[derive(Component, Resource, StatContainer, PartialEq, Debug, Clone)] - struct Health(Stat); - - #[test] - fn reset_component() { - let mut world = World::new(); - let system = world.register_system(reset_component_modifiers::); - - let entity = world - .spawn(Health(Stat { - base: 100, - bonus: 50, - multiplier: 2.0, - })) - .id(); - - world.run_system(system).unwrap(); - - assert_eq!( - world.get::(entity), - Some(Health(Stat::new(100))).as_ref() - ); - } - - #[test] - fn pause_component() { - let mut world = World::new(); - let system = world.register_system(reset_component_modifiers::); - - let health = Health(Stat { - base: 100, - bonus: 50, - multiplier: 2.0, - }); - - let entity = world.spawn((health.clone(), PauseStatReset)).id(); - - world.run_system(system).unwrap(); - - assert_eq!(world.get::(entity), Some(health).as_ref()); - } - - #[test] - fn reset_resource() { - let mut world = World::new(); - let system = world.register_system(reset_resource_modifiers::); - - world.insert_resource(Health(Stat { - base: 100, - bonus: 50, - multiplier: 2.0, - })); - - world.run_system(system).unwrap(); - - assert_eq!( - world.get_resource::(), - Some(Health(Stat::new(100))).as_ref() - ); } } diff --git a/immediate_stats/src/lib.rs b/immediate_stats/src/lib.rs --- a/immediate_stats/src/lib.rs +++ b/immediate_stats/src/lib.rs @@ -2,6 +2,7 @@ #[cfg(feature = "bevy")] pub mod bevy; +pub mod modifier; pub mod stat; // Todo Move into internal module called `__internal` like Bevy Butler. @@ -11,6 +12,7 @@ #[cfg(feature = "bevy")] pub use bevy::*; +pub use modifier::*; pub use stat::*; /// Types that contain stats that need to be reset. @@ -22,171 +24,4 @@ pub trait StatContainer { /// Resets all stat bonuses to zero, and stat multipliers to one. fn reset_modifiers(&mut self); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::stat::Stat; - use immediate_stats_macros::StatContainer; - - #[derive(StatContainer, PartialEq, Debug)] - struct Movement { - speed: Stat, - other: bool, - } - - #[test] - fn reset() { - for base in 0..10 { - let mut movement = Movement { - speed: Stat { - base, - bonus: 3, - multiplier: 1.5, - }, - other: true, - }; - - movement.reset_modifiers(); - - assert_eq!( - movement, - Movement { - speed: Stat::new(base), - other: true - } - ); - } - } - - #[derive(StatContainer, PartialEq, Debug)] - struct MaxHealth(Stat, bool); - - #[test] - fn reset_tuple_struct() { - for base in 0..10 { - let mut max_health = MaxHealth( - Stat { - base, - bonus: 3, - multiplier: 1.5, - }, - true, - ); - - max_health.reset_modifiers(); - - assert_eq!(max_health.0, Stat::new(base)); - } - } - - #[derive(StatContainer, PartialEq, Debug)] - struct Health { - #[stat] - max: MaxHealth, - current: i32, - } - - #[test] - fn reset_with_attribute() { - for base in 0..10 { - let mut health = Health { - max: MaxHealth( - Stat { - base, - bonus: 3, - multiplier: 1.5, - }, - true, - ), - current: base, - }; - - health.reset_modifiers(); - - assert_eq!(health.max.0, Stat::new(base)); - } - } - - #[derive(StatContainer, PartialEq, Debug)] - struct PartialReset { - #[stat_ignore] - ignored: Stat, - reset: Stat, - } - - #[test] - fn reset_ignored() { - for base in 0..10 { - let stat = Stat { - base, - bonus: 3, - multiplier: 1.5, - }; - - let mut partial = PartialReset { - ignored: stat, - reset: stat, - }; - - partial.reset_modifiers(); - - assert_eq!(partial.ignored, stat); - assert_eq!(partial.reset, Stat::new(base)); - } - } - - #[derive(StatContainer, PartialEq, Debug)] - enum EnumStat { - Named { - stat: Stat, - other: u8, - }, - Unnamed(Stat, u8), - #[expect(dead_code)] - Other, - } - - #[test] - fn reset_enum_named() { - for base in 0..10 { - let mut stat = EnumStat::Named { - stat: Stat { - base, - bonus: 3, - multiplier: 1.5, - }, - other: 0, - }; - - stat.reset_modifiers(); - - assert_eq!( - stat, - EnumStat::Named { - stat: Stat::new(base), - other: 0, - } - ); - } - } - - #[test] - fn reset_enum_unnamed() { - for base in 0..10 { - let mut stat = EnumStat::Unnamed( - Stat { - base, - bonus: 3, - multiplier: 1.5, - }, - 0, - ); - - stat.reset_modifiers(); - - assert_eq!(stat, EnumStat::Unnamed(Stat::new(base), 0)); - } - } } diff --git a/immediate_stats/src/modifier.rs b/immediate_stats/src/modifier.rs new file mode 100644 --- /dev/null +++ b/immediate_stats/src/modifier.rs @@ -0,0 +1,68 @@ +use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign}; + +/// Modifier values that can be applied to a [`super::Stat`]. +#[derive(PartialEq, Debug, Copy, Clone)] +#[cfg_attr( + feature = "bevy", + derive(bevy_reflect::Reflect), + reflect(PartialEq, Debug, Clone) +)] +pub struct Modifier { + /// Added to `base` of a [`super::Stat`] during calculation. + pub bonus: i32, + /// Multiplies the `base` of a [`super::Stat`] during calculation. + pub multiplier: f32, +} + +impl Modifier { + pub fn new(bonus: i32, multiplier: f32) -> Self { + Self { bonus, multiplier } + } + + pub fn from_bonus(bonus: i32) -> Self { + Self { + bonus, + ..Self::default() + } + } + + pub fn from_multiplier(multiplier: f32) -> Self { + Self { + multiplier, + ..Self::default() + } + } +} + +impl Default for Modifier { + fn default() -> Self { + Self { + bonus: 0, + multiplier: 1.0, + } + } +} + +impl AddAssign for Modifier { + fn add_assign(&mut self, rhs: i32) { + self.bonus += rhs; + } +} + +impl SubAssign for Modifier { + fn sub_assign(&mut self, rhs: i32) { + self.bonus -= rhs; + } +} + +impl MulAssign for Modifier { + fn mul_assign(&mut self, rhs: f32) { + self.multiplier *= rhs; + } +} + +impl DivAssign for Modifier { + fn div_assign(&mut self, rhs: f32) { + self.multiplier /= rhs; + } +} diff --git a/immediate_stats/src/stat.rs b/immediate_stats/src/stat.rs --- a/immediate_stats/src/stat.rs +++ b/immediate_stats/src/stat.rs @@ -1,7 +1,5 @@ -pub mod modifier; - use crate::StatContainer; -use crate::stat::modifier::Modifier; +use crate::modifier::Modifier; use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign}; /// A stat that [resets] to a base value every iteration. @@ -114,132 +112,5 @@ impl DivAssign for Stat { fn div_assign(&mut self, rhs: f32) { self.multiplier /= rhs; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reset() { - for i in 0..10 { - let mut stat = Stat { - base: i, - bonus: 4, - multiplier: 1.5, - }; - stat.reset_modifiers(); - - assert_eq!(stat, Stat::new(i)); - } - } - - #[test] - fn add() { - let mut stat = Stat::new(10); - stat += 5; - assert_eq!( - stat, - Stat { - base: 10, - bonus: 5, - multiplier: 1.0, - } - ); - } - - #[test] - fn subtract() { - let mut stat = Stat::new(10); - stat -= 5; - assert_eq!( - stat, - Stat { - base: 10, - bonus: -5, - multiplier: 1.0, - } - ); - } - - #[test] - fn multiply() { - let mut stat = Stat::new(10); - stat *= 2.0; - assert_eq!( - stat, - Stat { - base: 10, - bonus: 0, - multiplier: 2.0, - } - ); - } - - #[test] - fn divide() { - let mut stat = Stat::new(10); - stat /= 2.0; - assert_eq!( - stat, - Stat { - base: 10, - bonus: 0, - multiplier: 0.5, - } - ); - } - - #[test] - fn default_total() { - for i in 0..10 { - assert_eq!(Stat::new(i).total(), i); - } - } - - #[test] - fn total() { - assert_eq!( - Stat { - base: 10, - bonus: 4, - multiplier: 1.5, - } - .total(), - 21 - ) - } - - #[test] - fn total_no_bonus() { - assert_eq!(Stat::new(20).with_multiplier(0.5).total(), 10); - } - - #[test] - fn total_no_multiplier() { - assert_eq!(Stat::new(2).with_bonus(1).total(), 3); - } - - #[test] - fn total_with_modifier() { - assert_eq!(Stat::new(5).with_modifier(Modifier::new(1, 0.5)).total(), 3); - } - - #[test] - fn apply() { - let mut stat = Stat::new(10); - - stat.apply(Modifier::new(2, 2.0)); - stat.apply(Modifier::new(3, 4.0)); - - assert_eq!( - stat, - Stat { - base: 10, - bonus: 5, - multiplier: 8.0, - } - ) } } diff --git a/immediate_stats/tests/bevy.rs b/immediate_stats/tests/bevy.rs new file mode 100644 --- /dev/null +++ b/immediate_stats/tests/bevy.rs @@ -0,0 +1,66 @@ +#![cfg(feature = "bevy")] +//! Tests the reset systems and `PauseStatReset`. + +use bevy_ecs::prelude::*; +use immediate_stats::*; + +#[derive(Component, Resource, StatContainer, PartialEq, Debug, Clone)] +struct Health(Stat); + +#[test] +fn reset_component() { + let mut world = World::new(); + let system = world.register_system(reset_component_modifiers::); + + let entity = world + .spawn(Health(Stat { + base: 100, + bonus: 50, + multiplier: 2.0, + })) + .id(); + + world.run_system(system).unwrap(); + + assert_eq!( + world.get::(entity), + Some(Health(Stat::new(100))).as_ref() + ); +} + +#[test] +fn pause_component() { + let mut world = World::new(); + let system = world.register_system(reset_component_modifiers::); + + let health = Health(Stat { + base: 100, + bonus: 50, + multiplier: 2.0, + }); + + let entity = world.spawn((health.clone(), PauseStatReset)).id(); + + world.run_system(system).unwrap(); + + assert_eq!(world.get::(entity), Some(health).as_ref()); +} + +#[test] +fn reset_resource() { + let mut world = World::new(); + let system = world.register_system(reset_resource_modifiers::); + + world.insert_resource(Health(Stat { + base: 100, + bonus: 50, + multiplier: 2.0, + })); + + world.run_system(system).unwrap(); + + assert_eq!( + world.get_resource::(), + Some(Health(Stat::new(100))).as_ref() + ); +} diff --git a/immediate_stats/tests/bevy_butler.rs b/immediate_stats/tests/bevy_butler.rs new file mode 100644 --- /dev/null +++ b/immediate_stats/tests/bevy_butler.rs @@ -0,0 +1,60 @@ +#![cfg(feature = "bevy_butler")] +//! Tests the `add_component` attribute for automatic system registration. + +use crate::StatContainer; +use crate::stat::Stat; +use bevy_app::App; +use bevy_butler::*; +use bevy_ecs::prelude::*; +use immediate_stats::*; + +#[butler_plugin] +struct MyPlugin; + +#[derive(Resource, Component, StatContainer, Default, PartialEq, Debug)] +#[add_component(plugin = MyPlugin)] +#[add_resource(plugin = MyPlugin)] +struct Health(Stat); + +#[test] +fn reset_component_auto() { + let mut app = App::new(); + + app.add_plugins(MyPlugin); + + let entity = app + .world_mut() + .spawn(Health(Stat { + base: 100, + bonus: 50, + multiplier: 2.0, + })) + .id(); + + app.update(); + + assert_eq!( + app.world().get::(entity), + Some(Health(Stat::new(100))).as_ref() + ); +} + +#[test] +fn reset_resource_auto() { + let mut app = App::new(); + + app.add_plugins(MyPlugin); + + app.insert_resource(Health(Stat { + base: 100, + bonus: 50, + multiplier: 2.0, + })); + + app.update(); + + assert_eq!( + app.world().get_resource::(), + Some(Health(Stat::new(100))).as_ref() + ); +} diff --git a/immediate_stats/tests/derive.rs b/immediate_stats/tests/derive.rs new file mode 100644 --- /dev/null +++ b/immediate_stats/tests/derive.rs @@ -0,0 +1,163 @@ +//! Tests the basic `StatContainer` derive with no extra features. + +use immediate_stats::*; + +#[derive(StatContainer, PartialEq, Debug)] +struct Movement { + speed: Stat, + other: bool, +} + +#[test] +fn reset() { + for base in 0..10 { + let mut movement = Movement { + speed: Stat { + base, + bonus: 3, + multiplier: 1.5, + }, + other: true, + }; + + movement.reset_modifiers(); + + assert_eq!( + movement, + Movement { + speed: Stat::new(base), + other: true + } + ); + } +} + +#[derive(StatContainer, PartialEq, Debug)] +struct MaxHealth(Stat, bool); + +#[test] +fn reset_tuple_struct() { + for base in 0..10 { + let mut max_health = MaxHealth( + Stat { + base, + bonus: 3, + multiplier: 1.5, + }, + true, + ); + + max_health.reset_modifiers(); + + assert_eq!(max_health.0, Stat::new(base)); + } +} + +#[derive(StatContainer, PartialEq, Debug)] +struct Health { + #[stat] + max: MaxHealth, + current: i32, +} + +#[test] +fn reset_with_attribute() { + for base in 0..10 { + let mut health = Health { + max: MaxHealth( + Stat { + base, + bonus: 3, + multiplier: 1.5, + }, + true, + ), + current: base, + }; + + health.reset_modifiers(); + + assert_eq!(health.max.0, Stat::new(base)); + } +} + +#[derive(StatContainer, PartialEq, Debug)] +struct PartialReset { + #[stat_ignore] + ignored: Stat, + reset: Stat, +} + +#[test] +fn reset_ignored() { + for base in 0..10 { + let stat = Stat { + base, + bonus: 3, + multiplier: 1.5, + }; + + let mut partial = PartialReset { + ignored: stat, + reset: stat, + }; + + partial.reset_modifiers(); + + assert_eq!(partial.ignored, stat); + assert_eq!(partial.reset, Stat::new(base)); + } +} + +#[derive(StatContainer, PartialEq, Debug)] +enum EnumStat { + Named { + stat: Stat, + other: u8, + }, + Unnamed(Stat, u8), + #[expect(dead_code)] + Other, +} + +#[test] +fn reset_enum_named() { + for base in 0..10 { + let mut stat = EnumStat::Named { + stat: Stat { + base, + bonus: 3, + multiplier: 1.5, + }, + other: 0, + }; + + stat.reset_modifiers(); + + assert_eq!( + stat, + EnumStat::Named { + stat: Stat::new(base), + other: 0, + } + ); + } +} + +#[test] +fn reset_enum_unnamed() { + for base in 0..10 { + let mut stat = EnumStat::Unnamed( + Stat { + base, + bonus: 3, + multiplier: 1.5, + }, + 0, + ); + + stat.reset_modifiers(); + + assert_eq!(stat, EnumStat::Unnamed(Stat::new(base), 0)); + } +} diff --git a/immediate_stats/tests/modifier.rs b/immediate_stats/tests/modifier.rs new file mode 100644 --- /dev/null +++ b/immediate_stats/tests/modifier.rs @@ -0,0 +1,55 @@ +//! Tests the various methods of `Modifier`. + +use immediate_stats::*; + +#[test] +fn add() { + let mut modifier = Modifier::default(); + modifier += 5; + assert_eq!( + modifier, + Modifier { + bonus: 5, + multiplier: 1.0, + } + ); +} + +#[test] +fn subtract() { + let mut modifier = Modifier::default(); + modifier -= 5; + assert_eq!( + modifier, + Modifier { + bonus: -5, + multiplier: 1.0, + } + ); +} + +#[test] +fn multiply() { + let mut modifier = Modifier::default(); + modifier *= 2.0; + assert_eq!( + modifier, + Modifier { + bonus: 0, + multiplier: 2.0, + } + ); +} + +#[test] +fn divide() { + let mut modifier = Modifier::default(); + modifier /= 2.0; + assert_eq!( + modifier, + Modifier { + bonus: 0, + multiplier: 0.5, + } + ); +} diff --git a/immediate_stats/tests/stat.rs b/immediate_stats/tests/stat.rs new file mode 100644 --- /dev/null +++ b/immediate_stats/tests/stat.rs @@ -0,0 +1,125 @@ +//! Tests the various methods of `Stat`. + +use immediate_stats::*; + +#[test] +fn reset() { + for i in 0..10 { + let mut stat = Stat { + base: i, + bonus: 4, + multiplier: 1.5, + }; + stat.reset_modifiers(); + + assert_eq!(stat, Stat::new(i)); + } +} + +#[test] +fn add() { + let mut stat = Stat::new(10); + stat += 5; + assert_eq!( + stat, + Stat { + base: 10, + bonus: 5, + multiplier: 1.0, + } + ); +} + +#[test] +fn subtract() { + let mut stat = Stat::new(10); + stat -= 5; + assert_eq!( + stat, + Stat { + base: 10, + bonus: -5, + multiplier: 1.0, + } + ); +} + +#[test] +fn multiply() { + let mut stat = Stat::new(10); + stat *= 2.0; + assert_eq!( + stat, + Stat { + base: 10, + bonus: 0, + multiplier: 2.0, + } + ); +} + +#[test] +fn divide() { + let mut stat = Stat::new(10); + stat /= 2.0; + assert_eq!( + stat, + Stat { + base: 10, + bonus: 0, + multiplier: 0.5, + } + ); +} + +#[test] +fn default_total() { + for i in 0..10 { + assert_eq!(Stat::new(i).total(), i); + } +} + +#[test] +fn total() { + assert_eq!( + Stat { + base: 10, + bonus: 4, + multiplier: 1.5, + } + .total(), + 21 + ) +} + +#[test] +fn total_no_bonus() { + assert_eq!(Stat::new(20).with_multiplier(0.5).total(), 10); +} + +#[test] +fn total_no_multiplier() { + assert_eq!(Stat::new(2).with_bonus(1).total(), 3); +} + +#[test] +fn total_with_modifier() { + assert_eq!(Stat::new(5).with_modifier(Modifier::new(1, 0.5)).total(), 3); +} + +#[test] +fn apply() { + let mut stat = Stat::new(10); + + stat.apply(Modifier::new(2, 2.0)); + stat.apply(Modifier::new(3, 4.0)); + + assert_eq!( + stat, + Stat { + base: 10, + bonus: 5, + multiplier: 8.0, + } + ) +} diff --git a/immediate_stats/src/bevy/butler.rs b/immediate_stats/src/bevy/butler.rs deleted file mode 100644 --- a/immediate_stats/src/bevy/butler.rs +++ /dev/null @@ -1,60 +0,0 @@ -#[cfg(test)] -mod tests { - use crate as immediate_stats; - use crate::StatContainer; - use crate::stat::Stat; - use bevy_app::App; - use bevy_butler::*; - use bevy_ecs::prelude::*; - - #[butler_plugin] - struct MyPlugin; - - #[derive(Resource, Component, StatContainer, Default, PartialEq, Debug)] - #[add_component(plugin = MyPlugin)] - #[add_resource(plugin = MyPlugin)] - struct Health(Stat); - - #[test] - fn reset_component_auto() { - let mut app = App::new(); - - app.add_plugins(MyPlugin); - - let entity = app - .world_mut() - .spawn(Health(Stat { - base: 100, - bonus: 50, - multiplier: 2.0, - })) - .id(); - - app.update(); - - assert_eq!( - app.world().get::(entity), - Some(Health(Stat::new(100))).as_ref() - ); - } - - #[test] - fn reset_resource_auto() { - let mut app = App::new(); - - app.add_plugins(MyPlugin); - - app.insert_resource(Health(Stat { - base: 100, - bonus: 50, - multiplier: 2.0, - })); - - app.update(); - - assert_eq!( - app.world().get_resource::(), - Some(Health(Stat::new(100))).as_ref() - ); - } -} diff --git a/immediate_stats/src/stat/modifier.rs b/immediate_stats/src/stat/modifier.rs deleted file mode 100644 --- a/immediate_stats/src/stat/modifier.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign}; - -/// Modifier values that can be applied to a [`super::Stat`]. -#[derive(PartialEq, Debug, Copy, Clone)] -#[cfg_attr( - feature = "bevy", - derive(bevy_reflect::Reflect), - reflect(PartialEq, Debug, Clone) -)] -pub struct Modifier { - /// Added to `base` of a [`super::Stat`] during calculation. - pub bonus: i32, - /// Multiplies the `base` of a [`super::Stat`] during calculation. - pub multiplier: f32, -} - -impl Modifier { - pub fn new(bonus: i32, multiplier: f32) -> Self { - Self { bonus, multiplier } - } - - pub fn from_bonus(bonus: i32) -> Self { - Self { - bonus, - ..Self::default() - } - } - - pub fn from_multiplier(multiplier: f32) -> Self { - Self { - multiplier, - ..Self::default() - } - } -} - -impl Default for Modifier { - fn default() -> Self { - Self { - bonus: 0, - multiplier: 1.0, - } - } -} - -impl AddAssign for Modifier { - fn add_assign(&mut self, rhs: i32) { - self.bonus += rhs; - } -} - -impl SubAssign for Modifier { - fn sub_assign(&mut self, rhs: i32) { - self.bonus -= rhs; - } -} - -impl MulAssign for Modifier { - fn mul_assign(&mut self, rhs: f32) { - self.multiplier *= rhs; - } -} - -impl DivAssign for Modifier { - fn div_assign(&mut self, rhs: f32) { - self.multiplier /= rhs; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn add() { - let mut modifier = Modifier::default(); - modifier += 5; - assert_eq!( - modifier, - Modifier { - bonus: 5, - multiplier: 1.0, - } - ); - } - - #[test] - fn subtract() { - let mut modifier = Modifier::default(); - modifier -= 5; - assert_eq!( - modifier, - Modifier { - bonus: -5, - multiplier: 1.0, - } - ); - } - - #[test] - fn multiply() { - let mut modifier = Modifier::default(); - modifier *= 2.0; - assert_eq!( - modifier, - Modifier { - bonus: 0, - multiplier: 2.0, - } - ); - } - - #[test] - fn divide() { - let mut modifier = Modifier::default(); - modifier /= 2.0; - assert_eq!( - modifier, - Modifier { - bonus: 0, - multiplier: 0.5, - } - ); - } -}