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