From 78662a8e66938007501fd2af331d27074d3e2bf2 Mon Sep 17 00:00:00 2001 From: AlephCubed Date: Mon, 26 Jan 2026 09:44:07 -0800 Subject: [PATCH 1/7] Change how merge functions are publicized. This makes effect timers and effect stacks consistent. --- src/component/stack.rs | 6 +++--- src/component/timer.rs | 16 ++++++---------- src/registry.rs | 3 +-- tests/merge_mode.rs | 10 ++++++---- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/component/stack.rs b/src/component/stack.rs index ab717b1..634c676 100644 --- a/src/component/stack.rs +++ b/src/component/stack.rs @@ -11,7 +11,7 @@ pub(crate) struct StackPlugin; impl Plugin for StackPlugin { fn build(&self, app: &mut App) { app.world_mut() - .resource_mut::() + .get_resource_or_init::() .register::(merge_effect_stacks); } } @@ -55,8 +55,8 @@ impl AddAssign for EffectStacks { } } -/// Merge logic for [`EffectStacks`]. -fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) { +/// A [merge function](crate::EffectMergeFn) for the [`EffectStacks`] component. +pub fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) { let outgoing = *new.world().get::(outgoing).unwrap(); *new.get_mut::().unwrap() += outgoing.0; } diff --git a/src/component/timer.rs b/src/component/timer.rs index 73e3db3..95e65c4 100644 --- a/src/component/timer.rs +++ b/src/component/timer.rs @@ -14,19 +14,15 @@ pub(crate) struct TimerPlugin; impl Plugin for TimerPlugin { fn build(&self, app: &mut App) { app.add_systems(PreUpdate, (despawn_finished_lifetimes, tick_delay).chain()); - register_timer_merge_functions(&mut app.world_mut().resource_mut::()); + app.world_mut() + .get_resource_or_init::() + .register::(merge_effect_timer::) + .register::(merge_effect_timer::); } } -/// Registers the default merge logic for [`Lifetime`] and [`Delay`]. -pub fn register_timer_merge_functions(registry: &mut EffectMergeRegistry) { - registry - .register::(merge_timer::) - .register::(merge_timer::); -} - -/// Merge logic for [`Lifetime`] and [`Delay`]. -fn merge_timer + Clone>( +/// A [merge function](crate::EffectMergeFn) for [`EffectTimer`] components ([`Lifetime`] and [`Delay`]). +pub fn merge_effect_timer + Clone>( mut new: EntityWorldMut, outgoing: Entity, ) { diff --git a/src/registry.rs b/src/registry.rs index 254ba17..287c4d3 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -32,9 +32,8 @@ pub type EffectMergeFn = fn(new: EntityWorldMut, outgoing: Entity); /// /// fn main() { /// let mut world = World::new(); -/// world.init_resource::(); /// -/// world.resource_mut::() +/// world.get_resource_or_init::() /// .register::(merge_my_effect); /// } /// diff --git a/tests/merge_mode.rs b/tests/merge_mode.rs index e1469b8..5fe44ba 100644 --- a/tests/merge_mode.rs +++ b/tests/merge_mode.rs @@ -12,7 +12,9 @@ fn init_world() -> World { let mut world = World::new(); let mut registry = EffectMergeRegistry::default(); - register_timer_merge_functions(&mut registry); + registry + .register::(merge_effect_timer::) + .register::(merge_effect_timer::); world.insert_resource(registry); @@ -41,7 +43,7 @@ fn stack() { let effects: Vec = world .query::<&MyEffect>() - .iter(&mut world) + .iter(&world) .map(|c| c.0) .collect(); @@ -70,7 +72,7 @@ fn insert() { let effects: Vec = world .query::<&MyEffect>() - .iter(&mut world) + .iter(&world) .map(|c| c.0) .collect(); @@ -108,7 +110,7 @@ fn mixed() { let effects: Vec = world .query::<&MyEffect>() - .iter(&mut world) + .iter(&world) .map(|c| c.0) .collect(); From 9b6996cf0c6afbdd0402fa3d4ead6ace17dbf4ef Mon Sep 17 00:00:00 2001 From: AlephCubed Date: Mon, 26 Jan 2026 09:45:32 -0800 Subject: [PATCH 2/7] Fixed warning referencing old `StatusEffectPlugin`. --- src/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command.rs b/src/command.rs index a7acb84..b526a57 100644 --- a/src/command.rs +++ b/src/command.rs @@ -47,7 +47,7 @@ impl AddEffectCommand { fn merge(self, world: &mut World, existing_entity: Entity) { if !world.contains_resource::() { warn_once!( - "No `EffectComponentMergeRegistry` found. Did you forget to add the `StatusEffectPlugin`?" + "No `EffectComponentMergeRegistry` found. Did you forget to add the `AlchemyPlugin`?" ); return; } From 16a6e54c3d8806c14b9954378c4658b497c8173a Mon Sep 17 00:00:00 2001 From: AlephCubed Date: Mon, 26 Jan 2026 09:52:32 -0800 Subject: [PATCH 3/7] Updated version number. --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92ced39..d1814bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,7 +338,7 @@ dependencies = [ [[package]] name = "bevy_alchemy" -version = "0.2.1" +version = "0.3.0" dependencies = [ "bevy", "bevy_app", diff --git a/Cargo.toml b/Cargo.toml index 40c8feb..a57a7c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_alchemy" -version = "0.2.1" +version = "0.3.0" edition = "2024" description = "An experimental, status effects-as-entities system for Bevy." categories = ["game-development"] diff --git a/README.md b/README.md index e3e6aa1..b6d6be2 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Two timers are added by the crate: ### Bevy Version Compatibility -| Bevy | Bevy Alchemy | -|--------|--------------| -| `0.18` | `0.2` | -| `0.17` | `0.1` | \ No newline at end of file +| Bevy | Bevy Alchemy | +|--------|---------------| +| `0.18` | `0.2` - `0.3` | +| `0.17` | `0.1` | \ No newline at end of file From 4d068d1bd55d5cad6c91a89f34e3f978b98c1b2c Mon Sep 17 00:00:00 2001 From: AlephCubed Date: Mon, 26 Jan 2026 10:00:50 -0800 Subject: [PATCH 4/7] Added `EffectStacks` to readme and some doc rewording. --- README.md | 12 ++++++++---- src/component/stack.rs | 2 +- src/component/timer.rs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b6d6be2..3d88008 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,14 @@ fn deal_poison_damage( } ``` -### Timers -Two timers are added by the crate: -1. `Lifetime` - Despawns the effect when the timer ends. -2. `Delay` - A repeating timer used for the delay between effect applications. +### Utility Components +A handful of components are included that are intended to make it easier to create common effects. + +| Component | Description | +|----------------|-------------------------------------------------------------------------------| +| `Lifetime` | A timer that despawns the effect when the timer finishes. | +| `Delay` | A repeating timer used for the delay between effect applications. | +| `EffectStacks` | Tracks the number of times a merge-mode effect has been applied to an entity. | ### Bevy Version Compatibility diff --git a/src/component/stack.rs b/src/component/stack.rs index 634c676..4e077d6 100644 --- a/src/component/stack.rs +++ b/src/component/stack.rs @@ -16,7 +16,7 @@ impl Plugin for StackPlugin { } } -/// Tracks the number stacks of a [merge effect](crate::EffectMode::Merge) that have been applied to an entity. +/// Tracks the number of times a [merge-mode](crate::EffectMode::Merge) effect has been applied to an entity. #[derive(Component, Reflect, Eq, PartialEq, Ord, PartialOrd, Debug, Copy, Clone)] #[reflect(Component, Default, PartialEq, Debug, Clone)] pub struct EffectStacks(pub u8); diff --git a/src/component/timer.rs b/src/component/timer.rs index 95e65c4..9173a9e 100644 --- a/src/component/timer.rs +++ b/src/component/timer.rs @@ -92,7 +92,7 @@ macro_rules! impl_effect_timer { }; } -/// Despawns the entity when the timer finishes. +/// A timer that despawns the effect when the timer finishes. #[doc(alias = "Duration")] #[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)] #[reflect(Component, PartialEq, Debug, Clone)] From 7119a55fe02f2c4becd8b9eb163cea833ff8834f Mon Sep 17 00:00:00 2001 From: AlephCubed Date: Mon, 26 Jan 2026 10:05:08 -0800 Subject: [PATCH 5/7] More implementations for `EffectStacks`. --- src/component/stack.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/component/stack.rs b/src/component/stack.rs index 4e077d6..01d374d 100644 --- a/src/component/stack.rs +++ b/src/component/stack.rs @@ -41,6 +41,20 @@ impl DerefMut for EffectStacks { } } +impl Add for EffectStacks { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl AddAssign for EffectStacks { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0 + } +} + impl Add for EffectStacks { type Output = Self; @@ -55,6 +69,18 @@ impl AddAssign for EffectStacks { } } +impl From for EffectStacks { + fn from(value: u8) -> Self { + EffectStacks(value) + } +} + +impl From for u8 { + fn from(value: EffectStacks) -> Self { + value.0 + } +} + /// A [merge function](crate::EffectMergeFn) for the [`EffectStacks`] component. pub fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) { let outgoing = *new.world().get::(outgoing).unwrap(); From 2de0852e8d495773df318a7d882b6990536be33c Mon Sep 17 00:00:00 2001 From: AlephCubed Date: Mon, 26 Jan 2026 10:18:52 -0800 Subject: [PATCH 6/7] Added getters to `EffectTimer` and a default implementation to the `merge` method. --- src/component/timer.rs | 79 +++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/src/component/timer.rs b/src/component/timer.rs index 9173a9e..e3a8aca 100644 --- a/src/component/timer.rs +++ b/src/component/timer.rs @@ -30,8 +30,7 @@ pub fn merge_effect_timer + Clo new.get_mut::().unwrap().merge(&outgoing); } -// Todo With more getters/settings, `merge` could have a default implementation. -/// A timer which is used for status effects and includes a [`TimerMergeMode`]. +/// A [timer](Timer) which is used for status effects and includes a [`TimerMergeMode`]. pub trait EffectTimer: Sized { /// Creates a new timer from a duration. fn new(duration: Duration) -> Self; @@ -44,9 +43,44 @@ pub trait EffectTimer: Sized { /// A builder that overwrites the current merge mode with a new value. fn with_mode(self, mode: TimerMergeMode) -> Self; - /// Merges a new timer (self) with the old one (other). + /// Returns reference to the internal timer. + fn get_timer(&self) -> &Timer; + + /// Returns mutable reference to the internal timer. + fn get_timer_mut(&mut self) -> &mut Timer; + + /// Returns reference to the timer's merge mode. + fn get_mode(&self) -> &TimerMergeMode; + + /// Returns mutable reference to the timer's merge mode. + fn get_mode_mut(&mut self) -> &mut TimerMergeMode; + + /// Merges an old timer (self) with the new one (incoming). /// Behaviour depends on the current [`TimerMergeMode`]. - fn merge(&mut self, incoming: &Self); + fn merge(&mut self, incoming: &Self) { + match self.get_mode() { + TimerMergeMode::Replace => {} + TimerMergeMode::Keep => *self.get_timer_mut() = incoming.get_timer().clone(), + TimerMergeMode::Fraction => { + let fraction = incoming.get_timer().fraction(); + let duration = self.get_timer().duration().as_secs_f32(); + self.get_timer_mut() + .set_elapsed(Duration::from_secs_f32(fraction * duration)); + } + TimerMergeMode::Max => { + let old = incoming.get_timer().remaining_secs(); + let new = self.get_timer().remaining_secs(); + + if old > new { + *self.get_timer_mut() = incoming.get_timer().clone(); + } + } + TimerMergeMode::Sum => { + let duration = incoming.get_timer().duration() + self.get_timer().duration(); + self.get_timer_mut().set_duration(duration); + } + } + } } macro_rules! impl_effect_timer { @@ -64,29 +98,20 @@ macro_rules! impl_effect_timer { self } - fn merge(&mut self, other: &Self) { - match self.mode { - TimerMergeMode::Replace => {} - TimerMergeMode::Keep => self.timer = other.timer.clone(), - TimerMergeMode::Fraction => { - let fraction = other.timer.fraction(); - let duration = self.timer.duration().as_secs_f32(); - self.timer - .set_elapsed(Duration::from_secs_f32(fraction * duration)); - } - TimerMergeMode::Max => { - let old = other.timer.remaining_secs(); - let new = self.timer.remaining_secs(); - - if old > new { - self.timer = other.timer.clone(); - } - } - TimerMergeMode::Sum => { - self.timer - .set_duration(other.timer.duration() + self.timer.duration()); - } - } + fn get_timer(&self) -> &Timer { + &self.timer + } + + fn get_timer_mut(&mut self) -> &mut Timer { + &mut self.timer + } + + fn get_mode(&self) -> &TimerMergeMode { + &self.mode + } + + fn get_mode_mut(&mut self) -> &mut TimerMergeMode { + &mut self.mode } } }; From 343e1baab1b7360cd30b6b69f713e220894910e1 Mon Sep 17 00:00:00 2001 From: AlephCubed Date: Mon, 26 Jan 2026 10:36:46 -0800 Subject: [PATCH 7/7] Added `trigger_immediately` method to `Delay`. --- examples/poison.rs | 5 +++-- examples/poison_falloff.rs | 5 +++-- src/component/timer.rs | 12 +++++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/poison.rs b/examples/poison.rs index f7b4cbe..05238e5 100644 --- a/examples/poison.rs +++ b/examples/poison.rs @@ -46,8 +46,9 @@ fn on_space_pressed( commands.entity(*target).with_effect(EffectBundle { bundle: ( - Lifetime::from_seconds(4.0), // The duration of the effect. - Delay::from_seconds(1.0), // The time between damage ticks. + Lifetime::from_seconds(3.0), // The duration of the effect. + Delay::from_seconds(1.0) // The time between damage ticks. + .trigger_immediately(), // Make damage tick immediately when the effect is applied. Poison { damage: 1 }, // The amount of damage to apply per tick. ), ..default() diff --git a/examples/poison_falloff.rs b/examples/poison_falloff.rs index e27a6b5..73ea919 100644 --- a/examples/poison_falloff.rs +++ b/examples/poison_falloff.rs @@ -52,8 +52,9 @@ fn on_space_pressed( mode: EffectMode::Merge, // Stack tracking requires effect merging. bundle: ( EffectStacks::default(), // Enable stack tracking. - Lifetime::from_seconds(4.0), // The duration of the effect. - Delay::from_seconds(1.0), // The time between damage ticks. + Lifetime::from_seconds(3.0), // The duration of the effect. + Delay::from_seconds(1.0) // The time between damage ticks. + .trigger_immediately(), // Make damage tick immediately when the effect is applied. Poison { damage: 5 }, // The amount of damage to apply per tick. ), ..default() diff --git a/src/component/timer.rs b/src/component/timer.rs index e3a8aca..d980729 100644 --- a/src/component/timer.rs +++ b/src/component/timer.rs @@ -139,7 +139,7 @@ impl Default for Lifetime { } } -/// A repeating timer used for the delay between effect applications. +/// A repeating timer used for the delay between effect applications. #[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)] #[reflect(Component, PartialEq, Debug, Clone)] pub struct Delay { @@ -151,6 +151,16 @@ pub struct Delay { impl_effect_timer!(Delay, TimerMode::Repeating); +impl Delay { + /// Makes the timer [almost finished](Timer::almost_finish), leaving 1ns of remaining time. + /// This allows effects to trigger immediately when applied. + #[doc(alias = "trigger_on_start", alias = "almost_finish")] + pub fn trigger_immediately(mut self) -> Self { + self.timer.almost_finish(); + self + } +} + impl Default for Delay { fn default() -> Self { Self {