bevy_tnua/control_helpers/
air_actions_tracking.rs

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