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}