bevy_tnua/control_helpers/air_actions_tracking.rs
1#![allow(deprecated)]
2use bevy::prelude::*;
3
4use crate::basis_capabilities::TnuaBasisWithGround;
5use crate::controller::TnuaActionFlowStatus;
6use crate::{TnuaActionDiscriminant, prelude::*};
7
8/// An helper for tracking air actions.
9///
10/// It's [`update`](Self::update) must be called every frame - even when the result is not used -
11/// in the same schedule as [`TnuaControllerPlugin`].
12///
13/// For simpler usage, see [`TnuaSimpleAirActionsCounter`].
14#[derive(Default)]
15#[deprecated = "Use TnuaActionCountingStatus instead"]
16pub struct TnuaAirActionsTracker {
17 considered_in_air: bool,
18}
19
20/// Must be implemented by control schemes that want to use [`TnuaAirActionsTracker`] or
21/// [`TnuaSimpleAirActionsCounter`].
22pub trait TnuaAirActionDefinition: TnuaScheme {
23 /// Whether or not this action is capable of making the character airborne.
24 fn is_air_action(action: Self::ActionDiscriminant) -> bool;
25}
26
27impl TnuaAirActionsTracker {
28 /// Call this every frame, in the same schedule as [`TnuaControllerPlugin`], to track the air
29 /// actions.
30 pub fn update<S>(
31 &mut self,
32 controller: &TnuaController<S>,
33 ) -> TnuaAirActionsUpdate<S::ActionDiscriminant>
34 where
35 S: TnuaScheme + TnuaAirActionDefinition,
36 S::Basis: TnuaBasisWithGround,
37 {
38 match controller.action_flow_status() {
39 TnuaActionFlowStatus::NoAction => self.update_regardless_of_action(controller),
40 TnuaActionFlowStatus::ActionOngoing(action_discriminant) => {
41 if controller
42 .action_discriminant()
43 .is_some_and(S::is_air_action)
44 {
45 if self.considered_in_air {
46 TnuaAirActionsUpdate::NoChange
47 } else {
48 self.considered_in_air = true;
49 TnuaAirActionsUpdate::AirActionStarted(*action_discriminant)
50 }
51 } else {
52 self.update_regardless_of_action(controller)
53 }
54 }
55 TnuaActionFlowStatus::ActionStarted(action_discriminant)
56 | TnuaActionFlowStatus::Cancelled {
57 old: _,
58 new: action_discriminant,
59 } => {
60 if controller
61 .action_discriminant()
62 .is_some_and(S::is_air_action)
63 {
64 self.considered_in_air = true;
65 TnuaAirActionsUpdate::AirActionStarted(*action_discriminant)
66 } else {
67 self.update_regardless_of_action(controller)
68 }
69 }
70 TnuaActionFlowStatus::ActionEnded(_) => {
71 let result = self.update_regardless_of_action(controller);
72 if self.considered_in_air {
73 TnuaAirActionsUpdate::ActionFinishedInAir
74 } else {
75 result
76 }
77 }
78 }
79 }
80
81 fn update_regardless_of_action<S>(
82 &mut self,
83 controller: &TnuaController<S>,
84 ) -> TnuaAirActionsUpdate<S::ActionDiscriminant>
85 where
86 S: TnuaScheme,
87 S::Basis: TnuaBasisWithGround,
88 {
89 if let Ok(basis_access) = controller.basis_access() {
90 if S::Basis::is_airborne(&basis_access) {
91 if self.considered_in_air {
92 TnuaAirActionsUpdate::NoChange
93 } else {
94 self.considered_in_air = true;
95 TnuaAirActionsUpdate::FreeFallStarted
96 }
97 } else if self.considered_in_air {
98 self.considered_in_air = false;
99 TnuaAirActionsUpdate::JustLanded
100 } else {
101 TnuaAirActionsUpdate::NoChange
102 }
103 } else {
104 TnuaAirActionsUpdate::NoChange
105 }
106 }
107}
108
109/// The result of [`TnuaAirActionsTracker::update()`].
110#[derive(Debug, Clone, Copy)]
111pub enum TnuaAirActionsUpdate<D: TnuaActionDiscriminant> {
112 /// Nothing of interest happened this frame.
113 NoChange,
114
115 /// The character has just started a free fall this frame.
116 FreeFallStarted,
117
118 /// The character has just started an air action this frame.
119 AirActionStarted(D),
120
121 /// The character has just finished an air action this frame, and is still in the air.
122 ActionFinishedInAir,
123
124 /// The character has just landed this frame.
125 JustLanded,
126}
127
128/// A simple counter that counts together all the air actions a character is able to perform.
129///
130/// It's [`update`](Self::update) must be called every frame, in the same schedule as
131/// [`TnuaControllerPlugin`].
132#[derive(Component)]
133#[deprecated = "Use TnuaActionsCounter instead"]
134pub struct TnuaSimpleAirActionsCounter<S: TnuaScheme> {
135 tracker: TnuaAirActionsTracker,
136 current_action: Option<(S::ActionDiscriminant, usize)>,
137 air_actions_count: usize,
138}
139
140impl<S: TnuaScheme> Default for TnuaSimpleAirActionsCounter<S> {
141 fn default() -> Self {
142 Self {
143 tracker: Default::default(),
144 current_action: None,
145 air_actions_count: 0,
146 }
147 }
148}
149
150impl<S> TnuaSimpleAirActionsCounter<S>
151where
152 S: TnuaScheme + TnuaAirActionDefinition,
153 S::Basis: TnuaBasisWithGround,
154{
155 /// Call this every frame, in the same schedule as [`TnuaControllerPlugin`], to track the air
156 /// actions.
157 pub fn update(&mut self, controller: &TnuaController<S>)
158 where
159 S: TnuaScheme + TnuaAirActionDefinition,
160 S::Basis: TnuaBasisWithGround,
161 {
162 let update = self.tracker.update(controller);
163 match update {
164 TnuaAirActionsUpdate::NoChange => {}
165 TnuaAirActionsUpdate::FreeFallStarted => {
166 // The free fall is considered the first action
167 self.current_action = None;
168 self.air_actions_count += 1;
169 }
170 TnuaAirActionsUpdate::AirActionStarted(action_discriminant) => {
171 self.current_action = Some((action_discriminant, self.air_actions_count));
172 self.air_actions_count += 1;
173 }
174 TnuaAirActionsUpdate::ActionFinishedInAir => {
175 self.current_action = None;
176 }
177 TnuaAirActionsUpdate::JustLanded => {
178 self.current_action = None;
179 self.air_actions_count = 0;
180 }
181 }
182 }
183
184 /// Resets the air actions counter to a specific count, excluding the current action.
185 ///
186 /// This method allows you to manually set the count of air actions (excluding the current
187 /// action) to a specified value. Use this when you need to synchronize or initialize the air
188 /// actions count to a specific state.
189 ///
190 /// # Arguments
191 ///
192 /// * `count` - The new count to set for air actions, excluding the current action.
193 ///
194 /// # Example
195 ///
196 /// ```
197 /// # use bevy_tnua::control_helpers::{TnuaSimpleAirActionsCounter, TnuaAirActionDefinition};
198 /// # #[derive(bevy_tnua::TnuaScheme)] #[scheme(basis = bevy_tnua::builtins::TnuaBuiltinWalk)] enum ControlScheme {}
199 /// # impl TnuaAirActionDefinition for ControlScheme { fn is_air_action(_: Self::ActionDiscriminant) -> bool { false } }
200 /// # let mut air_actions_counter = TnuaSimpleAirActionsCounter::<ControlScheme>::default();
201 ///
202 /// // Reset the air actions count to 3 (excluding the current action). should also be updated as stated in TnuaAirActionsTracker
203 /// air_actions_counter.reset_count_to(3);
204 /// ```
205 pub fn reset_count_to(&mut self, count: usize) {
206 self.air_actions_count = count;
207 }
208
209 /// Obtain a mutable reference to the air counter.
210 ///
211 /// This can be use to modify the air counter while the player is in the air - for example,
212 /// restoring an air jump when they pick up a floating token.
213 ///
214 /// When it fits the usage, prefer [`reset_count`](Self::reset_count) which is simpler.
215 /// `get_count_mut` should be used for more complex cases, e.g. when the player is allowed
216 /// multiple air jumps, but only one jump gets restored per token.
217 ///
218 /// Note that:
219 ///
220 /// * When the character is grounded, this method returns `None`. This is only for mutating the
221 /// counter while the character is airborne.
222 /// * When the character jumps from the ground, or starts a free fall, the counter is one - not
223 /// zero. Setting the counter to 0 will mean that the next air jump will actually be treated
224 /// as a ground jump - and they'll get another air jump in addition to it. This is usually
225 /// not the desired behavior.
226 /// * Changing the action counter returned by this method will not affect the value
227 /// [`air_count_for`](Self::air_count_for) returns for an action that continues to be fed.
228 pub fn get_count_mut(&mut self) -> Option<&mut usize> {
229 if self.air_actions_count == 0 {
230 None
231 } else {
232 Some(&mut self.air_actions_count)
233 }
234 }
235
236 /// Resets the air actions counter.
237 ///
238 /// This is equivalent to setting the counter to 1 using:
239 ///
240 /// ```no_run
241 /// # use bevy_tnua::control_helpers::{TnuaSimpleAirActionsCounter, TnuaAirActionDefinition};
242 /// # #[derive(bevy_tnua::TnuaScheme)] #[scheme(basis = bevy_tnua::builtins::TnuaBuiltinWalk)] enum ControlScheme {}
243 /// # impl TnuaAirActionDefinition for ControlScheme { fn is_air_action(_: Self::ActionDiscriminant) -> bool { false } }
244 /// # let mut air_actions_counter = TnuaSimpleAirActionsCounter::<ControlScheme>::default();
245 /// if let Some(count) = air_actions_counter.get_count_mut() {
246 /// *count = 1;
247 /// }
248 /// ```
249 ///
250 /// The reason it is set to 1 and not 0 is that when the character jumps from the ground or
251 /// starts a free fall the counter is 1 - and this is what one would usually want to reset to.
252 /// Having a counter of 0 means that the character is grounded - but in that case
253 /// [`get_count_mut`](Self::get_count_mut) will return `None` and the counter will not change.
254 pub fn reset_count(&mut self) {
255 if let Some(count) = self.get_count_mut() {
256 *count = 1;
257 }
258 }
259
260 /// Calculate the "air number" of an action.
261 ///
262 /// The air number of a ground action is 0. The first air jump (double jump) as an air number
263 /// of 1, the second (triple jump) has an air number of 2 and so on. Other air actions (like
264 /// air dashes) are counted together with the jumps.
265 ///
266 /// Use this number to:
267 /// 1. Determine if the action is allowed.
268 /// 2. Optionally change the action's parameters as the air number progresses.
269 ///
270 /// Note that the action discriminant is important, because Tnua relies on constant feed of
271 /// some actions. As long as you pass the correct discriminant, the number will not change
272 /// while the action continues to be fed. The discriminant can be obtained with
273 /// [`TnuaController::action_discriminant`].
274 pub fn air_count_for(&self, action: S::ActionDiscriminant) -> usize {
275 if let Some((current_action, actions)) = self.current_action
276 && current_action == action
277 {
278 return actions;
279 }
280 self.air_actions_count
281 }
282}