bevy_tnua/builtins/
crouch.rs

1use bevy::time::Stopwatch;
2use serde::{Deserialize, Serialize};
3
4use crate::basis_capabilities::{
5    TnuaBasisWithFloating, TnuaBasisWithGround, TnuaBasisWithHeadroom, TnuaBasisWithSpring,
6};
7use crate::{TnuaAction, TnuaActionContext, TnuaBasis};
8use crate::{
9    TnuaActionInitiationDirective, TnuaActionLifecycleDirective, TnuaActionLifecycleStatus, math::*,
10};
11use crate::{TnuaMotor, TnuaVelChange};
12
13#[derive(Debug, Default)]
14#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
15pub struct TnuaBuiltinCrouch;
16
17#[derive(Clone, Serialize, Deserialize)]
18pub struct TnuaBuiltinCrouchConfig {
19    /// Controls how low the character will crouch, compared to its regular float offset while
20    /// standing.
21    ///
22    /// This field should typically have a negative value. A positive value will cause the
23    /// character to "crouch" upward - which may be an interesting gameplay action, but not
24    /// what one would call a "crouch".
25    pub float_offset: Float,
26
27    /// A duration, in seconds, that it should take for the character to change its floating height
28    /// to start or stop the crouch.
29    ///
30    /// Set this to more than the expected duration of a single frame, so that the character will
31    /// some distance for the
32    /// [`spring_dampening`](crate::builtins::TnuaBuiltinWalkConfig::spring_dampening) force to
33    /// reduce its vertical velocity.
34    pub height_change_impulse_for_duration: Float,
35
36    /// The maximum impulse to apply when starting or stopping the crouch.
37    pub height_change_impulse_limit: Float,
38}
39
40impl Default for TnuaBuiltinCrouchConfig {
41    fn default() -> Self {
42        Self {
43            float_offset: 0.0,
44            height_change_impulse_for_duration: 0.02,
45            height_change_impulse_limit: 40.0,
46        }
47    }
48}
49
50#[derive(Default, Debug)]
51#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
52pub enum TnuaBuiltinCrouchMemory {
53    /// The character is transitioning from standing to crouching.
54    #[default]
55    Sinking,
56    /// The character is currently crouched.
57    Maintaining,
58    /// The character is transitioning from crouching to standing.
59    Rising,
60}
61
62impl<B: TnuaBasis> TnuaAction<B> for TnuaBuiltinCrouch
63where
64    B: TnuaBasisWithFloating,
65    B: TnuaBasisWithSpring,
66    B: TnuaBasisWithGround,
67    B: TnuaBasisWithHeadroom,
68{
69    type Config = TnuaBuiltinCrouchConfig;
70    type Memory = TnuaBuiltinCrouchMemory;
71
72    fn initiation_decision(
73        &self,
74        _config: &Self::Config,
75        sensors: &B::Sensors<'_>,
76        _ctx: TnuaActionContext<B>,
77        _being_fed_for: &Stopwatch,
78    ) -> TnuaActionInitiationDirective {
79        if B::ground_sensor(sensors).output.is_some() {
80            TnuaActionInitiationDirective::Allow
81        } else {
82            TnuaActionInitiationDirective::Delay
83        }
84    }
85
86    fn apply(
87        &self,
88        config: &Self::Config,
89        memory: &mut Self::Memory,
90        sensors: &B::Sensors<'_>,
91        ctx: TnuaActionContext<B>,
92        lifecycle_status: TnuaActionLifecycleStatus,
93        motor: &mut TnuaMotor,
94    ) -> crate::TnuaActionLifecycleDirective {
95        let Some(sensor_output) = &B::ground_sensor(sensors).output else {
96            return TnuaActionLifecycleDirective::Reschedule { after_seconds: 0.0 };
97        };
98        let spring_offset_up =
99            B::float_height(ctx.basis) - sensor_output.proximity.adjust_precision();
100        let spring_offset_down =
101            spring_offset_up.adjust_precision() + config.float_offset.adjust_precision();
102
103        match lifecycle_status {
104            TnuaActionLifecycleStatus::Initiated => {}
105            TnuaActionLifecycleStatus::CancelledFrom => {}
106            TnuaActionLifecycleStatus::StillFed => {}
107            TnuaActionLifecycleStatus::NoLongerFed => {
108                *memory = Self::Memory::Rising;
109            }
110            TnuaActionLifecycleStatus::CancelledInto => {
111                *memory = TnuaBuiltinCrouchMemory::Rising;
112            }
113        }
114
115        let can_stand = B::headroom_intrusion(ctx.basis, sensors)
116            .map(|headroom_intrusion| {
117                spring_offset_up < headroom_intrusion.end - headroom_intrusion.start
118            })
119            .unwrap_or(true);
120
121        if !can_stand && matches!(memory, TnuaBuiltinCrouchMemory::Rising) {
122            *memory = TnuaBuiltinCrouchMemory::Maintaining;
123        }
124
125        let spring_force = |spring_offset: Float| -> TnuaVelChange {
126            B::spring_force(ctx.basis, &ctx.as_basis_context(), spring_offset)
127        };
128
129        let impulse_or_spring_force = |spring_offset: Float| -> TnuaVelChange {
130            let spring_force = spring_force(spring_offset);
131            let spring_force_boost = crate::util::calc_boost(&spring_force, ctx.frame_duration);
132            let impulse_boost = config.impulse_boost(spring_offset);
133            if spring_force_boost.length_squared() < impulse_boost.powi(2) {
134                TnuaVelChange::boost(impulse_boost * ctx.up_direction.adjust_precision())
135            } else {
136                spring_force
137            }
138        };
139
140        let mut set_vel_change = |vel_change: TnuaVelChange| {
141            motor
142                .lin
143                .cancel_on_axis(ctx.up_direction.adjust_precision());
144            motor.lin += vel_change;
145        };
146
147        match memory {
148            TnuaBuiltinCrouchMemory::Sinking => {
149                if spring_offset_down < -0.01 {
150                    set_vel_change(impulse_or_spring_force(spring_offset_down));
151                } else {
152                    *memory = TnuaBuiltinCrouchMemory::Maintaining;
153                    set_vel_change(spring_force(spring_offset_down));
154                }
155                lifecycle_status.directive_simple()
156            }
157            TnuaBuiltinCrouchMemory::Maintaining => {
158                set_vel_change(spring_force(spring_offset_down));
159                // If it's finished/cancelled, something else should changed its state
160                TnuaActionLifecycleDirective::StillActive
161            }
162            TnuaBuiltinCrouchMemory::Rising => {
163                if 0.01 < spring_offset_up {
164                    set_vel_change(impulse_or_spring_force(spring_offset_up));
165
166                    if matches!(lifecycle_status, TnuaActionLifecycleStatus::CancelledInto) {
167                        // Don't finish the rise - just do the other action
168                        TnuaActionLifecycleDirective::Reschedule { after_seconds: 0.0 }
169                    } else {
170                        // Finish the rise
171                        TnuaActionLifecycleDirective::StillActive
172                    }
173                } else {
174                    TnuaActionLifecycleDirective::Finished
175                }
176            }
177        }
178    }
179
180    fn influence_basis(
181        &self,
182        config: &Self::Config,
183        _memory: &Self::Memory,
184        _ctx: crate::TnuaBasisContext,
185        _basis_input: &B,
186        _basis_config: &<B as TnuaBasis>::Config,
187        basis_memory: &mut <B as TnuaBasis>::Memory,
188    ) {
189        B::set_extra_headroom(basis_memory, -config.float_offset);
190    }
191}
192
193impl TnuaBuiltinCrouchConfig {
194    fn impulse_boost(&self, spring_offset: Float) -> Float {
195        let velocity_to_get_to_new_float_height =
196            spring_offset / self.height_change_impulse_for_duration;
197        velocity_to_get_to_new_float_height.clamp(
198            -self.height_change_impulse_limit,
199            self.height_change_impulse_limit,
200        )
201    }
202}
203
204// TOOD: this
205// impl TnuaCrouchEnforcedAction for TnuaBuiltinCrouch {
206// fn range_to_cast_up(&self, _memory: &Self::Memory) -> Float {
207// -self.float_offset
208// }
209
210// fn prevent_cancellation(&mut self) {
211// self.uncancellable = true;
212// }
213// }