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::NUM_VARIANTS).map(|_| Default::default()).collect(),
198            contender_action: None,
199            action_flow_status: TnuaActionFlowStatus::NoAction,
200            up_direction: None,
201            action_feeding_initiated: false,
202            current_action: None,
203        }
204    }
205}
206
207/// The result of [`TnuaController::action_flow_status()`].
208#[derive(Debug, Clone, Default)]
209#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
210pub enum TnuaActionFlowStatus<D: TnuaActionDiscriminant> {
211    /// No action is going on.
212    #[default]
213    NoAction,
214
215    /// An action just started.
216    ActionStarted(D),
217
218    /// An action was fed in a past frame and is still ongoing.
219    ActionOngoing(D),
220
221    /// An action has stopped being fed.
222    ///
223    /// Note that the action may still have a termination sequence after this happens.
224    ActionEnded(D),
225
226    /// An action has just been canceled into another action.
227    Cancelled { old: D, new: D },
228}
229
230impl<D: TnuaActionDiscriminant> TnuaActionFlowStatus<D> {
231    /// The discriminant of the ongoing action, if there is an ongoing action.
232    ///
233    /// Will also return a value if the action has just started.
234    pub fn ongoing(&self) -> Option<D> {
235        match self {
236            TnuaActionFlowStatus::NoAction | TnuaActionFlowStatus::ActionEnded(_) => None,
237            TnuaActionFlowStatus::ActionStarted(discriminant)
238            | TnuaActionFlowStatus::ActionOngoing(discriminant)
239            | TnuaActionFlowStatus::Cancelled {
240                old: _,
241                new: discriminant,
242            } => Some(*discriminant),
243        }
244    }
245
246    /// The discriminant of the action that has just started this frame.
247    ///
248    /// Will return `None` if there is no action, or if the ongoing action has started in a past
249    /// frame.
250    pub fn just_starting(&self) -> Option<D> {
251        match self {
252            TnuaActionFlowStatus::NoAction
253            | TnuaActionFlowStatus::ActionOngoing(_)
254            | TnuaActionFlowStatus::ActionEnded(_) => None,
255            TnuaActionFlowStatus::ActionStarted(discriminant)
256            | TnuaActionFlowStatus::Cancelled {
257                old: _,
258                new: discriminant,
259            } => Some(*discriminant),
260        }
261    }
262}
263
264impl<S: TnuaScheme> TnuaController<S> {
265    /// Access to the entire basis state. This is what the [basis
266    /// capabilities](crate::basis_capabilities) usually use.
267    pub fn basis_access(
268        &'_ self,
269    ) -> Result<TnuaBasisAccess<'_, S::Basis>, TnuaControllerHasNotPulledConfiguration> {
270        Ok(TnuaBasisAccess {
271            input: &self.basis,
272            config: self
273                .basis_config
274                .as_ref()
275                .ok_or(TnuaControllerHasNotPulledConfiguration)?,
276            memory: &self.basis_memory,
277        })
278    }
279
280    /// **Must** be called each frame before feeding the actions (unless [`action`](Self::action))
281    /// is never used)
282    pub fn initiate_action_feeding(&mut self) {
283        self.action_feeding_initiated = true;
284    }
285
286    /// Feed an action.
287    ///
288    /// This is used in pull fashion - a system (or set of systems) that checks the state of the
289    /// playter input (or of some NPC controller) and can feed the action **every frame** its still
290    /// on. This system must start by calling
291    /// [`initiate_action_feeding`](Self::initiate_action_feeding).
292    ///
293    /// For pushing actions, use one of the other `action_*` methods.
294    pub fn action(&mut self, action: S) {
295        assert!(
296            self.action_feeding_initiated,
297            "Feeding action without invoking `initiate_action_feeding()`"
298        );
299        let fed_entry = &mut self.actions_being_fed[action.variant_idx()];
300
301        match fed_entry.status {
302            FedStatus::Lingering
303            | FedStatus::Fresh
304            | FedStatus::Trigger
305            | FedStatus::Interrupt
306            | FedStatus::Held => {
307                fed_entry.status = FedStatus::Fresh;
308                if let Some(current_action) = self.current_action.as_mut() {
309                    match action.update_in_action_state(current_action) {
310                        TnuaUpdateInActionStateResult::Success => {
311                            // Do nothing farther
312                        }
313                        TnuaUpdateInActionStateResult::WrongVariant(_) => {
314                            // different action is running - will not override because button was
315                            // already pressed.
316                        }
317                    }
318                } else if self.contender_action.is_none()
319                    && fed_entry
320                        .rescheduled_in
321                        .as_ref()
322                        .is_some_and(|timer| timer.is_finished())
323                {
324                    // no action is running - but this action is rescheduled and there is no
325                    // already-existing contender that would have taken priority
326                    self.contender_action = Some(ContenderAction {
327                        action,
328                        being_fed_for: Stopwatch::new(),
329                    });
330                } else {
331                    // no action is running - will not set because button was already pressed.
332                }
333            }
334            FedStatus::Not => {
335                *fed_entry = FedEntry {
336                    status: FedStatus::Fresh,
337                    rescheduled_in: None,
338                };
339                if let Some(contender_action) = self.contender_action.as_mut()
340                    && action.discriminant() == contender_action.action.discriminant()
341                {
342                    contender_action.action = action;
343                } else if let Some(contender_action) = self.contender_action.as_ref()
344                    && self.actions_being_fed[contender_action.action.discriminant().variant_idx()]
345                        .status
346                        == FedStatus::Interrupt
347                {
348                    // If the existing condender is an interrupt, we will not overwrite it.
349                } else {
350                    self.contender_action = Some(ContenderAction {
351                        action,
352                        being_fed_for: Stopwatch::new(),
353                    });
354                }
355            }
356        }
357    }
358
359    /// Trigger an action in a push fashion. The action must be one that handles its own duration.
360    ///
361    /// This means that actions like [`TnuaBuiltinCrouch`](crate::builtins::TnuaBuiltinCrouch)
362    /// cannot be used with this method - the character will rise after one frame of starting to
363    /// crouch. Actions like ['TnuaBuiltinDash`](crate::builtins::TnuaBuiltinDash) are okay because
364    /// the dash will only end when the motion itself is finished.
365    pub fn action_trigger(&mut self, action: S) {
366        let fed_entry = &mut self.actions_being_fed[action.variant_idx()];
367
368        match fed_entry.status {
369            FedStatus::Lingering
370            | FedStatus::Fresh
371            | FedStatus::Trigger
372            | FedStatus::Interrupt
373            | FedStatus::Held => {
374                // Do nothing because the action was already triggered
375            }
376            FedStatus::Not => {
377                *fed_entry = FedEntry {
378                    status: FedStatus::Trigger,
379                    rescheduled_in: None,
380                };
381                if let Some(contender_action) = self.contender_action.as_mut()
382                    && action.discriminant() == contender_action.action.discriminant()
383                {
384                    contender_action.action = action;
385                } else if let Some(contender_action) = self.contender_action.as_ref()
386                    && self.actions_being_fed[contender_action.action.discriminant().variant_idx()]
387                        .status
388                        == FedStatus::Interrupt
389                {
390                    // If the existing condender is an interrupt, we will not overwrite it.
391                } else {
392                    self.contender_action = Some(ContenderAction {
393                        action,
394                        being_fed_for: Stopwatch::new(),
395                    });
396                }
397            }
398        }
399    }
400
401    /// Similar to [`action_trigger`](Self::action_trigger), but can override other action
402    /// contenders and other action feeding methods cannot override it.
403    pub fn action_interrupt(&mut self, action: S) {
404        // Because this is an interrupt, we ignore the old fed status - but we still care not to
405        // set the contender if we are the current action.
406        self.actions_being_fed[action.variant_idx()] = FedEntry {
407            status: FedStatus::Interrupt,
408            rescheduled_in: None,
409        };
410
411        let action = if let Some(current_action) = self.current_action.as_mut() {
412            match action.update_in_action_state(current_action) {
413                TnuaUpdateInActionStateResult::Success => {
414                    return;
415                }
416                TnuaUpdateInActionStateResult::WrongVariant(action) => {
417                    // different action is running - we'll have to
418                    action
419                }
420            }
421        } else {
422            action
423        };
424        // Overwrite the condender action even if there already was a contender action.
425        self.contender_action = Some(ContenderAction {
426            action,
427            being_fed_for: Stopwatch::new(),
428        });
429    }
430
431    /// Trigger an action in a push fashion. The action will continue until
432    /// [`action_end`](Self::action_end) is called - or until the motion itself is finished.
433    pub fn action_start(&mut self, action: S) {
434        let fed_entry = &mut self.actions_being_fed[action.variant_idx()];
435
436        match fed_entry.status {
437            FedStatus::Lingering | FedStatus::Fresh | FedStatus::Trigger | FedStatus::Interrupt => {
438                // Do nothing because the action was already triggered
439            }
440            FedStatus::Held => {
441                // Action was already started - but still allowed to change its parameters
442                let action = if let Some(current_action) = self.current_action.as_mut() {
443                    match action.update_in_action_state(current_action) {
444                        TnuaUpdateInActionStateResult::Success => {
445                            // Managed to update current_action - no need to check the contender_action
446                            return;
447                        }
448                        TnuaUpdateInActionStateResult::WrongVariant(action) => action,
449                    }
450                } else {
451                    action
452                };
453                if let Some(contender_action) = self.contender_action.as_mut()
454                    && action.discriminant() == contender_action.action.discriminant()
455                {
456                    // This action is still condender - we can safely update it
457                    contender_action.action = action;
458                }
459            }
460            FedStatus::Not => {
461                *fed_entry = FedEntry {
462                    status: FedStatus::Held,
463                    rescheduled_in: None,
464                };
465                if let Some(contender_action) = self.contender_action.as_mut()
466                    && action.discriminant() == contender_action.action.discriminant()
467                {
468                    contender_action.action = action;
469                } else if let Some(contender_action) = self.contender_action.as_ref()
470                    && self.actions_being_fed[contender_action.action.discriminant().variant_idx()]
471                        .status
472                        == FedStatus::Interrupt
473                {
474                    // If the existing condender is an interrupt, we will not overwrite it.
475                } else {
476                    self.contender_action = Some(ContenderAction {
477                        action,
478                        being_fed_for: Stopwatch::new(),
479                    });
480                }
481            }
482        }
483    }
484
485    /// End an action that started with [`action_start`](Self::action_start)
486    pub fn action_end(&mut self, action: S::ActionDiscriminant) {
487        // Note that even if the action was an interrupt -this is a direct order to end it.
488        self.actions_being_fed[action.variant_idx()] = Default::default();
489    }
490
491    /// Re-feed the same action that is currently active.
492    ///
493    /// This is useful when matching on [`current_action`](Self::current_action) and wanting to
494    /// continue feeding the **exact same** action with the **exact same** input without having to
495    pub fn prolong_action(&mut self) {
496        if let Some(current_action) = self.action_discriminant() {
497            self.actions_being_fed[current_action.variant_idx()].status = FedStatus::Fresh;
498        }
499    }
500
501    /// The discriminant of the currently running action.
502    pub fn action_discriminant(&self) -> Option<S::ActionDiscriminant> {
503        Some(self.current_action.as_ref()?.discriminant())
504    }
505
506    /// Indicator for the state and flow of movement actions.
507    ///
508    /// Query this every frame to keep track of the actions. For air actions,
509    /// [`TnuaAirActionsTracker`](crate::control_helpers::TnuaAirActionsTracker) is easier to use
510    /// (and uses this behind the scenes)
511    ///
512    /// The benefits of this over querying [`action_discriminant`](Self::action_discriminant) every
513    /// frame are:
514    ///
515    /// * `action_flow_status` can indicate when the same action has been fed again immediately
516    ///   after stopping or cancelled into itself.
517    /// * `action_flow_status` shows an [`ActionEnded`](TnuaActionFlowStatus::ActionEnded) when the
518    ///   action is no longer fed, even if the action is still active (termination sequence)
519    pub fn action_flow_status(&self) -> &TnuaActionFlowStatus<S::ActionDiscriminant> {
520        &self.action_flow_status
521    }
522
523    /// Returns the direction considered as up.
524    ///
525    /// Note that the up direction is based on gravity, as reported by
526    /// [`TnuaRigidBodyTracker::gravity`], and that it'd typically be one frame behind since it
527    /// gets updated in the same system that applies the controller logic. If this is unacceptable,
528    /// consider using [`TnuaRigidBodyTracker::gravity`] directly or deducing the up direction via
529    /// different means.
530    pub fn up_direction(&self) -> Option<Dir3> {
531        self.up_direction
532    }
533}
534
535impl<S: TnuaScheme> TnuaController<S>
536where
537    S::Basis: TnuaBasisWithGround,
538{
539    /// Checks if the character is currently airborne.
540    ///
541    /// The check is done based on the basis, and is equivalent to getting the controller's
542    /// [`basis_access`](Self::basis_access) and using [`TnuaBasisWithGround::is_airborne`] on it.
543    pub fn is_airborne(&self) -> Result<bool, TnuaControllerHasNotPulledConfiguration> {
544        Ok(S::Basis::is_airborne(&self.basis_access()?))
545    }
546}
547
548#[derive(thiserror::Error, Debug)]
549#[error("The Tnua controller did not pull the configuration asset yet")]
550pub struct TnuaControllerHasNotPulledConfiguration;
551
552fn apply_ghost_overwrites<S: TnuaScheme>(
553    mut query: Query<(&TnuaSensorsEntities<S>, &mut TnuaGhostOverwrites<S>)>,
554    mut proximity_sensors_query: Query<(&mut TnuaProximitySensor, &TnuaGhostSensor)>,
555) {
556    for (sensors_entities, mut ghost_overwrites) in query.iter_mut() {
557        for (ghost_overwrite, sensor_entity) in
558            S::Basis::ghost_sensor_overwrites(ghost_overwrites.as_mut().as_mut(), sensors_entities)
559        {
560            let Ok((mut proximity_sensor, ghost_sensor)) =
561                proximity_sensors_query.get_mut(sensor_entity)
562            else {
563                continue;
564            };
565            if let Some(ghost_output) = ghost_overwrite.find_in(&ghost_sensor.0) {
566                proximity_sensor.output = Some(ghost_output.clone());
567            } else {
568                ghost_overwrite.set(None);
569            }
570        }
571    }
572}
573
574#[allow(clippy::type_complexity)]
575fn apply_controller_system<S: TnuaScheme>(
576    time: Res<Time>,
577    mut query: Query<(
578        Entity,
579        &mut TnuaController<S>,
580        &TnuaConfig<S>,
581        &mut TnuaSensorsEntities<S>,
582        &TnuaRigidBodyTracker,
583        &mut TnuaMotor,
584        Option<&TnuaToggle>,
585        Has<TnuaGhostOverwrites<S>>,
586    )>,
587    proximity_sensors_query: Query<(&TnuaProximitySensor, Has<TnuaGhostSensor>)>,
588    config_assets: Res<Assets<S::Config>>,
589    mut commands: Commands,
590) {
591    let frame_duration = time.delta().as_secs_f64() as Float;
592    if frame_duration == 0.0 {
593        return;
594    }
595    for (
596        controller_entity,
597        mut controller,
598        config_handle,
599        mut sensors_entities,
600        tracker,
601        mut motor,
602        tnua_toggle,
603        has_ghost_overwrites,
604    ) in query.iter_mut()
605    {
606        match tnua_toggle.copied().unwrap_or_default() {
607            TnuaToggle::Disabled => continue,
608            TnuaToggle::SenseOnly => {}
609            TnuaToggle::Enabled => {}
610        }
611        let controller = controller.as_mut();
612
613        let Some(config) = config_assets.get(&config_handle.0) else {
614            continue;
615        };
616        controller.basis_config = Some({
617            let mut basis_config = config.basis_config().clone();
618            if let Some(current_action) = controller.current_action.as_ref() {
619                current_action.modify_basis_config(&mut basis_config);
620            }
621            basis_config
622        });
623        let basis_config = controller
624            .basis_config
625            .as_ref()
626            .expect("We just set it to Some");
627
628        let up_direction = Dir3::new(-tracker.gravity.f32()).ok();
629        controller.up_direction = up_direction;
630        // TODO: support the case where there is no up direction at all?
631        let up_direction = up_direction.unwrap_or(Dir3::Y);
632
633        let Some(sensors) = S::Basis::get_or_create_sensors(
634            up_direction,
635            basis_config,
636            &controller.basis_memory,
637            &mut sensors_entities.as_mut().sensors_entities,
638            &proximity_sensors_query,
639            controller_entity,
640            &mut commands,
641            has_ghost_overwrites,
642        ) else {
643            continue;
644        };
645
646        match controller.action_flow_status {
647            TnuaActionFlowStatus::NoAction | TnuaActionFlowStatus::ActionOngoing(_) => {}
648            TnuaActionFlowStatus::ActionEnded(_) => {
649                controller.action_flow_status = TnuaActionFlowStatus::NoAction;
650            }
651            TnuaActionFlowStatus::ActionStarted(discriminant)
652            | TnuaActionFlowStatus::Cancelled {
653                old: _,
654                new: discriminant,
655            } => {
656                controller.action_flow_status = TnuaActionFlowStatus::ActionOngoing(discriminant);
657            }
658        }
659
660        controller.basis.apply(
661            basis_config,
662            &mut controller.basis_memory,
663            &sensors,
664            TnuaBasisContext {
665                frame_duration,
666                tracker,
667                up_direction,
668            },
669            &mut motor,
670        );
671
672        if controller.action_feeding_initiated {
673            controller.action_feeding_initiated = false;
674            for fed_entry in controller.actions_being_fed.iter_mut() {
675                match fed_entry.status {
676                    FedStatus::Not | FedStatus::Held => {}
677                    FedStatus::Lingering => {
678                        *fed_entry = Default::default();
679                    }
680                    FedStatus::Fresh | FedStatus::Trigger | FedStatus::Interrupt => {
681                        fed_entry.status = FedStatus::Lingering;
682                        if let Some(rescheduled_in) = &mut fed_entry.rescheduled_in {
683                            rescheduled_in.tick(time.delta());
684                        }
685                    }
686                }
687            }
688        }
689
690        let has_valid_contender =
691            if let Some(contender_action) = controller.contender_action.as_mut() {
692                if controller.actions_being_fed[contender_action.action.variant_idx()]
693                    .status
694                    .considered_fed()
695                {
696                    let initiation_decision = contender_action.action.initiation_decision(
697                        config,
698                        &sensors,
699                        TnuaActionContext {
700                            frame_duration,
701                            tracker,
702                            up_direction,
703                            basis: &TnuaBasisAccess {
704                                input: &controller.basis,
705                                config: basis_config,
706                                memory: &controller.basis_memory,
707                            },
708                        },
709                        &contender_action.being_fed_for,
710                    );
711                    contender_action.being_fed_for.tick(time.delta());
712                    match initiation_decision {
713                        TnuaActionInitiationDirective::Reject => {
714                            controller.contender_action = None;
715                            false
716                        }
717                        TnuaActionInitiationDirective::Delay => false,
718                        TnuaActionInitiationDirective::Allow => true,
719                    }
720                } else {
721                    controller.contender_action = None;
722                    false
723                }
724            } else {
725                false
726            };
727
728        if let Some(action_state) = controller.current_action.as_mut() {
729            let lifecycle_status = if has_valid_contender {
730                TnuaActionLifecycleStatus::CancelledInto
731            } else if controller.actions_being_fed[action_state.variant_idx()]
732                .status
733                .considered_fed()
734            {
735                TnuaActionLifecycleStatus::StillFed
736            } else {
737                TnuaActionLifecycleStatus::NoLongerFed
738            };
739
740            let directive = action_state.interface_mut().apply(
741                &sensors,
742                TnuaActionContext {
743                    frame_duration,
744                    tracker,
745                    basis: &TnuaBasisAccess {
746                        input: &controller.basis,
747                        config: basis_config,
748                        memory: &controller.basis_memory,
749                    },
750                    up_direction,
751                },
752                lifecycle_status,
753                motor.as_mut(),
754            );
755            action_state.interface_mut().influence_basis(
756                TnuaBasisContext {
757                    frame_duration,
758                    tracker,
759                    up_direction,
760                },
761                &controller.basis,
762                basis_config,
763                &mut controller.basis_memory,
764            );
765            match directive {
766                TnuaActionLifecycleDirective::StillActive => {
767                    if !lifecycle_status.is_active()
768                        && let TnuaActionFlowStatus::ActionOngoing(action_discriminant) =
769                            controller.action_flow_status
770                    {
771                        controller.action_flow_status =
772                            TnuaActionFlowStatus::ActionEnded(action_discriminant);
773                    }
774                }
775                TnuaActionLifecycleDirective::Finished
776                | TnuaActionLifecycleDirective::Reschedule { .. } => {
777                    if let TnuaActionLifecycleDirective::Reschedule { after_seconds } = directive {
778                        controller.actions_being_fed[action_state.variant_idx()].rescheduled_in =
779                            Some(Timer::from_seconds(after_seconds.f32(), TimerMode::Once));
780                    }
781                    controller.current_action = if has_valid_contender {
782                        let contender_action = controller.contender_action.take().expect(
783                            "has_valid_contender can only be true if contender_action is Some",
784                        );
785                        let mut contender_action_state =
786                            contender_action.action.into_action_state_variant(config);
787
788                        controller.actions_being_fed[contender_action_state.variant_idx()]
789                            .rescheduled_in = None;
790
791                        let contender_directive = contender_action_state.interface_mut().apply(
792                            &sensors,
793                            TnuaActionContext {
794                                frame_duration,
795                                tracker,
796                                basis: &TnuaBasisAccess {
797                                    input: &controller.basis,
798                                    config: basis_config,
799                                    memory: &controller.basis_memory,
800                                },
801                                up_direction,
802                            },
803                            TnuaActionLifecycleStatus::CancelledFrom,
804                            motor.as_mut(),
805                        );
806                        contender_action_state.interface_mut().influence_basis(
807                            TnuaBasisContext {
808                                frame_duration,
809                                tracker,
810                                up_direction,
811                            },
812                            &controller.basis,
813                            basis_config,
814                            &mut controller.basis_memory,
815                        );
816                        match contender_directive {
817                            TnuaActionLifecycleDirective::StillActive => {
818                                controller.action_flow_status =
819                                    if let TnuaActionFlowStatus::ActionOngoing(discriminant) =
820                                        controller.action_flow_status
821                                    {
822                                        TnuaActionFlowStatus::Cancelled {
823                                            old: discriminant,
824                                            new: contender_action_state.discriminant(),
825                                        }
826                                    } else {
827                                        TnuaActionFlowStatus::ActionStarted(
828                                            contender_action_state.discriminant(),
829                                        )
830                                    };
831                                Some(contender_action_state)
832                            }
833                            TnuaActionLifecycleDirective::Finished
834                            | TnuaActionLifecycleDirective::Reschedule { after_seconds: _ } => {
835                                if let TnuaActionLifecycleDirective::Reschedule { after_seconds } =
836                                    contender_directive
837                                {
838                                    controller.actions_being_fed
839                                        [contender_action_state.variant_idx()]
840                                    .rescheduled_in = Some(Timer::from_seconds(
841                                        after_seconds.f32(),
842                                        TimerMode::Once,
843                                    ));
844                                }
845                                if let TnuaActionFlowStatus::ActionOngoing(discriminant) =
846                                    controller.action_flow_status
847                                {
848                                    controller.action_flow_status =
849                                        TnuaActionFlowStatus::ActionEnded(discriminant);
850                                }
851                                None
852                            }
853                        }
854                    } else {
855                        controller.action_flow_status =
856                            TnuaActionFlowStatus::ActionEnded(action_state.discriminant());
857                        None
858                    };
859                }
860            }
861        } else if has_valid_contender {
862            let contender_action = controller
863                .contender_action
864                .take()
865                .expect("has_valid_contender can only be true if contender_action is Some");
866            let mut contender_action_state =
867                contender_action.action.into_action_state_variant(config);
868
869            contender_action_state.interface_mut().apply(
870                &sensors,
871                TnuaActionContext {
872                    frame_duration,
873                    tracker,
874                    basis: &TnuaBasisAccess {
875                        input: &controller.basis,
876                        config: basis_config,
877                        memory: &controller.basis_memory,
878                    },
879                    up_direction,
880                },
881                TnuaActionLifecycleStatus::Initiated,
882                motor.as_mut(),
883            );
884            contender_action_state.interface_mut().influence_basis(
885                TnuaBasisContext {
886                    frame_duration,
887                    tracker,
888                    up_direction,
889                },
890                &controller.basis,
891                basis_config,
892                &mut controller.basis_memory,
893            );
894            controller.action_flow_status =
895                TnuaActionFlowStatus::ActionStarted(contender_action_state.discriminant());
896            controller.current_action = Some(contender_action_state);
897        }
898
899        for fed_entry in controller.actions_being_fed.iter_mut() {
900            match fed_entry.status {
901                FedStatus::Not | FedStatus::Lingering | FedStatus::Fresh | FedStatus::Held => {}
902                FedStatus::Trigger | FedStatus::Interrupt => {
903                    fed_entry.status = FedStatus::Not;
904                }
905            }
906        }
907    }
908}