bevy_tnua/
controller.rs

1use std::marker::PhantomData;
2
3use crate::TnuaSensorsEntities;
4use crate::basis_capabilities::TnuaBasisWithGround;
5use crate::ghost_overrides::TnuaGhostOverwrites;
6use crate::{
7    TnuaActionInitiationDirective, TnuaActionLifecycleDirective, TnuaActionLifecycleStatus, math::*,
8};
9use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
10use bevy::prelude::*;
11use bevy::time::Stopwatch;
12use bevy_tnua_physics_integration_layer::data_for_backends::TnuaGhostSensor;
13#[cfg(feature = "serialize")]
14use serde::{Deserialize, Serialize};
15
16use crate::basis_action_traits::{
17    TnuaActionContext, TnuaActionDiscriminant, TnuaActionState, TnuaBasis, TnuaBasisAccess,
18    TnuaScheme, TnuaSchemeConfig, TnuaUpdateInActionStateResult,
19};
20use crate::{
21    TnuaBasisContext, TnuaMotor, TnuaPipelineSystems, TnuaProximitySensor, TnuaRigidBodyTracker,
22    TnuaSystems, TnuaToggle, TnuaUserControlsSystems,
23};
24
25pub struct TnuaControllerPlugin<S: TnuaScheme> {
26    schedule: InternedScheduleLabel,
27    _phantom: PhantomData<S>,
28}
29
30/// The main for supporting Tnua character controller.
31///
32/// Will not work without a physics backend plugin (like `TnuaRapier2dPlugin` or
33/// `TnuaRapier3dPlugin`)
34///
35/// Make sure the schedule for this plugin, the physics backend plugin, and the physics backend
36/// itself are all using the same timestep. This usually means that the physics backend is in e.g.
37/// `FixedPostUpdate` and the Tnua plugins are at `PostUpdate`.
38///
39/// **DO NOT mix `Update` with `FixedUpdate`!** This will mess up Tnua's calculations, resulting in
40/// very unstable character motion.
41impl<S: TnuaScheme> TnuaControllerPlugin<S> {
42    pub fn new(schedule: impl ScheduleLabel) -> Self {
43        Self {
44            schedule: schedule.intern(),
45            _phantom: PhantomData,
46        }
47    }
48}
49
50impl<S: TnuaScheme> Plugin for TnuaControllerPlugin<S> {
51    fn build(&self, app: &mut App) {
52        app.init_asset::<S::Config>();
53        app.configure_sets(
54            self.schedule,
55            (
56                TnuaPipelineSystems::Sensors,
57                TnuaUserControlsSystems,
58                TnuaPipelineSystems::Logic,
59                TnuaPipelineSystems::Motors,
60            )
61                .chain()
62                .in_set(TnuaSystems),
63        );
64        app.register_required_components_with::<TnuaController<S>, _>(
65            || TnuaSensorsEntities::<S> {
66                sensors_entities: Default::default(),
67            },
68        );
69        app.add_systems(
70            self.schedule,
71            (apply_ghost_overwrites::<S>, apply_controller_system::<S>)
72                .chain()
73                .in_set(TnuaPipelineSystems::Logic),
74        );
75    }
76}
77
78#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
79struct ContenderAction<S: TnuaScheme> {
80    action: S,
81    being_fed_for: Stopwatch,
82}
83
84#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
85#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
86enum FedStatus {
87    #[default]
88    Not,
89    Lingering,
90    Fresh,
91    Trigger,
92    Interrupt,
93    Held,
94}
95
96impl FedStatus {
97    fn considered_fed(&self) -> bool {
98        match self {
99            FedStatus::Not => false,
100            FedStatus::Lingering => true,
101            FedStatus::Fresh => true,
102            FedStatus::Trigger => true,
103            FedStatus::Interrupt => true,
104            FedStatus::Held => true,
105        }
106    }
107}
108
109#[derive(Default, Debug)]
110#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
111struct FedEntry {
112    status: FedStatus,
113    rescheduled_in: Option<Timer>,
114}
115
116/// Configuration for a [`TnuaController`].
117#[derive(Component)]
118pub struct TnuaConfig<S: TnuaScheme>(pub Handle<S::Config>);
119
120/// The main component used for interaction with the controls and animation code (needs a
121/// [`TnuaConfig`])
122///
123/// Every frame, the game code should invoke
124/// [`initiate_action_feeding`](Self::initiate_action_feeding) and then feed input this component
125/// on every controlled entity. What should be fed is:
126///
127/// * A basis - this is the main movement command - usually
128///   [`TnuaBuiltinWalk`](crate::builtins::TnuaBuiltinWalk), but there can be others. The
129///   controller's basis is takens from the scheme (the generic argument). Controlling it is done
130///   by modifying the [`basis`](Self::basis) field of the controller.
131///
132///   Refer to the documentation of [the implementors of
133///   `TnuaBasis`](crate::TnuaBasis#implementors) for more information.
134///
135/// * Zero or more actions - these are movements like jumping, dashing, crouching, etc. Multiple
136///   actions can be fed, but only one can be active at any given moment. Unlike basis, there is a
137///   smart mechanism for deciding which action to use and which to discard, so it is safe to feed
138///   many actions at the same frame. Actions are also defined in the scheme, and can be fed using
139///   the [`action`](Self::action) method.
140///
141///   Refer to the documentation of [the implementors of
142///   `TnuaAction`](crate::TnuaAction#implementors) for more information.
143///
144/// Without [`TnuaControllerPlugin`] of the same scheme this component will not do anything.
145#[derive(Component)]
146#[require(TnuaMotor, TnuaRigidBodyTracker)]
147#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
148pub struct TnuaController<S: TnuaScheme> {
149    /// Input for the basis - the main movement command.
150    pub basis: S::Basis,
151    #[cfg_attr(
152        feature = "serialize",
153        serde(bound(
154            serialize = "<S::Basis as TnuaBasis>::Memory: Serialize",
155            deserialize = "<S::Basis as TnuaBasis>::Memory: Deserialize<'de>",
156        ))
157    )]
158    /// A copy of the basis' configuration from the asset.
159    pub basis_config: Option<<S::Basis as TnuaBasis>::Config>,
160    /// Kept by the basis itself - but user code may modify if it knows what it`s doing.
161    pub basis_memory: <S::Basis as TnuaBasis>::Memory,
162    // TODO: If ever possible, make this a fixed size array:
163    actions_being_fed: Vec<FedEntry>,
164    contender_action: Option<ContenderAction<S>>,
165    #[cfg_attr(
166        feature = "serialize",
167        serde(bound(
168            serialize = "S::ActionDiscriminant: Serialize",
169            deserialize = "S::ActionDiscriminant: Deserialize<'de>",
170        ))
171    )]
172    action_flow_status: TnuaActionFlowStatus<S::ActionDiscriminant>,
173    up_direction: Option<Dir3>,
174    action_feeding_initiated: bool,
175    #[cfg_attr(
176        feature = "serialize",
177        serde(bound(
178            serialize = "S::ActionState: Serialize",
179            deserialize = "S::ActionState: Deserialize<'de>",
180        ))
181    )]
182    /// The full state of the currently running action.
183    ///
184    /// Be careful when touching that:
185    /// * Changing the `input` and even `config` is usually fine.
186    /// * Only change the `state` if you know what you're doing.
187    /// * Never change the actual variant.
188    pub current_action: Option<S::ActionState>,
189}
190
191impl<S: TnuaScheme> Default for TnuaController<S> {
192    fn default() -> Self {
193        Self {
194            basis: Default::default(),
195            basis_config: None,
196            basis_memory: Default::default(),
197            actions_being_fed: (0..S::ActionDiscriminant::NUM_FEED_STATUS_SLOTS)
198                .map(|_| Default::default())
199                .collect(),
200            contender_action: None,
201            action_flow_status: TnuaActionFlowStatus::NoAction,
202            up_direction: None,
203            action_feeding_initiated: false,
204            current_action: None,
205        }
206    }
207}
208
209/// The result of [`TnuaController::action_flow_status()`].
210#[derive(Debug, Clone, Default)]
211#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
212pub enum TnuaActionFlowStatus<D: TnuaActionDiscriminant> {
213    /// No action is going on.
214    #[default]
215    NoAction,
216
217    /// An action just started.
218    ActionStarted(D),
219
220    /// An action was fed in a past frame and is still ongoing.
221    ActionOngoing(D),
222
223    /// An action has stopped being fed.
224    ///
225    /// Note that the action may still have a termination sequence after this happens.
226    ActionEnded(D),
227
228    /// An action has just been canceled into another action.
229    Cancelled { old: D, new: D },
230}
231
232impl<D: TnuaActionDiscriminant> TnuaActionFlowStatus<D> {
233    /// The discriminant of the ongoing action, if there is an ongoing action.
234    ///
235    /// Will also return a value if the action has just started.
236    pub fn ongoing(&self) -> Option<D> {
237        match self {
238            TnuaActionFlowStatus::NoAction | TnuaActionFlowStatus::ActionEnded(_) => None,
239            TnuaActionFlowStatus::ActionStarted(discriminant)
240            | TnuaActionFlowStatus::ActionOngoing(discriminant)
241            | TnuaActionFlowStatus::Cancelled {
242                old: _,
243                new: discriminant,
244            } => Some(*discriminant),
245        }
246    }
247
248    /// The discriminant of the action that has just started this frame.
249    ///
250    /// Will return `None` if there is no action, or if the ongoing action has started in a past
251    /// frame.
252    pub fn just_starting(&self) -> Option<D> {
253        match self {
254            TnuaActionFlowStatus::NoAction
255            | TnuaActionFlowStatus::ActionOngoing(_)
256            | TnuaActionFlowStatus::ActionEnded(_) => None,
257            TnuaActionFlowStatus::ActionStarted(discriminant)
258            | TnuaActionFlowStatus::Cancelled {
259                old: _,
260                new: discriminant,
261            } => Some(*discriminant),
262        }
263    }
264}
265
266impl<S: TnuaScheme> TnuaController<S> {
267    /// Access to the entire basis state. This is what the [basis
268    /// capabilities](crate::basis_capabilities) usually use.
269    pub fn basis_access(
270        &'_ self,
271    ) -> Result<TnuaBasisAccess<'_, S::Basis>, TnuaControllerHasNotPulledConfiguration> {
272        Ok(TnuaBasisAccess {
273            input: &self.basis,
274            config: self
275                .basis_config
276                .as_ref()
277                .ok_or(TnuaControllerHasNotPulledConfiguration)?,
278            memory: &self.basis_memory,
279        })
280    }
281
282    /// **Must** be called each frame before feeding the actions (unless [`action`](Self::action))
283    /// is never used)
284    pub fn initiate_action_feeding(&mut self) {
285        self.action_feeding_initiated = true;
286    }
287
288    /// Feed an action.
289    ///
290    /// This is used in pull fashion - a system (or set of systems) that checks the state of the
291    /// playter input (or of some NPC controller) and can feed the action **every frame** its still
292    /// on. This system must start by calling
293    /// [`initiate_action_feeding`](Self::initiate_action_feeding).
294    ///
295    /// For pushing actions, use one of the other `action_*` methods.
296    pub fn action(&mut self, action: S) {
297        assert!(
298            self.action_feeding_initiated,
299            "Feeding action without invoking `initiate_action_feeding()`"
300        );
301        let fed_entry = &mut self.actions_being_fed[action.discriminant().feed_status_slot()];
302
303        match fed_entry.status {
304            FedStatus::Lingering
305            | FedStatus::Fresh
306            | FedStatus::Trigger
307            | FedStatus::Interrupt
308            | FedStatus::Held => {
309                fed_entry.status = FedStatus::Fresh;
310                if let Some(current_action) = self.current_action.as_mut() {
311                    match action.update_in_action_state(current_action) {
312                        TnuaUpdateInActionStateResult::Success => {
313                            // Do nothing farther
314                        }
315                        TnuaUpdateInActionStateResult::WrongVariant(_) => {
316                            // different action is running - will not override because button was
317                            // already pressed.
318                        }
319                    }
320                } else if self.contender_action.is_none()
321                    && fed_entry
322                        .rescheduled_in
323                        .as_ref()
324                        .is_some_and(|timer| timer.is_finished())
325                {
326                    // no action is running - but this action is rescheduled and there is no
327                    // already-existing contender that would have taken priority
328                    self.contender_action = Some(ContenderAction {
329                        action,
330                        being_fed_for: Stopwatch::new(),
331                    });
332                } else {
333                    // no action is running - will not set because button was already pressed.
334                }
335            }
336            FedStatus::Not => {
337                *fed_entry = FedEntry {
338                    status: FedStatus::Fresh,
339                    rescheduled_in: None,
340                };
341                if let Some(contender_action) = self.contender_action.as_mut()
342                    && action.discriminant() == contender_action.action.discriminant()
343                {
344                    contender_action.action = action;
345                } else if let Some(contender_action) = self.contender_action.as_ref()
346                    && self.actions_being_fed
347                        [contender_action.action.discriminant().feed_status_slot()]
348                    .status
349                        == FedStatus::Interrupt
350                {
351                    // If the existing condender is an interrupt, we will not overwrite it.
352                } else {
353                    self.contender_action = Some(ContenderAction {
354                        action,
355                        being_fed_for: Stopwatch::new(),
356                    });
357                }
358            }
359        }
360    }
361
362    /// Pretend to feed an action.
363    ///
364    /// This makes Tnua think the action is being fed without actually causing it to happen. Useful
365    /// for when the player press the button but the action cannot be sent (e.g. - wall jumping
366    /// button, but character is not next to a wall) - feeding a fake action will prevent Tnua from
367    /// triggering the action when it can, eventually, be sent - but the player did not release the
368    /// button so it shouldn't be invoked.
369    pub fn action_fake(&mut self, action: S::ActionDiscriminant) {
370        let fed_entry = &mut self.actions_being_fed[action.feed_status_slot()];
371        match fed_entry.status {
372            FedStatus::Not => {
373                *fed_entry = FedEntry {
374                    status: FedStatus::Fresh,
375                    rescheduled_in: None,
376                };
377            }
378            FedStatus::Lingering
379            | FedStatus::Fresh
380            | FedStatus::Trigger
381            | FedStatus::Interrupt
382            | FedStatus::Held => fed_entry.status = FedStatus::Fresh,
383        }
384    }
385
386    /// Trigger an action in a push fashion. The action must be one that handles its own duration.
387    ///
388    /// This means that actions like [`TnuaBuiltinCrouch`](crate::builtins::TnuaBuiltinCrouch)
389    /// cannot be used with this method - the character will rise after one frame of starting to
390    /// crouch. Actions like ['TnuaBuiltinDash`](crate::builtins::TnuaBuiltinDash) are okay because
391    /// the dash will only end when the motion itself is finished.
392    pub fn action_trigger(&mut self, action: S) {
393        let fed_entry = &mut self.actions_being_fed[action.discriminant().feed_status_slot()];
394
395        match fed_entry.status {
396            FedStatus::Lingering
397            | FedStatus::Fresh
398            | FedStatus::Trigger
399            | FedStatus::Interrupt
400            | FedStatus::Held => {
401                // Do nothing because the action was already triggered
402            }
403            FedStatus::Not => {
404                *fed_entry = FedEntry {
405                    status: FedStatus::Trigger,
406                    rescheduled_in: None,
407                };
408                if let Some(contender_action) = self.contender_action.as_mut()
409                    && action.discriminant() == contender_action.action.discriminant()
410                {
411                    contender_action.action = action;
412                } else if let Some(contender_action) = self.contender_action.as_ref()
413                    && self.actions_being_fed
414                        [contender_action.action.discriminant().feed_status_slot()]
415                    .status
416                        == FedStatus::Interrupt
417                {
418                    // If the existing condender is an interrupt, we will not overwrite it.
419                } else {
420                    self.contender_action = Some(ContenderAction {
421                        action,
422                        being_fed_for: Stopwatch::new(),
423                    });
424                }
425            }
426        }
427    }
428
429    /// Similar to [`action_trigger`](Self::action_trigger), but can override other action
430    /// contenders and other action feeding methods cannot override it.
431    pub fn action_interrupt(&mut self, action: S) {
432        // Because this is an interrupt, we ignore the old fed status - but we still care not to
433        // set the contender if we are the current action.
434        self.actions_being_fed[action.discriminant().feed_status_slot()] = FedEntry {
435            status: FedStatus::Interrupt,
436            rescheduled_in: None,
437        };
438
439        let action = if let Some(current_action) = self.current_action.as_mut() {
440            match action.update_in_action_state(current_action) {
441                TnuaUpdateInActionStateResult::Success => {
442                    return;
443                }
444                TnuaUpdateInActionStateResult::WrongVariant(action) => {
445                    // different action is running - we'll have to
446                    action
447                }
448            }
449        } else {
450            action
451        };
452        // Overwrite the condender action even if there already was a contender action.
453        self.contender_action = Some(ContenderAction {
454            action,
455            being_fed_for: Stopwatch::new(),
456        });
457    }
458
459    /// Trigger an action in a push fashion. The action will continue until
460    /// [`action_end`](Self::action_end) is called - or until the motion itself is finished.
461    pub fn action_start(&mut self, action: S) {
462        let fed_entry = &mut self.actions_being_fed[action.discriminant().feed_status_slot()];
463
464        match fed_entry.status {
465            FedStatus::Lingering | FedStatus::Fresh | FedStatus::Trigger | FedStatus::Interrupt => {
466                // Do nothing because the action was already triggered
467            }
468            FedStatus::Held => {
469                // Action was already started - but still allowed to change its parameters
470                let action = if let Some(current_action) = self.current_action.as_mut() {
471                    match action.update_in_action_state(current_action) {
472                        TnuaUpdateInActionStateResult::Success => {
473                            // Managed to update current_action - no need to check the contender_action
474                            return;
475                        }
476                        TnuaUpdateInActionStateResult::WrongVariant(action) => action,
477                    }
478                } else {
479                    action
480                };
481                if let Some(contender_action) = self.contender_action.as_mut()
482                    && action.discriminant() == contender_action.action.discriminant()
483                {
484                    // This action is still condender - we can safely update it
485                    contender_action.action = action;
486                }
487            }
488            FedStatus::Not => {
489                *fed_entry = FedEntry {
490                    status: FedStatus::Held,
491                    rescheduled_in: None,
492                };
493                if let Some(contender_action) = self.contender_action.as_mut()
494                    && action.discriminant() == contender_action.action.discriminant()
495                {
496                    contender_action.action = action;
497                } else if let Some(contender_action) = self.contender_action.as_ref()
498                    && self.actions_being_fed
499                        [contender_action.action.discriminant().feed_status_slot()]
500                    .status
501                        == FedStatus::Interrupt
502                {
503                    // If the existing condender is an interrupt, we will not overwrite it.
504                } else {
505                    self.contender_action = Some(ContenderAction {
506                        action,
507                        being_fed_for: Stopwatch::new(),
508                    });
509                }
510            }
511        }
512    }
513
514    /// End an action that started with [`action_start`](Self::action_start)
515    pub fn action_end(&mut self, action: S::ActionDiscriminant) {
516        // Note that even if the action was an interrupt -this is a direct order to end it.
517        self.actions_being_fed[action.feed_status_slot()] = Default::default();
518    }
519
520    /// Re-feed the same action that is currently active.
521    ///
522    /// This is useful when matching on [`current_action`](Self::current_action) and wanting to
523    /// continue feeding the **exact same** action with the **exact same** input without having to
524    pub fn prolong_action(&mut self) {
525        if let Some(current_action) = self.action_discriminant() {
526            self.actions_being_fed[current_action.feed_status_slot()].status = FedStatus::Fresh;
527        }
528    }
529
530    /// The discriminant of the currently running action.
531    pub fn action_discriminant(&self) -> Option<S::ActionDiscriminant> {
532        Some(self.current_action.as_ref()?.discriminant())
533    }
534
535    /// Indicator for the state and flow of movement actions.
536    ///
537    /// Query this every frame to keep track of the actions. For air actions,
538    /// [`TnuaAirActionsTracker`](crate::control_helpers::TnuaAirActionsTracker) is easier to use
539    /// (and uses this behind the scenes)
540    ///
541    /// The benefits of this over querying [`action_discriminant`](Self::action_discriminant) every
542    /// frame are:
543    ///
544    /// * `action_flow_status` can indicate when the same action has been fed again immediately
545    ///   after stopping or cancelled into itself.
546    /// * `action_flow_status` shows an [`ActionEnded`](TnuaActionFlowStatus::ActionEnded) when the
547    ///   action is no longer fed, even if the action is still active (termination sequence)
548    pub fn action_flow_status(&self) -> &TnuaActionFlowStatus<S::ActionDiscriminant> {
549        &self.action_flow_status
550    }
551
552    /// Returns the direction considered as up.
553    ///
554    /// Note that the up direction is based on gravity, as reported by
555    /// [`TnuaRigidBodyTracker::gravity`], and that it'd typically be one frame behind since it
556    /// gets updated in the same system that applies the controller logic. If this is unacceptable,
557    /// consider using [`TnuaRigidBodyTracker::gravity`] directly or deducing the up direction via
558    /// different means.
559    pub fn up_direction(&self) -> Option<Dir3> {
560        self.up_direction
561    }
562}
563
564impl<S: TnuaScheme> TnuaController<S>
565where
566    S::Basis: TnuaBasisWithGround,
567{
568    /// Checks if the character is currently airborne.
569    ///
570    /// The check is done based on the basis, and is equivalent to getting the controller's
571    /// [`basis_access`](Self::basis_access) and using [`TnuaBasisWithGround::is_airborne`] on it.
572    pub fn is_airborne(&self) -> Result<bool, TnuaControllerHasNotPulledConfiguration> {
573        Ok(S::Basis::is_airborne(&self.basis_access()?))
574    }
575}
576
577#[derive(thiserror::Error, Debug)]
578#[error("The Tnua controller did not pull the configuration asset yet")]
579pub struct TnuaControllerHasNotPulledConfiguration;
580
581fn apply_ghost_overwrites<S: TnuaScheme>(
582    mut query: Query<(&TnuaSensorsEntities<S>, &mut TnuaGhostOverwrites<S>)>,
583    mut proximity_sensors_query: Query<(&mut TnuaProximitySensor, &TnuaGhostSensor)>,
584) {
585    for (sensors_entities, mut ghost_overwrites) in query.iter_mut() {
586        for (ghost_overwrite, sensor_entity) in
587            S::Basis::ghost_sensor_overwrites(ghost_overwrites.as_mut().as_mut(), sensors_entities)
588        {
589            let Ok((mut proximity_sensor, ghost_sensor)) =
590                proximity_sensors_query.get_mut(sensor_entity)
591            else {
592                continue;
593            };
594            if let Some(ghost_output) = ghost_overwrite.find_in(&ghost_sensor.0) {
595                proximity_sensor.output = Some(ghost_output.clone());
596            } else {
597                ghost_overwrite.set(None);
598            }
599        }
600    }
601}
602
603#[allow(clippy::type_complexity)]
604fn apply_controller_system<S: TnuaScheme>(
605    time: Res<Time>,
606    mut query: Query<(
607        Entity,
608        &mut TnuaController<S>,
609        &TnuaConfig<S>,
610        &mut TnuaSensorsEntities<S>,
611        &TnuaRigidBodyTracker,
612        &mut TnuaMotor,
613        Option<&TnuaToggle>,
614        Has<TnuaGhostOverwrites<S>>,
615    )>,
616    proximity_sensors_query: Query<(&TnuaProximitySensor, Has<TnuaGhostSensor>)>,
617    config_assets: Res<Assets<S::Config>>,
618    mut commands: Commands,
619) {
620    let frame_duration = time.delta().as_secs_f64() as Float;
621    if frame_duration == 0.0 {
622        return;
623    }
624    for (
625        controller_entity,
626        mut controller,
627        config_handle,
628        mut sensors_entities,
629        tracker,
630        mut motor,
631        tnua_toggle,
632        has_ghost_overwrites,
633    ) in query.iter_mut()
634    {
635        match tnua_toggle.copied().unwrap_or_default() {
636            TnuaToggle::Disabled => continue,
637            TnuaToggle::SenseOnly => {}
638            TnuaToggle::Enabled => {}
639        }
640        let controller = controller.as_mut();
641
642        let Some(config) = config_assets.get(&config_handle.0) else {
643            continue;
644        };
645        controller.basis_config = Some({
646            let mut basis_config = config.basis_config().clone();
647            if let Some(current_action) = controller.current_action.as_ref() {
648                current_action.modify_basis_config(&mut basis_config);
649            }
650            basis_config
651        });
652        let basis_config = controller
653            .basis_config
654            .as_ref()
655            .expect("We just set it to Some");
656
657        let up_direction = Dir3::new(-tracker.gravity.f32()).ok();
658        controller.up_direction = up_direction;
659        // TODO: support the case where there is no up direction at all?
660        let up_direction = up_direction.unwrap_or(Dir3::Y);
661
662        let Some(sensors) = S::Basis::get_or_create_sensors(
663            up_direction,
664            basis_config,
665            &controller.basis_memory,
666            &mut sensors_entities.as_mut().sensors_entities,
667            &proximity_sensors_query,
668            controller_entity,
669            &mut commands,
670            has_ghost_overwrites,
671        ) else {
672            continue;
673        };
674
675        match controller.action_flow_status {
676            TnuaActionFlowStatus::NoAction | TnuaActionFlowStatus::ActionOngoing(_) => {}
677            TnuaActionFlowStatus::ActionEnded(_) => {
678                controller.action_flow_status = TnuaActionFlowStatus::NoAction;
679            }
680            TnuaActionFlowStatus::ActionStarted(discriminant)
681            | TnuaActionFlowStatus::Cancelled {
682                old: _,
683                new: discriminant,
684            } => {
685                controller.action_flow_status = TnuaActionFlowStatus::ActionOngoing(discriminant);
686            }
687        }
688
689        controller.basis.apply(
690            basis_config,
691            &mut controller.basis_memory,
692            &sensors,
693            TnuaBasisContext {
694                frame_duration,
695                tracker,
696                up_direction,
697            },
698            &mut motor,
699        );
700
701        if controller.action_feeding_initiated {
702            controller.action_feeding_initiated = false;
703            for fed_entry in controller.actions_being_fed.iter_mut() {
704                match fed_entry.status {
705                    FedStatus::Not | FedStatus::Held => {}
706                    FedStatus::Lingering => {
707                        *fed_entry = Default::default();
708                    }
709                    FedStatus::Fresh | FedStatus::Trigger | FedStatus::Interrupt => {
710                        fed_entry.status = FedStatus::Lingering;
711                        if let Some(rescheduled_in) = &mut fed_entry.rescheduled_in {
712                            rescheduled_in.tick(time.delta());
713                        }
714                    }
715                }
716            }
717        }
718
719        let has_valid_contender =
720            if let Some(contender_action) = controller.contender_action.as_mut() {
721                if controller.actions_being_fed
722                    [contender_action.action.discriminant().feed_status_slot()]
723                .status
724                .considered_fed()
725                {
726                    let initiation_decision = contender_action.action.initiation_decision(
727                        config,
728                        &sensors,
729                        TnuaActionContext {
730                            frame_duration,
731                            tracker,
732                            up_direction,
733                            basis: &TnuaBasisAccess {
734                                input: &controller.basis,
735                                config: basis_config,
736                                memory: &controller.basis_memory,
737                            },
738                        },
739                        &contender_action.being_fed_for,
740                    );
741                    contender_action.being_fed_for.tick(time.delta());
742                    match initiation_decision {
743                        TnuaActionInitiationDirective::Reject => {
744                            controller.contender_action = None;
745                            false
746                        }
747                        TnuaActionInitiationDirective::Delay => false,
748                        TnuaActionInitiationDirective::Allow => true,
749                    }
750                } else {
751                    controller.contender_action = None;
752                    false
753                }
754            } else {
755                false
756            };
757
758        if let Some(action_state) = controller.current_action.as_mut() {
759            let lifecycle_status = if has_valid_contender {
760                TnuaActionLifecycleStatus::CancelledInto
761            } else if controller.actions_being_fed[action_state.discriminant().feed_status_slot()]
762                .status
763                .considered_fed()
764            {
765                TnuaActionLifecycleStatus::StillFed
766            } else {
767                TnuaActionLifecycleStatus::NoLongerFed
768            };
769
770            let directive = action_state.interface_mut().apply(
771                &sensors,
772                TnuaActionContext {
773                    frame_duration,
774                    tracker,
775                    basis: &TnuaBasisAccess {
776                        input: &controller.basis,
777                        config: basis_config,
778                        memory: &controller.basis_memory,
779                    },
780                    up_direction,
781                },
782                lifecycle_status,
783                motor.as_mut(),
784            );
785            action_state.interface_mut().influence_basis(
786                TnuaBasisContext {
787                    frame_duration,
788                    tracker,
789                    up_direction,
790                },
791                &controller.basis,
792                basis_config,
793                &mut controller.basis_memory,
794            );
795            match directive {
796                TnuaActionLifecycleDirective::StillActive => {
797                    if !lifecycle_status.is_active()
798                        && let TnuaActionFlowStatus::ActionOngoing(action_discriminant) =
799                            controller.action_flow_status
800                    {
801                        controller.action_flow_status =
802                            TnuaActionFlowStatus::ActionEnded(action_discriminant);
803                    }
804                }
805                TnuaActionLifecycleDirective::Finished
806                | TnuaActionLifecycleDirective::Reschedule { .. } => {
807                    if let TnuaActionLifecycleDirective::Reschedule { after_seconds } = directive {
808                        controller.actions_being_fed
809                            [action_state.discriminant().feed_status_slot()]
810                        .rescheduled_in =
811                            Some(Timer::from_seconds(after_seconds.f32(), TimerMode::Once));
812                    }
813                    controller.current_action = if has_valid_contender {
814                        let contender_action = controller.contender_action.take().expect(
815                            "has_valid_contender can only be true if contender_action is Some",
816                        );
817                        let mut contender_action_state =
818                            contender_action.action.into_action_state_variant(config);
819
820                        controller.actions_being_fed
821                            [contender_action_state.discriminant().feed_status_slot()]
822                        .rescheduled_in = None;
823
824                        let contender_directive = contender_action_state.interface_mut().apply(
825                            &sensors,
826                            TnuaActionContext {
827                                frame_duration,
828                                tracker,
829                                basis: &TnuaBasisAccess {
830                                    input: &controller.basis,
831                                    config: basis_config,
832                                    memory: &controller.basis_memory,
833                                },
834                                up_direction,
835                            },
836                            TnuaActionLifecycleStatus::CancelledFrom,
837                            motor.as_mut(),
838                        );
839                        contender_action_state.interface_mut().influence_basis(
840                            TnuaBasisContext {
841                                frame_duration,
842                                tracker,
843                                up_direction,
844                            },
845                            &controller.basis,
846                            basis_config,
847                            &mut controller.basis_memory,
848                        );
849                        match contender_directive {
850                            TnuaActionLifecycleDirective::StillActive => {
851                                controller.action_flow_status =
852                                    if let TnuaActionFlowStatus::ActionOngoing(discriminant) =
853                                        controller.action_flow_status
854                                    {
855                                        TnuaActionFlowStatus::Cancelled {
856                                            old: discriminant,
857                                            new: contender_action_state.discriminant(),
858                                        }
859                                    } else {
860                                        TnuaActionFlowStatus::ActionStarted(
861                                            contender_action_state.discriminant(),
862                                        )
863                                    };
864                                Some(contender_action_state)
865                            }
866                            TnuaActionLifecycleDirective::Finished
867                            | TnuaActionLifecycleDirective::Reschedule { after_seconds: _ } => {
868                                if let TnuaActionLifecycleDirective::Reschedule { after_seconds } =
869                                    contender_directive
870                                {
871                                    controller.actions_being_fed[contender_action_state
872                                        .discriminant()
873                                        .feed_status_slot()]
874                                    .rescheduled_in = Some(Timer::from_seconds(
875                                        after_seconds.f32(),
876                                        TimerMode::Once,
877                                    ));
878                                }
879                                if let TnuaActionFlowStatus::ActionOngoing(discriminant) =
880                                    controller.action_flow_status
881                                {
882                                    controller.action_flow_status =
883                                        TnuaActionFlowStatus::ActionEnded(discriminant);
884                                }
885                                None
886                            }
887                        }
888                    } else {
889                        controller.action_flow_status =
890                            TnuaActionFlowStatus::ActionEnded(action_state.discriminant());
891                        None
892                    };
893                }
894            }
895        } else if has_valid_contender {
896            let contender_action = controller
897                .contender_action
898                .take()
899                .expect("has_valid_contender can only be true if contender_action is Some");
900            let mut contender_action_state =
901                contender_action.action.into_action_state_variant(config);
902
903            contender_action_state.interface_mut().apply(
904                &sensors,
905                TnuaActionContext {
906                    frame_duration,
907                    tracker,
908                    basis: &TnuaBasisAccess {
909                        input: &controller.basis,
910                        config: basis_config,
911                        memory: &controller.basis_memory,
912                    },
913                    up_direction,
914                },
915                TnuaActionLifecycleStatus::Initiated,
916                motor.as_mut(),
917            );
918            contender_action_state.interface_mut().influence_basis(
919                TnuaBasisContext {
920                    frame_duration,
921                    tracker,
922                    up_direction,
923                },
924                &controller.basis,
925                basis_config,
926                &mut controller.basis_memory,
927            );
928            controller.action_flow_status =
929                TnuaActionFlowStatus::ActionStarted(contender_action_state.discriminant());
930            controller.current_action = Some(contender_action_state);
931        }
932
933        for fed_entry in controller.actions_being_fed.iter_mut() {
934            match fed_entry.status {
935                FedStatus::Not | FedStatus::Lingering | FedStatus::Fresh | FedStatus::Held => {}
936                FedStatus::Trigger | FedStatus::Interrupt => {
937                    fed_entry.status = FedStatus::Not;
938                }
939            }
940        }
941    }
942}