bevy_tnua/
controller.rs

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