An experimental, status effects-as-entities system for Bevy.
1use crate::ReflectComponent;
2use crate::registry::EffectMergeRegistry;
3use bevy_app::{App, Plugin, PreUpdate};
4use bevy_ecs::component::Mutable;
5use bevy_ecs::prelude::{Commands, Component, Entity, EntityRef, Query, Res};
6use bevy_ecs::schedule::IntoScheduleConfigs;
7use bevy_ecs::world::EntityWorldMut;
8use bevy_reflect::Reflect;
9use bevy_time::{Time, Timer, TimerMode};
10use std::fmt::Debug;
11use std::time::Duration;
12
13pub(crate) struct TimerPlugin;
14
15impl Plugin for TimerPlugin {
16 fn build(&self, app: &mut App) {
17 app.add_systems(PreUpdate, (despawn_finished_lifetimes, tick_delay).chain());
18 app.world_mut()
19 .get_resource_or_init::<EffectMergeRegistry>()
20 .register::<Lifetime>(merge_effect_timer::<Lifetime>)
21 .register::<Delay>(merge_effect_timer::<Delay>);
22 }
23}
24
25/// A [merge function](crate::EffectMergeFn) for [`EffectTimer`] components ([`Lifetime`] and [`Delay`]).
26pub fn merge_effect_timer<T: EffectTimer + Component<Mutability = Mutable>>(
27 mut existing: EntityWorldMut,
28 incoming: EntityRef,
29) {
30 let incoming = incoming.get::<T>().unwrap();
31 existing.get_mut::<T>().unwrap().merge(incoming);
32}
33
34/// A [timer](Timer) which is used for status effects and includes a [`TimerMergeMode`].
35pub trait EffectTimer: Clone {
36 /// Creates a new timer from a duration.
37 fn new(duration: Duration) -> Self;
38
39 /// Creates a new time from a duration, in seconds.
40 fn from_seconds(seconds: f32) -> Self {
41 Self::new(Duration::from_secs_f32(seconds))
42 }
43
44 /// A builder that overwrites the current merge mode with a new value.
45 fn with_mode(self, mode: TimerMergeMode) -> Self;
46
47 /// Returns reference to the internal timer.
48 fn get_timer(&self) -> &Timer;
49
50 /// Returns mutable reference to the internal timer.
51 fn get_timer_mut(&mut self) -> &mut Timer;
52
53 /// Returns reference to the timer's merge mode.
54 fn get_mode(&self) -> &TimerMergeMode;
55
56 /// Returns mutable reference to the timer's merge mode.
57 fn get_mode_mut(&mut self) -> &mut TimerMergeMode;
58
59 /// Merges an old timer (self) with the new one (incoming).
60 /// Behaviour depends on the current [`TimerMergeMode`].
61 fn merge(&mut self, incoming: &Self) {
62 match self.get_mode() {
63 TimerMergeMode::Replace => self.clone_from(incoming),
64 TimerMergeMode::Keep => {}
65 TimerMergeMode::Fraction => {
66 let fraction = self.get_timer().fraction();
67 let duration = incoming.get_timer().duration().as_secs_f32();
68
69 self.clone_from(incoming);
70 self.get_timer_mut()
71 .set_elapsed(Duration::from_secs_f32(fraction * duration));
72 }
73 TimerMergeMode::Max => {
74 let old = self.get_timer().remaining_secs();
75 let new = incoming.get_timer().remaining_secs();
76
77 if new > old {
78 self.clone_from(incoming);
79 }
80 }
81 TimerMergeMode::Sum => {
82 let duration = self.get_timer().duration() + incoming.get_timer().duration();
83 self.clone_from(incoming);
84 self.get_timer_mut().set_duration(duration);
85 }
86 }
87 }
88}
89
90macro_rules! impl_effect_timer {
91 ($ident:ident, $timer_mode:expr) => {
92 impl EffectTimer for $ident {
93 fn new(duration: Duration) -> Self {
94 Self {
95 timer: Timer::new(duration, $timer_mode),
96 ..Self::default()
97 }
98 }
99
100 fn with_mode(mut self, mode: TimerMergeMode) -> Self {
101 self.mode = mode;
102 self
103 }
104
105 fn get_timer(&self) -> &Timer {
106 &self.timer
107 }
108
109 fn get_timer_mut(&mut self) -> &mut Timer {
110 &mut self.timer
111 }
112
113 fn get_mode(&self) -> &TimerMergeMode {
114 &self.mode
115 }
116
117 fn get_mode_mut(&mut self) -> &mut TimerMergeMode {
118 &mut self.mode
119 }
120 }
121 };
122}
123
124/// A timer that despawns the effect when the timer finishes.
125#[doc(alias = "Duration")]
126#[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)]
127#[reflect(Component, PartialEq, Debug, Clone)]
128pub struct Lifetime {
129 /// Tracks the elapsed time. Once the timer is finished, the entity will be despawned.
130 pub timer: Timer,
131 /// Controls the merge behaviour when an effect is [merged](crate::EffectMode::Merge).
132 pub mode: TimerMergeMode,
133}
134
135impl_effect_timer!(Lifetime, TimerMode::Once);
136
137impl Default for Lifetime {
138 fn default() -> Self {
139 Self {
140 timer: Timer::default(),
141 mode: TimerMergeMode::Max,
142 }
143 }
144}
145
146/// A repeating timer used for the delay between effect applications.
147#[derive(Component, Reflect, Eq, PartialEq, Debug, Clone)]
148#[reflect(Component, PartialEq, Debug, Clone)]
149pub struct Delay {
150 /// Tracks the elapsed time.
151 pub timer: Timer,
152 /// Controls the merge behaviour when an effect is [merged](crate::EffectMode::Merge).
153 pub mode: TimerMergeMode,
154}
155
156impl_effect_timer!(Delay, TimerMode::Repeating);
157
158impl Delay {
159 /// Makes the timer [almost finished](Timer::almost_finish), leaving 1ns of remaining time.
160 /// This allows effects to trigger immediately when applied.
161 #[doc(alias = "trigger_on_start", alias = "almost_finish")]
162 pub fn trigger_immediately(mut self) -> Self {
163 self.timer.almost_finish();
164 self
165 }
166}
167
168impl Default for Delay {
169 fn default() -> Self {
170 Self {
171 timer: Timer::default(),
172 mode: TimerMergeMode::Fraction,
173 }
174 }
175}
176
177/// Controls the merge behaviour of a timer when its effect is [merged](crate::EffectMode::Merge).
178#[derive(Reflect, Eq, PartialEq, Debug, Copy, Clone)]
179#[reflect(PartialEq, Debug, Clone)]
180pub enum TimerMergeMode {
181 /// The new effect's time will be used, ignoring the old one.
182 /// Results in same behaviour as [`EffectMode::Insert`](crate::EffectMode::Insert), but on a per-timer basis.
183 Replace,
184 /// The old effect's time will be used, ignoring the new one.
185 Keep,
186 /// The new timer is used, but with the same fraction of the old timer's elapsed time.
187 Fraction,
188 /// The timer with the larger time remaining will be used.
189 Max,
190 /// The timers' durations will be added together.
191 Sum,
192}
193
194pub(super) fn despawn_finished_lifetimes(
195 mut commands: Commands,
196 time: Res<Time>,
197 mut query: Query<(Entity, &mut Lifetime)>,
198) {
199 for (entity, mut lifetime) in &mut query {
200 lifetime.timer.tick(time.delta());
201
202 if lifetime.timer.is_finished() {
203 commands.entity(entity).despawn();
204 }
205 }
206}
207
208pub(super) fn tick_delay(time: Res<Time>, mut query: Query<&mut Delay>) {
209 for mut delay in &mut query {
210 delay.timer.tick(time.delta());
211 }
212}