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.

Added a `poison_falloff` example that uses `EffectStacks`.

+129 -9
+4
Cargo.toml
··· 42 42 path = "examples/poison.rs" 43 43 44 44 [[example]] 45 + name = "poison_falloff" 46 + path = "examples/poison_falloff.rs" 47 + 48 + [[example]] 45 49 name = "decaying_speed" 46 50 path = "examples/immediate_stats/decaying_speed.rs" 47 51
+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).max(0); 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 + }
+1 -1
src/component/stack.rs
··· 2 2 use bevy_app::{App, Plugin}; 3 3 use bevy_ecs::prelude::ReflectComponent; 4 4 use bevy_ecs::prelude::{Component, Entity, EntityWorldMut}; 5 - use bevy_reflect::prelude::ReflectDefault; 6 5 use bevy_reflect::Reflect; 6 + use bevy_reflect::prelude::ReflectDefault; 7 7 use std::ops::{Add, AddAssign, Deref, DerefMut}; 8 8 9 9 pub(crate) struct StackPlugin;
+1 -1
src/component/timer.rs
··· 1 - use crate::registry::EffectMergeRegistry; 2 1 use crate::ReflectComponent; 2 + use crate::registry::EffectMergeRegistry; 3 3 use bevy_app::{App, Plugin, PreUpdate}; 4 4 use bevy_ecs::component::Mutable; 5 5 use bevy_ecs::prelude::{Commands, Component, Entity, Query, Res};
+1 -1
src/lib.rs
··· 8 8 9 9 use bevy_app::{App, Plugin}; 10 10 use bevy_ecs::prelude::*; 11 - use bevy_reflect::prelude::ReflectDefault; 12 11 use bevy_reflect::Reflect; 12 + use bevy_reflect::prelude::ReflectDefault; 13 13 14 14 pub use bundle::*; 15 15 pub use command::*;
+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