An experimental, status effects-as-entities system for Bevy.
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.
6
7use bevy::prelude::*;
8use bevy_alchemy::{AlchemyPlugin, Delay, EffectCommandsExt, EffectTimer, Effecting, Lifetime};
9
10fn main() {
11 App::new()
12 .add_plugins((DefaultPlugins, AlchemyPlugin))
13 .add_systems(Startup, init_scene)
14 .add_systems(Update, (on_space_pressed, deal_poison_damage))
15 .add_systems(PostUpdate, update_ui)
16 .run();
17}
18
19#[derive(Component)]
20struct Health(i32);
21
22/// Deals damage over time to the target entity.
23#[derive(Component, Default, Clone)]
24struct Poison {
25 damage: i32,
26}
27
28/// Spawn a target on startup.
29fn init_scene(mut commands: Commands) {
30 commands.spawn((Name::new("Target"), Health(100)));
31 commands.spawn((
32 Node {
33 margin: UiRect::all(Val::Px(10.0)),
34 ..default()
35 },
36 Text::default(),
37 ));
38 commands.spawn(Camera2d);
39}
40
41/// When space is pressed, apply poison to the target.
42fn 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((
52 Lifetime::from_seconds(3.0), // The duration of the effect.
53 Delay::from_seconds(1.0) // The time between damage ticks.
54 .trigger_immediately(), // Make damage tick immediately when the effect is applied.
55 Poison { damage: 1 }, // The amount of damage to apply per tick.
56 ));
57}
58
59/// Runs every frame and deals the poison damage.
60fn deal_poison_damage(
61 effects: Query<(&Effecting, &Delay, &Poison)>,
62 mut targets: Query<&mut Health>,
63) {
64 for (target, delay, poison) in effects {
65 // We wait until the delay finishes to apply the damage.
66 if !delay.timer.is_finished() {
67 continue;
68 }
69
70 // Skip if the target doesn't have health.
71 let Ok(mut health) = targets.get_mut(target.0) else {
72 continue;
73 };
74
75 // Otherwise, deal the damage.
76 health.0 -= poison.damage;
77 }
78}
79
80fn update_ui(
81 mut ui: Single<&mut Text>,
82 target: Single<&Health>,
83 effects: Query<(Entity, &Lifetime, &Delay), With<Poison>>,
84) {
85 ui.0 = "Press Space to apply poison\n\n".to_string();
86
87 ui.0 += &format!("Health: {}\n\n", target.0);
88
89 for (entity, lifetime, delay) in &effects {
90 ui.0 += &format!(
91 "{} - {:.1}s (tick in {:.1}s)\n",
92 entity,
93 lifetime.timer.remaining_secs(),
94 delay.timer.remaining_secs()
95 );
96 }
97}