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}