···11[package]
22name = "bevy_alchemy"
33-version = "0.2.1"
33+version = "0.3.0"
44edition = "2024"
55description = "An experimental, status effects-as-entities system for Bevy."
66categories = ["game-development"]
+12-8
README.md
···6565}
6666```
67676868-### Timers
6969-Two timers are added by the crate:
7070-1. `Lifetime` - Despawns the effect when the timer ends.
7171-2. `Delay` - A repeating timer used for the delay between effect applications.
6868+### Utility Components
6969+A handful of components are included that are intended to make it easier to create common effects.
7070+7171+| Component | Description |
7272+|----------------|-------------------------------------------------------------------------------|
7373+| `Lifetime` | A timer that despawns the effect when the timer finishes. |
7474+| `Delay` | A repeating timer used for the delay between effect applications. |
7575+| `EffectStacks` | Tracks the number of times a merge-mode effect has been applied to an entity. |
72767377### Bevy Version Compatibility
74787575-| Bevy | Bevy Alchemy |
7676-|--------|--------------|
7777-| `0.18` | `0.2` |
7878-| `0.17` | `0.1` |7979+| Bevy | Bevy Alchemy |
8080+|--------|---------------|
8181+| `0.18` | `0.2` - `0.3` |
8282+| `0.17` | `0.1` |
+3-2
examples/poison.rs
···46464747 commands.entity(*target).with_effect(EffectBundle {
4848 bundle: (
4949- Lifetime::from_seconds(4.0), // The duration of the effect.
5050- Delay::from_seconds(1.0), // The time between damage ticks.
4949+ Lifetime::from_seconds(3.0), // The duration of the effect.
5050+ Delay::from_seconds(1.0) // The time between damage ticks.
5151+ .trigger_immediately(), // Make damage tick immediately when the effect is applied.
5152 Poison { damage: 1 }, // The amount of damage to apply per tick.
5253 ),
5354 ..default()
+3-2
examples/poison_falloff.rs
···5252 mode: EffectMode::Merge, // Stack tracking requires effect merging.
5353 bundle: (
5454 EffectStacks::default(), // Enable stack tracking.
5555- Lifetime::from_seconds(4.0), // The duration of the effect.
5656- Delay::from_seconds(1.0), // The time between damage ticks.
5555+ Lifetime::from_seconds(3.0), // The duration of the effect.
5656+ Delay::from_seconds(1.0) // The time between damage ticks.
5757+ .trigger_immediately(), // Make damage tick immediately when the effect is applied.
5758 Poison { damage: 5 }, // The amount of damage to apply per tick.
5859 ),
5960 ..default()
+1-1
src/command.rs
···4747 fn merge(self, world: &mut World, existing_entity: Entity) {
4848 if !world.contains_resource::<EffectMergeRegistry>() {
4949 warn_once!(
5050- "No `EffectComponentMergeRegistry` found. Did you forget to add the `StatusEffectPlugin`?"
5050+ "No `EffectComponentMergeRegistry` found. Did you forget to add the `AlchemyPlugin`?"
5151 );
5252 return;
5353 }
+30-4
src/component/stack.rs
···1111impl Plugin for StackPlugin {
1212 fn build(&self, app: &mut App) {
1313 app.world_mut()
1414- .resource_mut::<EffectMergeRegistry>()
1414+ .get_resource_or_init::<EffectMergeRegistry>()
1515 .register::<EffectStacks>(merge_effect_stacks);
1616 }
1717}
18181919-/// Tracks the number stacks of a [merge effect](crate::EffectMode::Merge) that have been applied to an entity.
1919+/// Tracks the number of times a [merge-mode](crate::EffectMode::Merge) effect has been applied to an entity.
2020#[derive(Component, Reflect, Eq, PartialEq, Ord, PartialOrd, Debug, Copy, Clone)]
2121#[reflect(Component, Default, PartialEq, Debug, Clone)]
2222pub struct EffectStacks(pub u8);
···4141 }
4242}
43434444+impl Add for EffectStacks {
4545+ type Output = Self;
4646+4747+ fn add(self, rhs: Self) -> Self::Output {
4848+ Self(self.0 + rhs.0)
4949+ }
5050+}
5151+5252+impl AddAssign for EffectStacks {
5353+ fn add_assign(&mut self, rhs: Self) {
5454+ self.0 += rhs.0
5555+ }
5656+}
5757+4458impl Add<u8> for EffectStacks {
4559 type Output = Self;
4660···5569 }
5670}
57715858-/// Merge logic for [`EffectStacks`].
5959-fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) {
7272+impl From<u8> for EffectStacks {
7373+ fn from(value: u8) -> Self {
7474+ EffectStacks(value)
7575+ }
7676+}
7777+7878+impl From<EffectStacks> for u8 {
7979+ fn from(value: EffectStacks) -> Self {
8080+ value.0
8181+ }
8282+}
8383+8484+/// A [merge function](crate::EffectMergeFn) for the [`EffectStacks`] component.
8585+pub fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) {
6086 let outgoing = *new.world().get::<EffectStacks>(outgoing).unwrap();
6187 *new.get_mut::<EffectStacks>().unwrap() += outgoing.0;
6288}
+69-38
src/component/timer.rs
···1414impl Plugin for TimerPlugin {
1515 fn build(&self, app: &mut App) {
1616 app.add_systems(PreUpdate, (despawn_finished_lifetimes, tick_delay).chain());
1717- register_timer_merge_functions(&mut app.world_mut().resource_mut::<EffectMergeRegistry>());
1717+ app.world_mut()
1818+ .get_resource_or_init::<EffectMergeRegistry>()
1919+ .register::<Lifetime>(merge_effect_timer::<Lifetime>)
2020+ .register::<Delay>(merge_effect_timer::<Delay>);
1821 }
1922}
20232121-/// Registers the default merge logic for [`Lifetime`] and [`Delay`].
2222-pub fn register_timer_merge_functions(registry: &mut EffectMergeRegistry) {
2323- registry
2424- .register::<Lifetime>(merge_timer::<Lifetime>)
2525- .register::<Delay>(merge_timer::<Delay>);
2626-}
2727-2828-/// Merge logic for [`Lifetime`] and [`Delay`].
2929-fn merge_timer<T: EffectTimer + Component<Mutability = Mutable> + Clone>(
2424+/// A [merge function](crate::EffectMergeFn) for [`EffectTimer`] components ([`Lifetime`] and [`Delay`]).
2525+pub fn merge_effect_timer<T: EffectTimer + Component<Mutability = Mutable> + Clone>(
3026 mut new: EntityWorldMut,
3127 outgoing: Entity,
3228) {
···3430 new.get_mut::<T>().unwrap().merge(&outgoing);
3531}
36323737-// Todo With more getters/settings, `merge` could have a default implementation.
3838-/// A timer which is used for status effects and includes a [`TimerMergeMode`].
3333+/// A [timer](Timer) which is used for status effects and includes a [`TimerMergeMode`].
3934pub trait EffectTimer: Sized {
4035 /// Creates a new timer from a duration.
4136 fn new(duration: Duration) -> Self;
···4843 /// A builder that overwrites the current merge mode with a new value.
4944 fn with_mode(self, mode: TimerMergeMode) -> Self;
50455151- /// Merges a new timer (self) with the old one (other).
4646+ /// Returns reference to the internal timer.
4747+ fn get_timer(&self) -> &Timer;
4848+4949+ /// Returns mutable reference to the internal timer.
5050+ fn get_timer_mut(&mut self) -> &mut Timer;
5151+5252+ /// Returns reference to the timer's merge mode.
5353+ fn get_mode(&self) -> &TimerMergeMode;
5454+5555+ /// Returns mutable reference to the timer's merge mode.
5656+ fn get_mode_mut(&mut self) -> &mut TimerMergeMode;
5757+5858+ /// Merges an old timer (self) with the new one (incoming).
5259 /// Behaviour depends on the current [`TimerMergeMode`].
5353- fn merge(&mut self, incoming: &Self);
6060+ fn merge(&mut self, incoming: &Self) {
6161+ match self.get_mode() {
6262+ TimerMergeMode::Replace => {}
6363+ TimerMergeMode::Keep => *self.get_timer_mut() = incoming.get_timer().clone(),
6464+ TimerMergeMode::Fraction => {
6565+ let fraction = incoming.get_timer().fraction();
6666+ let duration = self.get_timer().duration().as_secs_f32();
6767+ self.get_timer_mut()
6868+ .set_elapsed(Duration::from_secs_f32(fraction * duration));
6969+ }
7070+ TimerMergeMode::Max => {
7171+ let old = incoming.get_timer().remaining_secs();
7272+ let new = self.get_timer().remaining_secs();
7373+7474+ if old > new {
7575+ *self.get_timer_mut() = incoming.get_timer().clone();
7676+ }
7777+ }
7878+ TimerMergeMode::Sum => {
7979+ let duration = incoming.get_timer().duration() + self.get_timer().duration();
8080+ self.get_timer_mut().set_duration(duration);
8181+ }
8282+ }
8383+ }
5484}
55855686macro_rules! impl_effect_timer {
···6898 self
6999 }
701007171- fn merge(&mut self, other: &Self) {
7272- match self.mode {
7373- TimerMergeMode::Replace => {}
7474- TimerMergeMode::Keep => self.timer = other.timer.clone(),
7575- TimerMergeMode::Fraction => {
7676- let fraction = other.timer.fraction();
7777- let duration = self.timer.duration().as_secs_f32();
7878- self.timer
7979- .set_elapsed(Duration::from_secs_f32(fraction * duration));
8080- }
8181- TimerMergeMode::Max => {
8282- let old = other.timer.remaining_secs();
8383- let new = self.timer.remaining_secs();
101101+ fn get_timer(&self) -> &Timer {
102102+ &self.timer
103103+ }
104104+105105+ fn get_timer_mut(&mut self) -> &mut Timer {
106106+ &mut self.timer
107107+ }
108108+109109+ fn get_mode(&self) -> &TimerMergeMode {
110110+ &self.mode
111111+ }
841128585- if old > new {
8686- self.timer = other.timer.clone();
8787- }
8888- }
8989- TimerMergeMode::Sum => {
9090- self.timer
9191- .set_duration(other.timer.duration() + self.timer.duration());
9292- }
9393- }
113113+ fn get_mode_mut(&mut self) -> &mut TimerMergeMode {
114114+ &mut self.mode
94115 }
95116 }
96117 };
97118}
981199999-/// Despawns the entity when the timer finishes.
120120+/// A timer that despawns the effect when the timer finishes.
100121#[doc(alias = "Duration")]
101122#[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)]
102123#[reflect(Component, PartialEq, Debug, Clone)]
···118139 }
119140}
120141121121-/// A repeating timer used for the delay between effect applications.
142142+/// A repeating timer used for the delay between effect applications.
122143#[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)]
123144#[reflect(Component, PartialEq, Debug, Clone)]
124145pub struct Delay {
···129150}
130151131152impl_effect_timer!(Delay, TimerMode::Repeating);
153153+154154+impl Delay {
155155+ /// Makes the timer [almost finished](Timer::almost_finish), leaving 1ns of remaining time.
156156+ /// This allows effects to trigger immediately when applied.
157157+ #[doc(alias = "trigger_on_start", alias = "almost_finish")]
158158+ pub fn trigger_immediately(mut self) -> Self {
159159+ self.timer.almost_finish();
160160+ self
161161+ }
162162+}
132163133164impl Default for Delay {
134165 fn default() -> Self {