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.

at dev 113 lines 3.7 kB view raw
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 10use bevy::prelude::*; 11use bevy_alchemy::{ 12 AlchemyPlugin, Delay, EffectCommandsExt, EffectMode, EffectStacks, EffectTimer, Effecting, 13 Lifetime, 14}; 15 16fn 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)] 26struct Health(i32); 27 28/// Deals damage over time to the target entity. 29#[derive(Component, Default, Clone)] 30struct Poison { 31 damage: i32, 32} 33 34/// Spawn a target on startup. 35fn init_scene(mut commands: Commands) { 36 commands.spawn((Name::new("Target"), Health(500))); 37 commands.spawn(( 38 Node { 39 margin: UiRect::all(Val::Px(10.0)), 40 ..default() 41 }, 42 Text::default(), 43 )); 44 commands.spawn(Camera2d); 45} 46 47/// When space is pressed, apply poison to the target. 48fn on_space_pressed( 49 mut commands: Commands, 50 keyboard_input: Res<ButtonInput<KeyCode>>, 51 target: Single<Entity, With<Health>>, 52) { 53 if !keyboard_input.just_pressed(KeyCode::Space) { 54 return; 55 } 56 57 commands.entity(*target).with_effect(( 58 EffectMode::Merge, // Stack tracking requires effect merging. 59 EffectStacks::default(), // Enable stack tracking. 60 Lifetime::from_seconds(3.0), // The duration of the effect. 61 Delay::from_seconds(1.0) // The time between damage ticks. 62 .trigger_immediately(), // Make damage tick immediately when the effect is applied. 63 Poison { damage: 5 }, // The amount of damage to apply per tick. 64 )); 65} 66 67/// Runs every frame and deals the poison damage. 68fn deal_poison_damage( 69 effects: Query<(&Effecting, &EffectStacks, &Delay, &Poison)>, 70 mut targets: Query<&mut Health>, 71) { 72 for (target, stacks, delay, poison) in effects { 73 // We wait until the delay finishes to apply the damage. 74 if !delay.timer.is_finished() { 75 continue; 76 } 77 78 // Skip if the target doesn't have health. 79 let Ok(mut health) = targets.get_mut(target.0) else { 80 continue; 81 }; 82 83 // Otherwise, deal the damage scaled with the number of stacks. 84 // Each subsequent stack has a decreasing effect, the first deals 5 damage, the next 4, then 3, and so on. 85 let stacks = poison.damage.min(stacks.0 as i32); // Clamp stacks to prevent negative damage. 86 let sub = (stacks * (stacks - 1)) / 2; 87 let damage = poison.damage * stacks - sub; 88 89 info!("Dealt {damage} damage!"); 90 91 health.0 -= damage; 92 } 93} 94 95fn update_ui( 96 mut ui: Single<&mut Text>, 97 target: Single<&Health>, 98 effects: Query<(Entity, &EffectStacks, &Lifetime, &Delay), With<Poison>>, 99) { 100 ui.0 = "Press Space to apply poison\n\n".to_string(); 101 102 ui.0 += &format!("Health: {}\n\n", target.0); 103 104 for (entity, stacks, lifetime, delay) in &effects { 105 ui.0 += &format!( 106 "{}, {} stacks - {:.1}s (tick in {:.1}s)\n", 107 entity, 108 stacks.0, 109 lifetime.timer.remaining_secs(), 110 delay.timer.remaining_secs() 111 ); 112 } 113}