An experimental, status effects-as-entities system for Bevy.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #5 from AlephCubed/cleanup

`EffectTimer` getters, `Delay::trigger_immediately`, and assorted cleanup.

authored by

Josiah Nelson and committed by
GitHub
7a0d076c a184d049

+127 -63
+1 -1
Cargo.lock
··· 338 338 339 339 [[package]] 340 340 name = "bevy_alchemy" 341 - version = "0.2.1" 341 + version = "0.3.0" 342 342 dependencies = [ 343 343 "bevy", 344 344 "bevy_app",
+1 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "bevy_alchemy" 3 - version = "0.2.1" 3 + version = "0.3.0" 4 4 edition = "2024" 5 5 description = "An experimental, status effects-as-entities system for Bevy." 6 6 categories = ["game-development"]
+12 -8
README.md
··· 65 65 } 66 66 ``` 67 67 68 - ### Timers 69 - Two timers are added by the crate: 70 - 1. `Lifetime` - Despawns the effect when the timer ends. 71 - 2. `Delay` - A repeating timer used for the delay between effect applications. 68 + ### Utility Components 69 + A handful of components are included that are intended to make it easier to create common effects. 70 + 71 + | Component | Description | 72 + |----------------|-------------------------------------------------------------------------------| 73 + | `Lifetime` | A timer that despawns the effect when the timer finishes. | 74 + | `Delay` | A repeating timer used for the delay between effect applications. | 75 + | `EffectStacks` | Tracks the number of times a merge-mode effect has been applied to an entity. | 72 76 73 77 ### Bevy Version Compatibility 74 78 75 - | Bevy | Bevy Alchemy | 76 - |--------|--------------| 77 - | `0.18` | `0.2` | 78 - | `0.17` | `0.1` | 79 + | Bevy | Bevy Alchemy | 80 + |--------|---------------| 81 + | `0.18` | `0.2` - `0.3` | 82 + | `0.17` | `0.1` |
+3 -2
examples/poison.rs
··· 46 46 47 47 commands.entity(*target).with_effect(EffectBundle { 48 48 bundle: ( 49 - Lifetime::from_seconds(4.0), // The duration of the effect. 50 - Delay::from_seconds(1.0), // The time between damage ticks. 49 + Lifetime::from_seconds(3.0), // The duration of the effect. 50 + Delay::from_seconds(1.0) // The time between damage ticks. 51 + .trigger_immediately(), // Make damage tick immediately when the effect is applied. 51 52 Poison { damage: 1 }, // The amount of damage to apply per tick. 52 53 ), 53 54 ..default()
+3 -2
examples/poison_falloff.rs
··· 52 52 mode: EffectMode::Merge, // Stack tracking requires effect merging. 53 53 bundle: ( 54 54 EffectStacks::default(), // Enable stack tracking. 55 - Lifetime::from_seconds(4.0), // The duration of the effect. 56 - Delay::from_seconds(1.0), // The time between damage ticks. 55 + Lifetime::from_seconds(3.0), // The duration of the effect. 56 + Delay::from_seconds(1.0) // The time between damage ticks. 57 + .trigger_immediately(), // Make damage tick immediately when the effect is applied. 57 58 Poison { damage: 5 }, // The amount of damage to apply per tick. 58 59 ), 59 60 ..default()
+1 -1
src/command.rs
··· 47 47 fn merge(self, world: &mut World, existing_entity: Entity) { 48 48 if !world.contains_resource::<EffectMergeRegistry>() { 49 49 warn_once!( 50 - "No `EffectComponentMergeRegistry` found. Did you forget to add the `StatusEffectPlugin`?" 50 + "No `EffectComponentMergeRegistry` found. Did you forget to add the `AlchemyPlugin`?" 51 51 ); 52 52 return; 53 53 }
+30 -4
src/component/stack.rs
··· 11 11 impl Plugin for StackPlugin { 12 12 fn build(&self, app: &mut App) { 13 13 app.world_mut() 14 - .resource_mut::<EffectMergeRegistry>() 14 + .get_resource_or_init::<EffectMergeRegistry>() 15 15 .register::<EffectStacks>(merge_effect_stacks); 16 16 } 17 17 } 18 18 19 - /// Tracks the number stacks of a [merge effect](crate::EffectMode::Merge) that have been applied to an entity. 19 + /// Tracks the number of times a [merge-mode](crate::EffectMode::Merge) effect has been applied to an entity. 20 20 #[derive(Component, Reflect, Eq, PartialEq, Ord, PartialOrd, Debug, Copy, Clone)] 21 21 #[reflect(Component, Default, PartialEq, Debug, Clone)] 22 22 pub struct EffectStacks(pub u8); ··· 41 41 } 42 42 } 43 43 44 + impl Add for EffectStacks { 45 + type Output = Self; 46 + 47 + fn add(self, rhs: Self) -> Self::Output { 48 + Self(self.0 + rhs.0) 49 + } 50 + } 51 + 52 + impl AddAssign for EffectStacks { 53 + fn add_assign(&mut self, rhs: Self) { 54 + self.0 += rhs.0 55 + } 56 + } 57 + 44 58 impl Add<u8> for EffectStacks { 45 59 type Output = Self; 46 60 ··· 55 69 } 56 70 } 57 71 58 - /// Merge logic for [`EffectStacks`]. 59 - fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) { 72 + impl From<u8> for EffectStacks { 73 + fn from(value: u8) -> Self { 74 + EffectStacks(value) 75 + } 76 + } 77 + 78 + impl From<EffectStacks> for u8 { 79 + fn from(value: EffectStacks) -> Self { 80 + value.0 81 + } 82 + } 83 + 84 + /// A [merge function](crate::EffectMergeFn) for the [`EffectStacks`] component. 85 + pub fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) { 60 86 let outgoing = *new.world().get::<EffectStacks>(outgoing).unwrap(); 61 87 *new.get_mut::<EffectStacks>().unwrap() += outgoing.0; 62 88 }
+69 -38
src/component/timer.rs
··· 14 14 impl Plugin for TimerPlugin { 15 15 fn build(&self, app: &mut App) { 16 16 app.add_systems(PreUpdate, (despawn_finished_lifetimes, tick_delay).chain()); 17 - register_timer_merge_functions(&mut app.world_mut().resource_mut::<EffectMergeRegistry>()); 17 + app.world_mut() 18 + .get_resource_or_init::<EffectMergeRegistry>() 19 + .register::<Lifetime>(merge_effect_timer::<Lifetime>) 20 + .register::<Delay>(merge_effect_timer::<Delay>); 18 21 } 19 22 } 20 23 21 - /// Registers the default merge logic for [`Lifetime`] and [`Delay`]. 22 - pub fn register_timer_merge_functions(registry: &mut EffectMergeRegistry) { 23 - registry 24 - .register::<Lifetime>(merge_timer::<Lifetime>) 25 - .register::<Delay>(merge_timer::<Delay>); 26 - } 27 - 28 - /// Merge logic for [`Lifetime`] and [`Delay`]. 29 - fn merge_timer<T: EffectTimer + Component<Mutability = Mutable> + Clone>( 24 + /// A [merge function](crate::EffectMergeFn) for [`EffectTimer`] components ([`Lifetime`] and [`Delay`]). 25 + pub fn merge_effect_timer<T: EffectTimer + Component<Mutability = Mutable> + Clone>( 30 26 mut new: EntityWorldMut, 31 27 outgoing: Entity, 32 28 ) { ··· 34 30 new.get_mut::<T>().unwrap().merge(&outgoing); 35 31 } 36 32 37 - // Todo With more getters/settings, `merge` could have a default implementation. 38 - /// A timer which is used for status effects and includes a [`TimerMergeMode`]. 33 + /// A [timer](Timer) which is used for status effects and includes a [`TimerMergeMode`]. 39 34 pub trait EffectTimer: Sized { 40 35 /// Creates a new timer from a duration. 41 36 fn new(duration: Duration) -> Self; ··· 48 43 /// A builder that overwrites the current merge mode with a new value. 49 44 fn with_mode(self, mode: TimerMergeMode) -> Self; 50 45 51 - /// Merges a new timer (self) with the old one (other). 46 + /// Returns reference to the internal timer. 47 + fn get_timer(&self) -> &Timer; 48 + 49 + /// Returns mutable reference to the internal timer. 50 + fn get_timer_mut(&mut self) -> &mut Timer; 51 + 52 + /// Returns reference to the timer's merge mode. 53 + fn get_mode(&self) -> &TimerMergeMode; 54 + 55 + /// Returns mutable reference to the timer's merge mode. 56 + fn get_mode_mut(&mut self) -> &mut TimerMergeMode; 57 + 58 + /// Merges an old timer (self) with the new one (incoming). 52 59 /// Behaviour depends on the current [`TimerMergeMode`]. 53 - fn merge(&mut self, incoming: &Self); 60 + fn merge(&mut self, incoming: &Self) { 61 + match self.get_mode() { 62 + TimerMergeMode::Replace => {} 63 + TimerMergeMode::Keep => *self.get_timer_mut() = incoming.get_timer().clone(), 64 + TimerMergeMode::Fraction => { 65 + let fraction = incoming.get_timer().fraction(); 66 + let duration = self.get_timer().duration().as_secs_f32(); 67 + self.get_timer_mut() 68 + .set_elapsed(Duration::from_secs_f32(fraction * duration)); 69 + } 70 + TimerMergeMode::Max => { 71 + let old = incoming.get_timer().remaining_secs(); 72 + let new = self.get_timer().remaining_secs(); 73 + 74 + if old > new { 75 + *self.get_timer_mut() = incoming.get_timer().clone(); 76 + } 77 + } 78 + TimerMergeMode::Sum => { 79 + let duration = incoming.get_timer().duration() + self.get_timer().duration(); 80 + self.get_timer_mut().set_duration(duration); 81 + } 82 + } 83 + } 54 84 } 55 85 56 86 macro_rules! impl_effect_timer { ··· 68 98 self 69 99 } 70 100 71 - fn merge(&mut self, other: &Self) { 72 - match self.mode { 73 - TimerMergeMode::Replace => {} 74 - TimerMergeMode::Keep => self.timer = other.timer.clone(), 75 - TimerMergeMode::Fraction => { 76 - let fraction = other.timer.fraction(); 77 - let duration = self.timer.duration().as_secs_f32(); 78 - self.timer 79 - .set_elapsed(Duration::from_secs_f32(fraction * duration)); 80 - } 81 - TimerMergeMode::Max => { 82 - let old = other.timer.remaining_secs(); 83 - let new = self.timer.remaining_secs(); 101 + fn get_timer(&self) -> &Timer { 102 + &self.timer 103 + } 104 + 105 + fn get_timer_mut(&mut self) -> &mut Timer { 106 + &mut self.timer 107 + } 108 + 109 + fn get_mode(&self) -> &TimerMergeMode { 110 + &self.mode 111 + } 84 112 85 - if old > new { 86 - self.timer = other.timer.clone(); 87 - } 88 - } 89 - TimerMergeMode::Sum => { 90 - self.timer 91 - .set_duration(other.timer.duration() + self.timer.duration()); 92 - } 93 - } 113 + fn get_mode_mut(&mut self) -> &mut TimerMergeMode { 114 + &mut self.mode 94 115 } 95 116 } 96 117 }; 97 118 } 98 119 99 - /// Despawns the entity when the timer finishes. 120 + /// A timer that despawns the effect when the timer finishes. 100 121 #[doc(alias = "Duration")] 101 122 #[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)] 102 123 #[reflect(Component, PartialEq, Debug, Clone)] ··· 118 139 } 119 140 } 120 141 121 - /// A repeating timer used for the delay between effect applications. 142 + /// A repeating timer used for the delay between effect applications. 122 143 #[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)] 123 144 #[reflect(Component, PartialEq, Debug, Clone)] 124 145 pub struct Delay { ··· 129 150 } 130 151 131 152 impl_effect_timer!(Delay, TimerMode::Repeating); 153 + 154 + impl Delay { 155 + /// Makes the timer [almost finished](Timer::almost_finish), leaving 1ns of remaining time. 156 + /// This allows effects to trigger immediately when applied. 157 + #[doc(alias = "trigger_on_start", alias = "almost_finish")] 158 + pub fn trigger_immediately(mut self) -> Self { 159 + self.timer.almost_finish(); 160 + self 161 + } 162 + } 132 163 133 164 impl Default for Delay { 134 165 fn default() -> Self {
+1 -2
src/registry.rs
··· 32 32 /// 33 33 /// fn main() { 34 34 /// let mut world = World::new(); 35 - /// world.init_resource::<EffectMergeRegistry>(); 36 35 /// 37 - /// world.resource_mut::<EffectMergeRegistry>() 36 + /// world.get_resource_or_init::<EffectMergeRegistry>() 38 37 /// .register::<MyEffect>(merge_my_effect); 39 38 /// } 40 39 ///
+6 -4
tests/merge_mode.rs
··· 12 12 let mut world = World::new(); 13 13 14 14 let mut registry = EffectMergeRegistry::default(); 15 - register_timer_merge_functions(&mut registry); 15 + registry 16 + .register::<Lifetime>(merge_effect_timer::<Lifetime>) 17 + .register::<Delay>(merge_effect_timer::<Delay>); 16 18 17 19 world.insert_resource(registry); 18 20 ··· 41 43 42 44 let effects: Vec<u8> = world 43 45 .query::<&MyEffect>() 44 - .iter(&mut world) 46 + .iter(&world) 45 47 .map(|c| c.0) 46 48 .collect(); 47 49 ··· 70 72 71 73 let effects: Vec<u8> = world 72 74 .query::<&MyEffect>() 73 - .iter(&mut world) 75 + .iter(&world) 74 76 .map(|c| c.0) 75 77 .collect(); 76 78 ··· 108 110 109 111 let effects: Vec<u8> = world 110 112 .query::<&MyEffect>() 111 - .iter(&mut world) 113 + .iter(&world) 112 114 .map(|c| c.0) 113 115 .collect(); 114 116