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 #4 from AlephCubed/stack-component

Added `EffectStacks` component and example.

authored by

Josiah Nelson and committed by
GitHub
a184d049 ceff179c

+205 -16
+1 -1
Cargo.lock
··· 338 338 339 339 [[package]] 340 340 name = "bevy_alchemy" 341 - version = "0.2.0" 341 + version = "0.2.1" 342 342 dependencies = [ 343 343 "bevy", 344 344 "bevy_app",
+5 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "bevy_alchemy" 3 - version = "0.2.0" 3 + version = "0.2.1" 4 4 edition = "2024" 5 5 description = "An experimental, status effects-as-entities system for Bevy." 6 6 categories = ["game-development"] ··· 40 40 [[example]] 41 41 name = "poison" 42 42 path = "examples/poison.rs" 43 + 44 + [[example]] 45 + name = "poison_falloff" 46 + path = "examples/poison_falloff.rs" 43 47 44 48 [[example]] 45 49 name = "decaying_speed"
+5 -4
examples/README.md
··· 1 1 # Examples 2 2 3 - | Example | Description | 4 - |-----------------------|-----------------------------------| 5 - | [`poison`](poison.rs) | A simple damage-over-time effect. | 3 + | Example | Description | 4 + |---------------------------------------|--------------------------------------------------------------------------------| 5 + | [`poison`](poison.rs) | A simple damage-over-time effect. | 6 + | [`poison_falloff`](poison_falloff.rs) | A damage-over-time effect where the damage falls off as more stacks are added. | 6 7 7 8 ## Immediate Stats 8 9 Examples in the `immediate_stats` subdirectory utilize the [`immediate_stats`](https://github.com/AlephCubed/immediate_stats) crate, which I also created. 9 - Some of these examples include a copy that utilizes [`bevy_auto_plugin`](https://github.com/StrikeForceZero/bevy_auto_plugin), which should behave exactly the same. 10 + Some of these examples include a version that utilizes [`bevy_auto_plugin`](https://github.com/StrikeForceZero/bevy_auto_plugin), which should behave exactly the same. 10 11 11 12 | Example | Description | 12 13 |--------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
+2 -1
examples/immediate_stats/decaying_speed.rs
··· 2 2 //! to add a decaying movement speed buff. 3 3 //! This means that the strength of the buff decreases throughout its duration. 4 4 //! 5 - //! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the same time (no 10x speed multiplier for you). 5 + //! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the 6 + //! same time (no 10x speed multiplier for you). 6 7 //! 7 8 //! There is a second version of this example, which uses Bevy Auto Plugin. 8 9
+4 -2
examples/immediate_stats/decaying_speed_auto_plugin.rs
··· 1 1 //! This example shows using [Immediate Stats](https://github.com/AlephCubed/immediate_stats) 2 - //! to add a decaying movement speed buff, using Bevy Auto Plugin (there is a second version of this example which just uses normal Bevy). 2 + //! to add a decaying movement speed buff, using Bevy Auto Plugin 3 + //! (there is a second version of this example which just uses normal Bevy). 3 4 //! This means that the strength of the buff decreases throughout its duration. 4 5 //! 5 - //! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the same time (no 10x speed multiplier for you). 6 + //! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the 7 + //! same time (no 10x speed multiplier for you). 6 8 7 9 use bevy::prelude::*; 8 10 use bevy_alchemy::*;
+4
examples/poison.rs
··· 1 1 //! A simple damage-over-time effect. 2 + //! 3 + //! Each application of the effect is its own entity, meaning an entity can be poisoned multiple times. 4 + //! This can be changed by using a different [`EffectMode`](bevy_alchemy::EffectMode). 5 + //! The `poison_falloff` example shows a different way to handle effect stacking. 2 6 3 7 use bevy::prelude::*; 4 8 use bevy_alchemy::{
+109
examples/poison_falloff.rs
··· 1 + //! A damage-over-time effect where the damage falls off as more stacks are added. 2 + //! 3 + //! When an entity is already poisoned, subsequent applications deal less damage. 4 + //! In this case the first stack deals 5 damage, the next 4, then 3, and so on. 5 + //! 6 + //! This works by [merging](EffectMode::Merge) the effects into a single entity and using the 7 + //! [number of stacks](EffectStacks) in damage calculations. 8 + //! A slightly simpler version is available in the `poison` example. 9 + 10 + use bevy::prelude::*; 11 + use bevy_alchemy::{ 12 + AlchemyPlugin, Delay, EffectBundle, EffectCommandsExt, EffectMode, EffectStacks, EffectTimer, 13 + Effecting, Lifetime, 14 + }; 15 + 16 + fn main() { 17 + App::new() 18 + .add_plugins((DefaultPlugins, AlchemyPlugin)) 19 + .add_systems(Startup, init_scene) 20 + .add_systems(Update, (on_space_pressed, deal_poison_damage)) 21 + .add_systems(PostUpdate, update_ui) 22 + .run(); 23 + } 24 + 25 + #[derive(Component)] 26 + struct Health(i32); 27 + 28 + /// Deals damage over time to the target entity. 29 + #[derive(Component, Default)] 30 + struct Poison { 31 + damage: i32, 32 + } 33 + 34 + /// Spawn a target on startup. 35 + fn init_scene(mut commands: Commands) { 36 + commands.spawn((Name::new("Target"), Health(500))); 37 + commands.spawn(Text::default()); 38 + commands.spawn(Camera2d); 39 + } 40 + 41 + /// When space is pressed, apply poison to the target. 42 + fn on_space_pressed( 43 + mut commands: Commands, 44 + keyboard_input: Res<ButtonInput<KeyCode>>, 45 + target: Single<Entity, With<Health>>, 46 + ) { 47 + if !keyboard_input.just_pressed(KeyCode::Space) { 48 + return; 49 + } 50 + 51 + commands.entity(*target).with_effect(EffectBundle { 52 + mode: EffectMode::Merge, // Stack tracking requires effect merging. 53 + bundle: ( 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. 57 + Poison { damage: 5 }, // The amount of damage to apply per tick. 58 + ), 59 + ..default() 60 + }); 61 + } 62 + 63 + /// Runs every frame and deals the poison damage. 64 + fn deal_poison_damage( 65 + effects: Query<(&Effecting, &EffectStacks, &Delay, &Poison)>, 66 + mut targets: Query<&mut Health>, 67 + ) { 68 + for (target, stacks, delay, poison) in effects { 69 + // We wait until the delay finishes to apply the damage. 70 + if !delay.timer.is_finished() { 71 + continue; 72 + } 73 + 74 + // Skip if the target doesn't have health. 75 + let Ok(mut health) = targets.get_mut(target.0) else { 76 + continue; 77 + }; 78 + 79 + // Otherwise, deal the damage scaled with the number of stacks. 80 + // Each subsequent stack has a decreasing effect, the first deals 5 damage, the next 4, then 3, and so on. 81 + let stacks = poison.damage.min(stacks.0 as i32); // Clamp stacks to prevent negative damage. 82 + let sub = (stacks * (stacks - 1)) / 2; 83 + let damage = poison.damage * stacks - sub; 84 + 85 + info!("Dealt {damage} damage!"); 86 + 87 + health.0 -= damage; 88 + } 89 + } 90 + 91 + fn update_ui( 92 + mut ui: Single<&mut Text>, 93 + target: Single<&Health>, 94 + effects: Query<(Entity, &EffectStacks, &Lifetime, &Delay), With<Poison>>, 95 + ) { 96 + ui.0 = "Press Space to apply poison\n\n".to_string(); 97 + 98 + ui.0 += &format!("Health: {}\n\n", target.0); 99 + 100 + for (entity, stacks, lifetime, delay) in &effects { 101 + ui.0 += &format!( 102 + "{}, {} stacks - {:.1}s (tick in {:.1}s)\n", 103 + entity, 104 + stacks.0, 105 + lifetime.timer.remaining_secs(), 106 + delay.timer.remaining_secs() 107 + ); 108 + } 109 + }
+5
src/component.rs
··· 1 + mod stack; 2 + mod timer; 3 + 4 + pub use stack::*; 5 + pub use timer::*;
+62
src/component/stack.rs
··· 1 + use crate::EffectMergeRegistry; 2 + use bevy_app::{App, Plugin}; 3 + use bevy_ecs::prelude::ReflectComponent; 4 + use bevy_ecs::prelude::{Component, Entity, EntityWorldMut}; 5 + use bevy_reflect::Reflect; 6 + use bevy_reflect::prelude::ReflectDefault; 7 + use std::ops::{Add, AddAssign, Deref, DerefMut}; 8 + 9 + pub(crate) struct StackPlugin; 10 + 11 + impl Plugin for StackPlugin { 12 + fn build(&self, app: &mut App) { 13 + app.world_mut() 14 + .resource_mut::<EffectMergeRegistry>() 15 + .register::<EffectStacks>(merge_effect_stacks); 16 + } 17 + } 18 + 19 + /// Tracks the number stacks of a [merge effect](crate::EffectMode::Merge) that have been applied to an entity. 20 + #[derive(Component, Reflect, Eq, PartialEq, Ord, PartialOrd, Debug, Copy, Clone)] 21 + #[reflect(Component, Default, PartialEq, Debug, Clone)] 22 + pub struct EffectStacks(pub u8); 23 + 24 + impl Default for EffectStacks { 25 + fn default() -> Self { 26 + Self(1) 27 + } 28 + } 29 + 30 + impl Deref for EffectStacks { 31 + type Target = u8; 32 + 33 + fn deref(&self) -> &Self::Target { 34 + &self.0 35 + } 36 + } 37 + 38 + impl DerefMut for EffectStacks { 39 + fn deref_mut(&mut self) -> &mut Self::Target { 40 + &mut self.0 41 + } 42 + } 43 + 44 + impl Add<u8> for EffectStacks { 45 + type Output = Self; 46 + 47 + fn add(self, rhs: u8) -> Self::Output { 48 + Self(self.0 + rhs) 49 + } 50 + } 51 + 52 + impl AddAssign<u8> for EffectStacks { 53 + fn add_assign(&mut self, rhs: u8) { 54 + self.0 += rhs 55 + } 56 + } 57 + 58 + /// Merge logic for [`EffectStacks`]. 59 + fn merge_effect_stacks(mut new: EntityWorldMut, outgoing: Entity) { 60 + let outgoing = *new.world().get::<EffectStacks>(outgoing).unwrap(); 61 + *new.get_mut::<EffectStacks>().unwrap() += outgoing.0; 62 + }
+4 -3
src/lib.rs
··· 2 2 3 3 mod bundle; 4 4 mod command; 5 + mod component; 5 6 mod registry; 6 7 mod relation; 7 - mod timer; 8 8 9 9 use bevy_app::{App, Plugin}; 10 10 use bevy_ecs::prelude::*; ··· 13 13 14 14 pub use bundle::*; 15 15 pub use command::*; 16 + pub use component::*; 16 17 pub use registry::*; 17 18 pub use relation::*; 18 - pub use timer::*; 19 19 20 20 /// Setup required types and systems for `bevy_alchemy`. 21 21 pub struct AlchemyPlugin; ··· 29 29 .register_type::<Delay>() 30 30 .register_type::<TimerMergeMode>() 31 31 .init_resource::<EffectMergeRegistry>() 32 - .add_plugins(TimerPlugin); 32 + .add_plugins(TimerPlugin) 33 + .add_plugins(StackPlugin); 33 34 } 34 35 } 35 36
+1 -1
src/timer.rs src/component/timer.rs
··· 9 9 use bevy_time::{Time, Timer, TimerMode}; 10 10 use std::time::Duration; 11 11 12 - pub(super) struct TimerPlugin; 12 + pub(crate) struct TimerPlugin; 13 13 14 14 impl Plugin for TimerPlugin { 15 15 fn build(&self, app: &mut App) {
+3 -3
tests/spawn_syntax.rs
··· 28 28 29 29 let effects: Vec<u8> = world 30 30 .query::<&MyEffect>() 31 - .iter(&mut world) 31 + .iter(&world) 32 32 .map(|c| c.0) 33 33 .collect(); 34 34 ··· 60 60 61 61 let effects: Vec<u8> = world 62 62 .query::<&MyEffect>() 63 - .iter(&mut world) 63 + .iter(&world) 64 64 .map(|c| c.0) 65 65 .collect(); 66 66 ··· 100 100 101 101 let effects: Vec<u8> = world 102 102 .query::<&MyEffect>() 103 - .iter(&mut world) 103 + .iter(&world) 104 104 .map(|c| c.0) 105 105 .collect(); 106 106