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