bevy_tnua/control_helpers/counted_actions.rs
1use std::marker::PhantomData;
2
3pub use bevy_tnua_macros::TnuaActionSlots;
4
5use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
6use bevy::prelude::*;
7#[cfg(feature = "serialize")]
8use serde::{Deserialize, Serialize};
9
10use crate::basis_capabilities::TnuaBasisWithGround;
11use crate::controller::TnuaActionFlowStatus;
12use crate::{
13 TnuaActionDiscriminant, TnuaBasisAccess, TnuaController, TnuaScheme, TnuaUserControlsSystems,
14};
15
16/// See [the derive macro](bevy_tnua_macros::TnuaActionSlots).
17pub trait TnuaActionSlots: 'static + Send + Sync {
18 /// The scheme who's actions are assigned to the slots.
19 type Scheme: TnuaScheme;
20
21 /// A state where all the slot counters are zeroed.
22 const ZEROES: Self;
23
24 /// Decision what to do when an action starts.
25 fn rule_for(
26 action: <Self::Scheme as TnuaScheme>::ActionDiscriminant,
27 ) -> TnuaActionCountingActionRule;
28
29 /// Get a mutable reference to the counter of the action's slot.
30 ///
31 /// Note that not all actions have slots. For actions not assigned to any slot, this will
32 /// return `None`.
33 fn get_mut(
34 &mut self,
35 action: <Self::Scheme as TnuaScheme>::ActionDiscriminant,
36 ) -> Option<&mut usize>;
37
38 /// Get the value of the counter of the action's slot.
39 ///
40 /// Note that not all actions have slots. For actions not assigned to any slot, this will
41 /// return `None`.
42 fn get(&self, action: <Self::Scheme as TnuaScheme>::ActionDiscriminant) -> Option<usize>;
43}
44
45/// An helper for tracking whether or not the character is in a situation when actions are counted.
46///
47/// This is a low level construct. Prefer using [`TnuaActionsCounter`], which uses this internally.
48#[derive(Default, Debug)]
49#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
50pub enum TnuaActionCountingStatus {
51 CountActions,
52 #[default]
53 ActionsAreFree,
54}
55
56/// The result of [`TnuaActionCountingStatus::update()`].
57#[derive(Debug, Clone, Copy)]
58pub enum TnuaActionCountingUpdate<D: TnuaActionDiscriminant> {
59 /// Nothing of interest happened this update.
60 NoChange,
61
62 /// The character has just started a duration where the counted actions are limited, without
63 /// performing a counted action.
64 ///
65 /// e.g.: for air actions, this could mean stepping off a cliff into a free fall.
66 CountingActivated,
67
68 /// The character has just started a duration where the counted actions are limited by
69 /// performing a counted action.
70 ///
71 /// e.g.: for air actions, this could mean jumping from the ground.
72 CountingActivatedByAction(D),
73
74 /// The character has just started a counted action this frame, when counted actions are
75 /// already limited.
76 ///
77 /// e.g.: for air actions, this could mean doing an air jump.
78 CountedActionStarted(D),
79
80 /// The character has just finished a counted action this frame, but counted actions are still
81 /// limited.
82 ///
83 /// e.g.: for air actions, this could mean finishing a dash while still in the air.
84 ActionFinishedStillCounting,
85
86 /// The duration where the counted actions are limited has ended for the character.
87 ///
88 /// e.g.: for air actions, this could mean the character has landed.
89 CountingEnded,
90}
91
92/// A decision, defined by [`TnuaActionSlots`], regarding an individual action.
93pub enum TnuaActionCountingActionRule {
94 /// This action needs to be counted.
95 ///
96 /// Only return this for actions that are assigned to a slot.
97 Counted,
98 /// This action does not participate in the action counting.
99 Uncounted,
100 /// This action ends the counting, even if otherwise the condition for that is not met.
101 ///
102 /// For example - when counting air actions, performing a wall slide action would reset the
103 /// counters even though the character is not "grounded".
104 EndingCount,
105}
106
107impl TnuaActionCountingStatus {
108 /// Call this every frame, in the same schedule as
109 /// [`TnuaControllerPlugin`](crate::TnuaControllerPlugin), to track the scenario where the
110 /// actions are counted.
111 ///
112 /// The predicates determine what to do based on the state of the current basis and - if an
113 /// action just started - based on that action.
114 ///
115 /// This function both changes the [`TnuaActionCountingStatus`] and returns a
116 /// [`TnuaActionCountingUpdate`] that can be used to decide how to update a more complex type
117 /// (like [`TnuaActionsCounter`]) that does the actual action counting.
118 pub fn update<S: TnuaScheme>(
119 &mut self,
120 controller: &TnuaController<S>,
121 status_for_basis: impl FnOnce(&TnuaBasisAccess<S::Basis>) -> TnuaActionCountingStatus,
122 rule_for_action: impl FnOnce(S::ActionDiscriminant) -> TnuaActionCountingActionRule,
123 ) -> TnuaActionCountingUpdate<S::ActionDiscriminant> {
124 match controller.action_flow_status() {
125 TnuaActionFlowStatus::NoAction => {
126 self.update_based_on_basis(controller, status_for_basis)
127 }
128 TnuaActionFlowStatus::ActionOngoing(action_discriminant) => {
129 match rule_for_action(*action_discriminant) {
130 TnuaActionCountingActionRule::Counted => match self {
131 Self::CountActions => TnuaActionCountingUpdate::NoChange,
132 Self::ActionsAreFree => {
133 *self = Self::CountActions;
134 TnuaActionCountingUpdate::CountingActivatedByAction(
135 *action_discriminant,
136 )
137 }
138 },
139 TnuaActionCountingActionRule::Uncounted => {
140 self.update_based_on_basis(controller, status_for_basis)
141 }
142 TnuaActionCountingActionRule::EndingCount => match self {
143 Self::CountActions => {
144 *self = Self::ActionsAreFree;
145 TnuaActionCountingUpdate::CountingEnded
146 }
147 Self::ActionsAreFree => TnuaActionCountingUpdate::NoChange,
148 },
149 }
150 }
151 TnuaActionFlowStatus::ActionStarted(action_discriminant)
152 | TnuaActionFlowStatus::Cancelled {
153 old: _,
154 new: action_discriminant,
155 } => match rule_for_action(*action_discriminant) {
156 TnuaActionCountingActionRule::Counted => match self {
157 Self::CountActions => {
158 TnuaActionCountingUpdate::CountedActionStarted(*action_discriminant)
159 }
160 Self::ActionsAreFree => {
161 *self = Self::CountActions;
162 TnuaActionCountingUpdate::CountingActivatedByAction(*action_discriminant)
163 }
164 },
165 TnuaActionCountingActionRule::Uncounted => {
166 self.update_based_on_basis(controller, status_for_basis)
167 }
168 TnuaActionCountingActionRule::EndingCount => {
169 *self = Self::ActionsAreFree;
170 TnuaActionCountingUpdate::CountingEnded
171 }
172 },
173 TnuaActionFlowStatus::ActionEnded(_) => {
174 let result = self.update_based_on_basis(controller, status_for_basis);
175 match self {
176 TnuaActionCountingStatus::CountActions => {
177 TnuaActionCountingUpdate::ActionFinishedStillCounting
178 }
179 TnuaActionCountingStatus::ActionsAreFree => result,
180 }
181 }
182 }
183 }
184
185 fn update_based_on_basis<S: TnuaScheme>(
186 &mut self,
187 controller: &TnuaController<S>,
188 status_for_basis: impl FnOnce(&TnuaBasisAccess<S::Basis>) -> TnuaActionCountingStatus,
189 ) -> TnuaActionCountingUpdate<S::ActionDiscriminant> {
190 let Ok(basis_access) = controller.basis_access() else {
191 return TnuaActionCountingUpdate::NoChange;
192 };
193 match (&self, status_for_basis(&basis_access)) {
194 (Self::CountActions, Self::CountActions) => TnuaActionCountingUpdate::NoChange,
195 (Self::CountActions, Self::ActionsAreFree) => {
196 *self = Self::ActionsAreFree;
197 TnuaActionCountingUpdate::CountingEnded
198 }
199 (Self::ActionsAreFree, Self::CountActions) => {
200 *self = Self::CountActions;
201 TnuaActionCountingUpdate::CountingActivated
202 }
203 (Self::ActionsAreFree, Self::ActionsAreFree) => TnuaActionCountingUpdate::NoChange,
204 }
205 }
206}
207
208/// An helper for counting the actions in scenarios where actions can only be done a limited amount
209/// of times. Mainly used for implementing air actions.
210///
211/// It's [`update`](Self::update) must be called every frame - even when the result is not used -
212/// in the same schedule as [`TnuaControllerPlugin`](crate::TnuaControllerPlugin). For air actions,
213/// this can usually be done with [`TnuaAirActionsPlugin`].
214///
215/// This type exposes the slots struct to allow manual interference with the counting, but the
216/// actually checking of counters should use [`count_for`](Self::count_for) which also takes into
217/// account the currently active action.
218#[derive(Component)]
219#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
220pub struct TnuaActionsCounter<S: TnuaActionSlots> {
221 counting_status: TnuaActionCountingStatus,
222 #[cfg_attr(
223 feature = "serialize",
224 serde(bound(
225 serialize = "<S::Scheme as TnuaScheme>::ActionDiscriminant: Serialize",
226 deserialize = "<S::Scheme as TnuaScheme>::ActionDiscriminant: Deserialize<'de>",
227 ))
228 )]
229 current_action: Option<(<S::Scheme as TnuaScheme>::ActionDiscriminant, usize)>,
230 pub slots: S,
231}
232
233impl<S: TnuaActionSlots> Default for TnuaActionsCounter<S> {
234 fn default() -> Self {
235 Self {
236 counting_status: Default::default(),
237 current_action: None,
238 slots: S::ZEROES,
239 }
240 }
241}
242
243impl<S: TnuaActionSlots> TnuaActionsCounter<S> {
244 /// Call this every frame, at the schedule of
245 /// [`TnuaControllerPlugin`](crate::TnuaControllerPlugin), to track the actions.
246 ///
247 /// The predicate and the [`TnuaActionSlots`] from the generic parameter define how the
248 /// counters will get updated.
249 pub fn update(
250 &mut self,
251 controller: &TnuaController<S::Scheme>,
252 status_for_basis: impl FnOnce(
253 &TnuaBasisAccess<<S::Scheme as TnuaScheme>::Basis>,
254 ) -> TnuaActionCountingStatus,
255 ) {
256 let update = self
257 .counting_status
258 .update(controller, status_for_basis, S::rule_for);
259
260 match update {
261 TnuaActionCountingUpdate::NoChange => {}
262 TnuaActionCountingUpdate::CountingActivated => {
263 self.current_action = None;
264 // No need to reset the slots - we can assume they are already at default
265 }
266 // TODO: should these two have different meaning?
267 TnuaActionCountingUpdate::CountingActivatedByAction(action_discriminant) => {
268 let slot = self
269 .slots
270 .get_mut(action_discriminant)
271 .expect("Should only get CountingActivatedByAction for air actions");
272 self.current_action = Some((action_discriminant, *slot));
273 }
274 TnuaActionCountingUpdate::CountedActionStarted(action_discriminant) => {
275 let slot = self
276 .slots
277 .get_mut(action_discriminant)
278 .expect("Should only get CountedActionStarted for air actions");
279 *slot += 1;
280 self.current_action = Some((action_discriminant, *slot));
281 }
282 TnuaActionCountingUpdate::ActionFinishedStillCounting => {
283 self.current_action = None;
284 }
285 TnuaActionCountingUpdate::CountingEnded => {
286 self.current_action = None;
287 self.slots = S::ZEROES;
288 }
289 }
290 }
291
292 /// Calculate the "number" of an action.
293 ///
294 /// If actions are not currently being counted, this will return 0. Otherwise, it will return
295 /// the number the requested action will be - meaning the first one in the counting duration
296 /// will be numbered 1.
297 ///
298 /// If the specified action is currently running, this method will return the number of the
299 /// currently running action, not the next action of the same variant. This is done so that
300 /// user control systems will keep feeding it - with `allow_in_air: true` - for as long as the
301 /// player holds the button. Note that this means that while the very action that triggered the
302 /// counting (e.g. - jumping off the ground when counting air actions) is still active, its
303 /// number will be 0 (even though action counting starts from 1, this action was from before
304 /// the counting so it gets to be 0)
305 ///
306 /// Each slot gets counted separately. If the action does not belong to any slot, or if actions
307 /// are not currently being counted, this returns 0.
308 ///
309 /// ```no_run
310 /// # use bevy_tnua::prelude::*;
311 /// # use bevy_tnua::control_helpers::{TnuaActionSlots, TnuaActionsCounter};
312 /// # #[derive(TnuaScheme)] #[scheme(basis = TnuaBuiltinWalk)] enum ControlScheme {Jump(TnuaBuiltinJump)}
313 /// # #[derive(TnuaActionSlots)] #[slots(scheme = ControlScheme)] struct AirActionSlots {#[slots(Jump)] jump: usize}
314 /// # let mut controller = TnuaController::<ControlScheme>::default();
315 /// let air_actions: TnuaActionsCounter<AirActionSlots>; // actually get this from a Query
316 ///
317 /// # air_actions = Default::default();
318 /// controller.action(ControlScheme::Jump(TnuaBuiltinJump {
319 /// allow_in_air: air_actions.count_for(ControlSchemeActionDiscriminant::Jump)
320 /// // Allow one air jump - use <= instead of < because the first one in the air will
321 /// // be have its `count_for` return 1.
322 /// <= 1,
323 /// ..Default::default()
324 /// }));
325 /// ```
326 pub fn count_for(&self, action: <S::Scheme as TnuaScheme>::ActionDiscriminant) -> usize {
327 if let Some((current_action, actions)) = self.current_action
328 && current_action == action
329 {
330 return actions;
331 }
332 let Some(slot_value) = self.slots.get(action) else {
333 return 0; // non-counted action
334 };
335 match self.counting_status {
336 TnuaActionCountingStatus::CountActions => slot_value + 1,
337 TnuaActionCountingStatus::ActionsAreFree => slot_value,
338 }
339 }
340}
341
342/// Use the action slots definition to track air actions.
343///
344/// Must use the same schedule as the [`TnuaControllerPlugin`](crate::TnuaControllerPlugin).
345///
346/// Note that this will automatically make [`TnuaActionsCounter<S>`] a dependency component of the
347/// [`TnuaController`] parametrized to `S`'s [`Scheme`](TnuaActionSlots::Scheme).
348pub struct TnuaAirActionsPlugin<S: TnuaActionSlots> {
349 schedule: InternedScheduleLabel,
350 _phantom: PhantomData<S>,
351}
352
353impl<S: TnuaActionSlots> TnuaAirActionsPlugin<S> {
354 pub fn new(schedule: impl ScheduleLabel) -> Self {
355 Self {
356 schedule: schedule.intern(),
357 _phantom: PhantomData,
358 }
359 }
360}
361
362impl<S: TnuaActionSlots> Plugin for TnuaAirActionsPlugin<S>
363where
364 <S::Scheme as TnuaScheme>::Basis: TnuaBasisWithGround,
365{
366 fn build(&self, app: &mut App) {
367 app.register_required_components::<TnuaController<S::Scheme>, TnuaActionsCounter<S>>();
368 app.add_systems(
369 self.schedule,
370 actions_counter_update_system::<S>.in_set(TnuaUserControlsSystems),
371 );
372 }
373}
374
375fn actions_counter_update_system<S: TnuaActionSlots>(
376 mut query: Query<(&mut TnuaActionsCounter<S>, &TnuaController<S::Scheme>)>,
377) where
378 <S::Scheme as TnuaScheme>::Basis: TnuaBasisWithGround,
379{
380 for (mut counter, controller) in query.iter_mut() {
381 counter.update(controller, |basis| {
382 if <<S::Scheme as TnuaScheme>::Basis as TnuaBasisWithGround>::is_airborne(basis) {
383 TnuaActionCountingStatus::CountActions
384 } else {
385 TnuaActionCountingStatus::ActionsAreFree
386 }
387 });
388 }
389}