bevy_tnua/control_helpers/
air_actions_tracking.rs

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