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}