bevy_tnua/builtins/
crouch.rs

1use crate::math::{AdjustPrecision, Float};
2use bevy::prelude::*;
3
4use crate::control_helpers::TnuaCrouchEnforcedAction;
5use crate::{TnuaAction, TnuaMotor, TnuaVelChange};
6use crate::{
7    TnuaActionContext, TnuaActionInitiationDirective, TnuaActionLifecycleDirective,
8    TnuaActionLifecycleStatus,
9};
10
11use super::TnuaBuiltinWalk;
12
13/// An [action](TnuaAction) for crouching. Only works when [`TnuaBuiltinWalk`] is the
14/// [basis](crate::TnuaBasis).
15///
16/// Most of the fields have sane defaults - the only field that must be set is
17/// [`float_offset`](Self::float_offset), which controls how low the character will crouch
18/// (compared to its regular float offset while standing). That field should typically have a
19/// negative value.
20///
21/// If the player stops crouching while crawling under an obstacle, Tnua will push the character
22/// upward toward the obstacle - which will bring about undesired physics behavior (especially if
23/// the player tries to move). To prevent that, use this action together with
24/// [`TnuaCrouchEnforcer`](crate::control_helpers::TnuaCrouchEnforcer).
25#[derive(Clone, Debug)]
26pub struct TnuaBuiltinCrouch {
27    /// Controls how low the character will crouch, compared to its regular float offset while
28    /// standing.
29    ///
30    /// This field should typically have a negative value. A positive value will cause the
31    /// character to "crouch" upward - which may be an interesting gameplay action, but not
32    /// what one would call a "crouch".
33    pub float_offset: Float,
34
35    /// A duration, in seconds, that it should take for the character to change its floating height
36    /// to start or stop the crouch.
37    ///
38    /// Set this to more than the expected duration of a single frame, so that the character will
39    /// some distance for the
40    /// [`spring_dampening`](crate::builtins::TnuaBuiltinWalk::spring_dampening) force to reduce
41    /// its vertical velocity.
42    pub height_change_impulse_for_duration: Float,
43
44    /// The maximum impulse to apply when starting or stopping the crouch.
45    pub height_change_impulse_limit: Float,
46
47    /// If set to `true`, this action will not yield to other action who try to take control.
48    ///
49    /// For example - if the player holds the crouch button, and then hits the jump button while
50    /// the crouch button is still pressed, the character will jump if `uncancellable` is `false`.
51    /// But if `uncancellable` is `true`, the character will stay crouched, ignoring the jump
52    /// action.
53    pub uncancellable: bool,
54}
55
56impl Default for TnuaBuiltinCrouch {
57    fn default() -> Self {
58        Self {
59            float_offset: 0.0,
60            height_change_impulse_for_duration: 0.02,
61            height_change_impulse_limit: 40.0,
62            uncancellable: false,
63        }
64    }
65}
66
67impl TnuaAction for TnuaBuiltinCrouch {
68    const NAME: &'static str = "TnuaBuiltinCrouch";
69    type State = TnuaBuiltinCrouchState;
70    const VIOLATES_COYOTE_TIME: bool = false;
71
72    fn initiation_decision(
73        &self,
74        ctx: TnuaActionContext,
75        _being_fed_for: &bevy::time::Stopwatch,
76    ) -> TnuaActionInitiationDirective {
77        if ctx.proximity_sensor.output.is_some() {
78            TnuaActionInitiationDirective::Allow
79        } else {
80            TnuaActionInitiationDirective::Delay
81        }
82    }
83
84    fn apply(
85        &self,
86        state: &mut Self::State,
87        ctx: TnuaActionContext,
88        lifecycle_status: TnuaActionLifecycleStatus,
89        motor: &mut TnuaMotor,
90    ) -> TnuaActionLifecycleDirective {
91        let Some((walk_basis, walk_state)) = ctx.concrete_basis::<TnuaBuiltinWalk>() else {
92            error!("Cannot crouch - basis is not TnuaBuiltinWalk");
93            return TnuaActionLifecycleDirective::Finished;
94        };
95        let Some(sensor_output) = &ctx.proximity_sensor.output else {
96            return TnuaActionLifecycleDirective::Reschedule { after_seconds: 0.0 };
97        };
98        let spring_offset_up = walk_basis.float_height - sensor_output.proximity.adjust_precision();
99        let spring_offset_down =
100            spring_offset_up.adjust_precision() + self.float_offset.adjust_precision();
101
102        match lifecycle_status {
103            TnuaActionLifecycleStatus::Initiated => {}
104            TnuaActionLifecycleStatus::CancelledFrom => {}
105            TnuaActionLifecycleStatus::StillFed => {}
106            TnuaActionLifecycleStatus::NoLongerFed => {
107                *state = TnuaBuiltinCrouchState::Rising;
108            }
109            TnuaActionLifecycleStatus::CancelledInto => {
110                if !self.uncancellable {
111                    *state = TnuaBuiltinCrouchState::Rising;
112                }
113            }
114        }
115
116        let spring_force = |spring_offset: Float| -> TnuaVelChange {
117            walk_basis.spring_force(walk_state, &ctx.as_basis_context(), spring_offset)
118        };
119
120        let impulse_or_spring_force = |spring_offset: Float| -> TnuaVelChange {
121            let spring_force = spring_force(spring_offset);
122            let spring_force_boost = crate::util::calc_boost(&spring_force, ctx.frame_duration);
123            let impulse_boost = self.impulse_boost(spring_offset);
124            if spring_force_boost.length_squared() < impulse_boost.powi(2) {
125                TnuaVelChange::boost(impulse_boost * ctx.up_direction.adjust_precision())
126            } else {
127                spring_force
128            }
129        };
130
131        let mut set_vel_change = |vel_change: TnuaVelChange| {
132            motor
133                .lin
134                .cancel_on_axis(ctx.up_direction.adjust_precision());
135            motor.lin += vel_change;
136        };
137
138        match state {
139            TnuaBuiltinCrouchState::Sinking => {
140                if spring_offset_down < -0.01 {
141                    set_vel_change(impulse_or_spring_force(spring_offset_down));
142                } else {
143                    *state = TnuaBuiltinCrouchState::Maintaining;
144                    set_vel_change(spring_force(spring_offset_down));
145                }
146                lifecycle_status.directive_simple()
147            }
148            TnuaBuiltinCrouchState::Maintaining => {
149                set_vel_change(spring_force(spring_offset_down));
150                // If it's finished/cancelled, something else should changed its state
151                TnuaActionLifecycleDirective::StillActive
152            }
153            TnuaBuiltinCrouchState::Rising => {
154                if 0.01 < spring_offset_up {
155                    set_vel_change(impulse_or_spring_force(spring_offset_up));
156
157                    if matches!(lifecycle_status, TnuaActionLifecycleStatus::CancelledInto) {
158                        // Don't finish the rise - just do the other action
159                        TnuaActionLifecycleDirective::Reschedule { after_seconds: 0.0 }
160                    } else {
161                        // Finish the rise
162                        TnuaActionLifecycleDirective::StillActive
163                    }
164                } else {
165                    TnuaActionLifecycleDirective::Finished
166                }
167            }
168        }
169    }
170}
171
172impl TnuaBuiltinCrouch {
173    fn impulse_boost(&self, spring_offset: Float) -> Float {
174        let velocity_to_get_to_new_float_height =
175            spring_offset / self.height_change_impulse_for_duration;
176        velocity_to_get_to_new_float_height.clamp(
177            -self.height_change_impulse_limit,
178            self.height_change_impulse_limit,
179        )
180    }
181}
182
183#[derive(Default, Debug, Clone)]
184pub enum TnuaBuiltinCrouchState {
185    /// The character is transitioning from standing to crouching.
186    #[default]
187    Sinking,
188    /// The character is currently crouched.
189    Maintaining,
190    /// The character is transitioning from crouching to standing.
191    Rising,
192}
193
194impl TnuaCrouchEnforcedAction for TnuaBuiltinCrouch {
195    fn range_to_cast_up(&self, _state: &Self::State) -> Float {
196        -self.float_offset
197    }
198
199    fn prevent_cancellation(&mut self) {
200        self.uncancellable = true;
201    }
202}