bevy_tnua/builtins/
dash.rs

1use crate::basis_capabilities::TnuaBasisWithGround;
2use crate::math::{AdjustPrecision, AsF32, Float, Vector3};
3use bevy::prelude::*;
4use serde::{Deserialize, Serialize};
5
6use crate::util::MotionHelper;
7use crate::{
8    TnuaAction, TnuaActionContext, TnuaActionInitiationDirective, TnuaActionLifecycleDirective,
9    TnuaActionLifecycleStatus, TnuaBasis, TnuaMotor,
10};
11
12/// The basic dash [action](TnuaAction).
13#[derive(Clone, Debug, Default)]
14#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
15pub struct TnuaBuiltinDash {
16    /// The direction and distance of the dash.
17    ///
18    /// The horizontal and vertical components of this vector are multiplied by the
19    /// [`horizontal_distance`](TnuaBuiltinDashConfig::horizontal_distance) and
20    /// [`vertical_distance`](TnuaBuiltinDashConfig::vertical_distance) configuration fields.
21    ///
22    /// This input parameter is cached when the action starts. This means that the control system
23    /// does not have to make sure the direction reamins the same even if the player changes it
24    /// mid-dash.
25    pub displacement: Vector3,
26
27    /// Point the negative Z axis of the characetr model in that direction during the dash.
28    ///
29    /// This input parameter is cached when the action starts. This means that the control system
30    /// does not have to make sure the direction reamins the same even if the player changes it
31    /// mid-dash.
32    pub desired_forward: Option<Dir3>,
33
34    /// Allow this action to start even if the character is not touching ground nor in coyote time.
35    pub allow_in_air: bool,
36}
37
38#[derive(Clone, Serialize, Deserialize)]
39pub struct TnuaBuiltinDashConfig {
40    /// The speed the character will move in during the dash.
41    pub speed: Float,
42
43    /// Multiplier for the horizontal component of the
44    /// [`displacement`](TnuaBuiltinDash::displacement) given in the input.
45    pub horizontal_distance: Float,
46
47    /// Multiplier for the vertical component of the
48    /// [`displacement`](TnuaBuiltinDash::displacement) given in the input.
49    pub vertical_distance: Float,
50
51    /// After the dash, the character will brake until its speed is below that number.
52    pub brake_to_speed: Float,
53
54    /// The maximum acceleration when starting the jump.
55    pub acceleration: Float,
56
57    /// The maximum acceleration when braking after the jump.
58    ///
59    /// Irrelevant if [`brake_to_speed`](Self::brake_to_speed) is set to infinity.
60    pub brake_acceleration: Float,
61
62    /// A duration, in seconds, where a player can press a dash button before a dash becomes
63    /// possible (typically when a character is still in the air and about the land) and the dash
64    /// action would still get registered and be executed once the dash is possible.
65    pub input_buffer_time: Float,
66}
67
68impl Default for TnuaBuiltinDashConfig {
69    fn default() -> Self {
70        Self {
71            speed: 80.0,
72            horizontal_distance: 1.0,
73            vertical_distance: 1.0,
74            brake_to_speed: 20.0,
75            acceleration: 400.0,
76            brake_acceleration: 200.0,
77            input_buffer_time: 0.2,
78        }
79    }
80}
81
82impl<B: TnuaBasis> TnuaAction<B> for TnuaBuiltinDash
83where
84    B: TnuaBasisWithGround,
85{
86    type Config = TnuaBuiltinDashConfig;
87    type Memory = TnuaBuiltinDashMemory;
88
89    fn initiation_decision(
90        &self,
91        config: &Self::Config,
92        _sensors: &B::Sensors<'_>,
93        ctx: crate::TnuaActionContext<B>,
94        being_fed_for: &bevy::time::Stopwatch,
95    ) -> crate::TnuaActionInitiationDirective {
96        if !self.displacement.is_finite() || self.displacement == Vector3::ZERO {
97            TnuaActionInitiationDirective::Reject
98        } else if self.allow_in_air || !B::is_airborne(ctx.basis) {
99            // Either not airborne, or air jumps are allowed
100            TnuaActionInitiationDirective::Allow
101        } else if (being_fed_for.elapsed().as_secs_f64() as Float) < config.input_buffer_time {
102            TnuaActionInitiationDirective::Delay
103        } else {
104            TnuaActionInitiationDirective::Reject
105        }
106    }
107
108    fn apply(
109        &self,
110        config: &Self::Config,
111        memory: &mut Self::Memory,
112        _sensors: &B::Sensors<'_>,
113        ctx: TnuaActionContext<B>,
114        _lifecycle_status: TnuaActionLifecycleStatus,
115        motor: &mut TnuaMotor,
116    ) -> TnuaActionLifecycleDirective {
117        // TODO: Once `std::mem::variant_count` gets stabilized, use that instead.
118        for _ in 0..3 {
119            return match memory {
120                TnuaBuiltinDashMemory::PreDash => {
121                    let horizontal_displacement = self
122                        .displacement
123                        .reject_from(ctx.up_direction.adjust_precision());
124                    let vertical_displacement = self
125                        .displacement
126                        .project_onto(ctx.up_direction.adjust_precision());
127                    let displacement = config.horizontal_distance * horizontal_displacement
128                        + config.vertical_distance * vertical_displacement;
129                    let Ok(direction) = Dir3::new(displacement.f32()) else {
130                        // Probably unneeded because of the `initiation_decision`, but still
131                        return TnuaActionLifecycleDirective::Finished;
132                    };
133                    *memory = TnuaBuiltinDashMemory::During {
134                        direction,
135                        destination: ctx.tracker.translation + displacement,
136                        desired_forward: self.desired_forward,
137                        consider_blocked_if_speed_is_less_than: Float::NEG_INFINITY,
138                    };
139                    continue;
140                }
141                TnuaBuiltinDashMemory::During {
142                    direction,
143                    destination,
144                    desired_forward,
145                    consider_blocked_if_speed_is_less_than,
146                } => {
147                    let distance_to_destination = direction
148                        .adjust_precision()
149                        .dot(*destination - ctx.tracker.translation);
150                    if distance_to_destination < 0.0 {
151                        *memory = TnuaBuiltinDashMemory::Braking {
152                            direction: *direction,
153                        };
154                        continue;
155                    }
156
157                    let current_speed = direction.adjust_precision().dot(ctx.tracker.velocity);
158                    if current_speed < *consider_blocked_if_speed_is_less_than {
159                        return TnuaActionLifecycleDirective::Finished;
160                    }
161
162                    motor.lin = Default::default();
163                    motor.lin.acceleration = -ctx.tracker.gravity;
164                    motor.lin.boost = (direction.adjust_precision() * config.speed
165                        - ctx.tracker.velocity)
166                        .clamp_length_max(ctx.frame_duration * config.acceleration);
167                    let expected_speed = direction
168                        .adjust_precision()
169                        .dot(ctx.tracker.velocity + motor.lin.boost);
170                    *consider_blocked_if_speed_is_less_than = if current_speed < expected_speed {
171                        0.5 * (current_speed + expected_speed)
172                    } else {
173                        0.5 * current_speed
174                    };
175
176                    if let Some(desired_forward) = desired_forward {
177                        motor
178                            .ang
179                            .cancel_on_axis(ctx.up_direction.adjust_precision());
180                        motor.ang += ctx.turn_to_direction(*desired_forward, ctx.up_direction);
181                    }
182
183                    TnuaActionLifecycleDirective::StillActive
184                }
185                TnuaBuiltinDashMemory::Braking { direction } => {
186                    let remaining_speed = direction.adjust_precision().dot(ctx.tracker.velocity);
187                    if remaining_speed <= config.brake_to_speed {
188                        TnuaActionLifecycleDirective::Finished
189                    } else {
190                        motor.lin.boost = -direction.adjust_precision()
191                            * (remaining_speed - config.brake_to_speed)
192                                .min(config.brake_acceleration);
193                        TnuaActionLifecycleDirective::StillActive
194                    }
195                }
196            };
197        }
198        error!("Tnua could not decide on dash state");
199        TnuaActionLifecycleDirective::Finished
200    }
201
202    fn influence_basis(
203        &self,
204        _config: &Self::Config,
205        _memory: &Self::Memory,
206        _ctx: crate::TnuaBasisContext,
207        _basis_input: &B,
208        _basis_config: &<B as TnuaBasis>::Config,
209        basis_memory: &mut <B as TnuaBasis>::Memory,
210    ) {
211        B::violate_coyote_time(basis_memory);
212    }
213}
214
215#[derive(Clone, Debug, Default)]
216#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
217pub enum TnuaBuiltinDashMemory {
218    #[default]
219    PreDash,
220    During {
221        direction: Dir3,
222        destination: Vector3,
223        desired_forward: Option<Dir3>,
224        consider_blocked_if_speed_is_less_than: Float,
225    },
226    Braking {
227        direction: Dir3,
228    },
229}