bevy_tnua/builtins/
dash.rs

1use crate::math::{AdjustPrecision, AsF32, Float, Vector3};
2use bevy::prelude::*;
3
4use crate::util::MotionHelper;
5use crate::{
6    prelude::*, TnuaActionContext, TnuaActionInitiationDirective, TnuaActionLifecycleDirective,
7    TnuaActionLifecycleStatus, TnuaMotor,
8};
9
10/// The basic dash [action](TnuaAction).
11#[derive(Clone, Debug)]
12pub struct TnuaBuiltinDash {
13    /// The direction and distance of the dash.
14    ///
15    /// This input parameter is cached when the action starts. This means that the control system
16    /// does not have to make sure the direction reamins the same even if the player changes it
17    /// mid-dash.
18    pub displacement: Vector3,
19
20    /// Point the negative Z axis of the characetr model in that direction during the dash.
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 desired_forward: Option<Dir3>,
26
27    /// Allow this action to start even if the character is not touching ground nor in coyote time.
28    pub allow_in_air: bool,
29
30    /// The speed the character will move in during the dash.
31    pub speed: Float,
32
33    /// After the dash, the character will brake until its speed is below that number.
34    pub brake_to_speed: Float,
35
36    /// The maximum acceleration when starting the jump.
37    pub acceleration: Float,
38
39    /// The maximum acceleration when braking after the jump.
40    ///
41    /// Irrelevant if [`brake_to_speed`](Self::brake_to_speed) is set to infinity.
42    pub brake_acceleration: Float,
43
44    /// A duration, in seconds, where a player can press a dash button before a dash becomes
45    /// possible (typically when a character is still in the air and about the land) and the dash
46    /// action would still get registered and be executed once the dash is possible.
47    pub input_buffer_time: Float,
48}
49
50impl Default for TnuaBuiltinDash {
51    fn default() -> Self {
52        Self {
53            displacement: Vector3::ZERO,
54            desired_forward: None,
55            allow_in_air: false,
56            speed: 80.0,
57            brake_to_speed: 20.0,
58            acceleration: 400.0,
59            brake_acceleration: 200.0,
60            input_buffer_time: 0.2,
61        }
62    }
63}
64
65impl TnuaAction for TnuaBuiltinDash {
66    const NAME: &'static str = "TnuaBuiltinStraightDash";
67    type State = TnuaBuiltinDashState;
68    const VIOLATES_COYOTE_TIME: bool = true;
69
70    fn initiation_decision(
71        &self,
72        ctx: crate::TnuaActionContext,
73        being_fed_for: &bevy::time::Stopwatch,
74    ) -> crate::TnuaActionInitiationDirective {
75        if !self.displacement.is_finite() || self.displacement == Vector3::ZERO {
76            TnuaActionInitiationDirective::Reject
77        } else if self.allow_in_air || !ctx.basis.is_airborne() {
78            // Either not airborne, or air jumps are allowed
79            TnuaActionInitiationDirective::Allow
80        } else if (being_fed_for.elapsed().as_secs_f64() as Float) < self.input_buffer_time {
81            TnuaActionInitiationDirective::Delay
82        } else {
83            TnuaActionInitiationDirective::Reject
84        }
85    }
86
87    fn apply(
88        &self,
89        state: &mut Self::State,
90        ctx: TnuaActionContext,
91        _lifecycle_status: TnuaActionLifecycleStatus,
92        motor: &mut TnuaMotor,
93    ) -> TnuaActionLifecycleDirective {
94        // TODO: Once `std::mem::variant_count` gets stabilized, use that instead.
95        for _ in 0..3 {
96            return match state {
97                TnuaBuiltinDashState::PreDash => {
98                    let Ok(direction) = Dir3::new(self.displacement.f32()) else {
99                        // Probably unneeded because of the `initiation_decision`, but still
100                        return TnuaActionLifecycleDirective::Finished;
101                    };
102                    *state = TnuaBuiltinDashState::During {
103                        direction,
104                        destination: ctx.tracker.translation + self.displacement,
105                        desired_forward: self.desired_forward,
106                        consider_blocked_if_speed_is_less_than: Float::NEG_INFINITY,
107                    };
108                    continue;
109                }
110                TnuaBuiltinDashState::During {
111                    direction,
112                    destination,
113                    desired_forward,
114                    consider_blocked_if_speed_is_less_than,
115                } => {
116                    let distance_to_destination = direction
117                        .adjust_precision()
118                        .dot(*destination - ctx.tracker.translation);
119                    if distance_to_destination < 0.0 {
120                        *state = TnuaBuiltinDashState::Braking {
121                            direction: *direction,
122                        };
123                        continue;
124                    }
125
126                    let current_speed = direction.adjust_precision().dot(ctx.tracker.velocity);
127                    if current_speed < *consider_blocked_if_speed_is_less_than {
128                        return TnuaActionLifecycleDirective::Finished;
129                    }
130
131                    motor.lin = Default::default();
132                    motor.lin.acceleration = -ctx.tracker.gravity;
133                    motor.lin.boost = (direction.adjust_precision() * self.speed
134                        - ctx.tracker.velocity)
135                        .clamp_length_max(ctx.frame_duration * self.acceleration);
136                    let expected_speed = direction
137                        .adjust_precision()
138                        .dot(ctx.tracker.velocity + motor.lin.boost);
139                    *consider_blocked_if_speed_is_less_than = if current_speed < expected_speed {
140                        0.5 * (current_speed + expected_speed)
141                    } else {
142                        0.5 * current_speed
143                    };
144
145                    if let Some(desired_forward) = desired_forward {
146                        motor
147                            .ang
148                            .cancel_on_axis(ctx.up_direction.adjust_precision());
149                        motor.ang += ctx.turn_to_direction(*desired_forward, ctx.up_direction);
150                    }
151
152                    TnuaActionLifecycleDirective::StillActive
153                }
154                TnuaBuiltinDashState::Braking { direction } => {
155                    let remaining_speed = direction.adjust_precision().dot(ctx.tracker.velocity);
156                    if remaining_speed <= self.brake_to_speed {
157                        TnuaActionLifecycleDirective::Finished
158                    } else {
159                        motor.lin.boost = -direction.adjust_precision()
160                            * (remaining_speed - self.brake_to_speed).min(self.brake_acceleration);
161                        TnuaActionLifecycleDirective::StillActive
162                    }
163                }
164            };
165        }
166        error!("Tnua could not decide on dash state");
167        TnuaActionLifecycleDirective::Finished
168    }
169}
170
171#[derive(Clone, Debug, Default)]
172pub enum TnuaBuiltinDashState {
173    #[default]
174    PreDash,
175    During {
176        direction: Dir3,
177        destination: Vector3,
178        desired_forward: Option<Dir3>,
179        consider_blocked_if_speed_is_less_than: Float,
180    },
181    Braking {
182        direction: Dir3,
183    },
184}