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}