An experimental, status effects-as-entities system for Bevy.
1//! This example shows using [Immediate Stats](https://github.com/AlephCubed/immediate_stats)
2//! to add a decaying movement speed buff.
3//! This means that the strength of the buff decreases throughout its duration.
4//!
5//! This uses [`EffectMode::Merge`], which prevents having multiple of the effect applied at the
6//! same time (no 10x speed multiplier for you).
7//!
8//! There is a second version of this example, which uses Bevy Auto Plugin.
9
10use bevy::prelude::*;
11use bevy_alchemy::*;
12use immediate_stats::*;
13
14fn main() {
15 App::new()
16 .add_plugins((DefaultPlugins, AlchemyPlugin, ImmediateStatsPlugin))
17 .add_plugins(ResetComponentPlugin::<MovementSpeed>::new())
18 .add_systems(Startup, init_scene)
19 .add_systems(Update, (on_space_pressed, apply_speed_boost))
20 .add_systems(PostUpdate, update_ui)
21 .run();
22}
23
24/// Tracks an entities current movement speed.
25#[derive(Component, StatContainer)]
26struct MovementSpeed(Stat);
27
28/// Applies a speed boost, which decreases throughout its duration.
29#[derive(Component, Default, Clone)]
30struct DecayingSpeed {
31 start_speed_boost: Modifier,
32}
33
34/// Spawn a target on startup.
35fn init_scene(mut commands: Commands) {
36 commands.spawn((Name::new("Target"), MovementSpeed(Stat::new(100))));
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 decaying speed to the target.
48fn on_space_pressed(
49 mut commands: Commands,
50 keyboard_input: Res<ButtonInput<KeyCode>>,
51 target: Single<Entity, With<MovementSpeed>>,
52) {
53 if !keyboard_input.just_pressed(KeyCode::Space) {
54 return;
55 }
56
57 commands.entity(*target).with_effect((
58 EffectMode::Insert, // Block having multiple of effect stacked on a single target.
59 Lifetime::from_seconds(2.0), // The duration of the effect.
60 DecayingSpeed {
61 start_speed_boost: Modifier {
62 bonus: 10,
63 multiplier: 2.0,
64 },
65 },
66 ));
67}
68
69/// Applies the effect to the target. Because of how Immediate Stats works, this needs to run every frame.
70fn apply_speed_boost(
71 effects: Query<(&Effecting, &Lifetime, &DecayingSpeed)>,
72 mut targets: Query<&mut MovementSpeed>,
73) {
74 for (target, lifetime, effect) in effects {
75 // Skip if the target doesn't have movement speed.
76 let Ok(mut speed) = targets.get_mut(target.0) else {
77 continue;
78 };
79
80 // Otherwise, apply the buff, scaled by the remaining time.
81 speed.0.apply_scaled(
82 effect.start_speed_boost,
83 lifetime.timer.fraction_remaining(),
84 );
85 }
86}
87
88/// Updates the UI to match the world state.
89fn update_ui(
90 mut ui: Single<&mut Text>,
91 target: Single<&MovementSpeed>,
92 effects: Query<(Entity, &Lifetime, &DecayingSpeed)>,
93) {
94 ui.0 = "Press Space to apply decaying movement speed\n\n".to_string();
95
96 ui.0 += &format!("Speed: {:.1} ({:.1})\n\n", target.0.total(), target.0);
97
98 for (entity, lifetime, speed) in &effects {
99 ui.0 += &format!(
100 "{} - {:.1}s ({:.1})\n",
101 entity,
102 lifetime.timer.remaining_secs(),
103 speed
104 .start_speed_boost
105 .scaled(lifetime.timer.fraction_remaining())
106 );
107 }
108}