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}