Skip to main content

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