bevy_tnua/builtins/
knockback.rs

1use crate::{
2    TnuaAction, TnuaActionContext, TnuaActionInitiationDirective, TnuaActionLifecycleDirective,
3    TnuaActionLifecycleStatus, TnuaBasis, TnuaMotor, TnuaVelChange,
4    basis_capabilities::TnuaBasisWithGround,
5    math::{AdjustPrecision, Float, Vector3},
6    util::{MotionHelper, VelocityBoundary},
7};
8use bevy::prelude::*;
9use serde::{Deserialize, Serialize};
10
11/// Apply this [action](TnuaAction) to shove the character in a way the [basis](crate::TnuaBasis)
12/// cannot easily nullify.
13///
14/// This action is typically applied outside the regular user input system - which means it should
15/// be applied with [`action_interrupt`](crate::TnuaController::action_interrupt).
16///
17/// Note that this action cannot be cancelled or stopped. Once it starts, it'll resume until the
18/// Pushover boundary is cleared (which means the character overcame the knockback). Unless the
19/// parameters are seriously skewed. The main parameters that can mess it up and unreasonably
20/// prolong the knockback duration are:
21/// * [`no_push_timer`](TnuaBuiltinKnockbackConfig::no_push_timeout). Setting it too high will
22///   allow the character to "move along" with the shove, prolonging the knockback action because
23///   the boundary does not get cleared. The action will not affect the velocity during that time,
24///   but it can still prolong the animation, apply [`force_forward`](Self::force_forward), and
25///   prevent other actions from happening.
26/// * [`barrier_strength_diminishing`](TnuaBuiltinKnockbackConfig::barrier_strength_diminishing).
27///   Setting it too low makes it very hard for the character to push through the boundary. It
28///   starts getting slightly weird below 1.0, and really weird below 0.5. Better keep it at above
29///   - 1.0 levels.
30#[derive(Clone, Debug, Default)]
31#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
32pub struct TnuaBuiltinKnockback {
33    /// Initial impulse to apply to the character before the Pushover stage starts.
34    ///
35    /// It is important that the impulse will be applied using the action (by setting this field)
36    /// and not directly via the physics backend so that Tnua can properly calculate the Pushover
37    /// boundary based on it.
38    pub shove: Vector3,
39
40    /// Force the character to face in a particular direction.
41    ///
42    /// Note that there are no acceleration limits because unlike
43    /// [crate::builtins::TnuaBuiltinWalk::desired_forward] this field will attempt to force the
44    /// direction during a single frame. It is useful for when the knockback animation needs to be
45    /// aligned with the knockback direction.
46    pub force_forward: Option<Dir3>,
47}
48
49#[derive(Clone, Serialize, Deserialize)]
50pub struct TnuaBuiltinKnockbackConfig {
51    /// Timeout (in seconds) for abandoning a Pushover boundary that no longer gets pushed.
52    pub no_push_timeout: f32,
53
54    /// An exponent for controlling the shape of the Pushover barrier diminishing.
55    ///
56    /// For best results, set it to values larger than 1.0.
57    pub barrier_strength_diminishing: Float,
58
59    /// Acceleration cap when pushing against the Pushover barrier.
60    ///
61    /// In practice this will be averaged with the acceleration the basis tries to apply (weighted
62    /// by a function of the Pushover boundary penetration percentage and
63    /// [`barrier_strength_diminishing`](Self::barrier_strength_diminishing)) so the actual
64    /// acceleration limit will higher than that.
65    pub acceleration_limit: Float,
66
67    /// Acceleration cap when pushing against the Pushover barrier while in the air.
68    ///
69    /// In practice this will be averaged with the acceleration the basis tries to apply (weighted
70    /// by a function of the Pushover boundary penetration percentage and
71    /// [`barrier_strength_diminishing`](Self::barrier_strength_diminishing)) so the actual
72    /// acceleration limit will higher than that.
73    pub air_acceleration_limit: Float,
74}
75
76impl Default for TnuaBuiltinKnockbackConfig {
77    fn default() -> Self {
78        Self {
79            no_push_timeout: 0.2,
80            barrier_strength_diminishing: 2.0,
81            acceleration_limit: 3.0,
82            air_acceleration_limit: 1.0,
83        }
84    }
85}
86
87impl<B: TnuaBasis> TnuaAction<B> for TnuaBuiltinKnockback
88where
89    B: TnuaBasisWithGround,
90{
91    type Config = TnuaBuiltinKnockbackConfig;
92    type Memory = TnuaBuiltinKnockbackMemory;
93
94    fn initiation_decision(
95        &self,
96        _config: &Self::Config,
97        _sensors: &B::Sensors<'_>,
98        _ctx: crate::TnuaActionContext<B>,
99        _being_fed_for: &bevy::time::Stopwatch,
100    ) -> TnuaActionInitiationDirective {
101        TnuaActionInitiationDirective::Allow
102    }
103
104    fn apply(
105        &self,
106        config: &Self::Config,
107        memory: &mut Self::Memory,
108        _sensors: &B::Sensors<'_>,
109        ctx: TnuaActionContext<B>,
110        _lifecycle_status: TnuaActionLifecycleStatus,
111        motor: &mut TnuaMotor,
112    ) -> TnuaActionLifecycleDirective {
113        match memory {
114            TnuaBuiltinKnockbackMemory::Shove => {
115                let Some(boundary) = VelocityBoundary::new(
116                    ctx.tracker.velocity,
117                    ctx.tracker.velocity + self.shove,
118                    config.no_push_timeout,
119                ) else {
120                    return TnuaActionLifecycleDirective::Finished;
121                };
122                motor.lin += TnuaVelChange::boost(self.shove);
123                *memory = TnuaBuiltinKnockbackMemory::Pushback { boundary };
124            }
125            TnuaBuiltinKnockbackMemory::Pushback { boundary } => {
126                boundary.update(ctx.tracker.velocity, ctx.frame_duration_as_duration());
127                if boundary.is_cleared() {
128                    return TnuaActionLifecycleDirective::Finished;
129                } else if let Some((component_direction, component_limit)) = boundary
130                    .calc_boost_part_on_boundary_axis_after_limit(
131                        ctx.tracker.velocity,
132                        motor.lin.calc_boost(ctx.frame_duration),
133                        ctx.frame_duration * config.acceleration_limit,
134                        config.barrier_strength_diminishing,
135                    )
136                {
137                    motor.lin.apply_boost_limit(
138                        ctx.frame_duration,
139                        component_direction,
140                        component_limit,
141                    );
142                }
143            }
144        }
145
146        if let Some(force_forward) = self.force_forward {
147            motor
148                .ang
149                .cancel_on_axis(ctx.up_direction.adjust_precision());
150            motor.ang += ctx.turn_to_direction(force_forward, ctx.up_direction);
151        }
152
153        TnuaActionLifecycleDirective::StillActive
154    }
155
156    fn influence_basis(
157        &self,
158        _config: &Self::Config,
159        _memory: &Self::Memory,
160        _ctx: crate::TnuaBasisContext,
161        _basis_input: &B,
162        _basis_config: &<B as TnuaBasis>::Config,
163        basis_memory: &mut <B as TnuaBasis>::Memory,
164    ) {
165        B::violate_coyote_time(basis_memory);
166    }
167}
168
169#[derive(Default, Clone, Debug)]
170#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
171pub enum TnuaBuiltinKnockbackMemory {
172    /// Applying the [`shove`](TnuaBuiltinKnockback::shove) impulse to the character.
173    #[default]
174    Shove,
175    /// Hindering the character's ability to overcome the
176    /// [`Shove`](TnuaBuiltinKnockbackMemory::Shove) while waiting for it to overcome it despite the
177    /// hindrance.
178    Pushback { boundary: VelocityBoundary },
179}