bevy-tnua 0.32.0

A floating character controller for Bevy
Documentation
use crate::basis_capabilities::TnuaBasisWithGround;
use crate::math::{AdjustPrecision, AsF32, Float, Vector3};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};

use crate::util::MotionHelper;
use crate::{
    TnuaAction, TnuaActionContext, TnuaActionInitiationDirective, TnuaActionLifecycleDirective,
    TnuaActionLifecycleStatus, TnuaBasis, TnuaMotor,
};

/// The basic dash [action](TnuaAction).
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
pub struct TnuaBuiltinDash {
    /// The direction and distance of the dash.
    ///
    /// The horizontal and vertical components of this vector are multiplied by the
    /// [`horizontal_distance`](TnuaBuiltinDashConfig::horizontal_distance) and
    /// [`vertical_distance`](TnuaBuiltinDashConfig::vertical_distance) configuration fields.
    ///
    /// This input parameter is cached when the action starts. This means that the control system
    /// does not have to make sure the direction reamins the same even if the player changes it
    /// mid-dash.
    pub displacement: Vector3,

    /// Point the negative Z axis of the characetr model in that direction during the dash.
    ///
    /// This input parameter is cached when the action starts. This means that the control system
    /// does not have to make sure the direction reamins the same even if the player changes it
    /// mid-dash.
    pub desired_forward: Option<Dir3>,

    /// Allow this action to start even if the character is not touching ground nor in coyote time.
    pub allow_in_air: bool,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct TnuaBuiltinDashConfig {
    /// The speed the character will move in during the dash.
    pub speed: Float,

    /// Multiplier for the horizontal component of the
    /// [`displacement`](TnuaBuiltinDash::displacement) given in the input.
    pub horizontal_distance: Float,

    /// Multiplier for the vertical component of the
    /// [`displacement`](TnuaBuiltinDash::displacement) given in the input.
    pub vertical_distance: Float,

    /// After the dash, the character will brake until its speed is below that number.
    pub brake_to_speed: Float,

    /// The maximum acceleration when starting the jump.
    pub acceleration: Float,

    /// The maximum acceleration when braking after the jump.
    ///
    /// Irrelevant if [`brake_to_speed`](Self::brake_to_speed) is set to infinity.
    pub brake_acceleration: Float,

    /// A duration, in seconds, where a player can press a dash button before a dash becomes
    /// possible (typically when a character is still in the air and about the land) and the dash
    /// action would still get registered and be executed once the dash is possible.
    pub input_buffer_time: Float,
}

impl Default for TnuaBuiltinDashConfig {
    fn default() -> Self {
        Self {
            speed: 80.0,
            horizontal_distance: 1.0,
            vertical_distance: 1.0,
            brake_to_speed: 20.0,
            acceleration: 400.0,
            brake_acceleration: 200.0,
            input_buffer_time: 0.2,
        }
    }
}

impl<B: TnuaBasis> TnuaAction<B> for TnuaBuiltinDash
where
    B: TnuaBasisWithGround,
{
    type Config = TnuaBuiltinDashConfig;
    type Memory = TnuaBuiltinDashMemory;

    fn initiation_decision(
        &self,
        config: &Self::Config,
        _sensors: &B::Sensors<'_>,
        ctx: crate::TnuaActionContext<B>,
        being_fed_for: &bevy::time::Stopwatch,
    ) -> crate::TnuaActionInitiationDirective {
        if !self.displacement.is_finite() || self.displacement == Vector3::ZERO {
            TnuaActionInitiationDirective::Reject
        } else if self.allow_in_air || !B::is_airborne(ctx.basis) {
            // Either not airborne, or air jumps are allowed
            TnuaActionInitiationDirective::Allow
        } else if (being_fed_for.elapsed().as_secs_f64() as Float) < config.input_buffer_time {
            TnuaActionInitiationDirective::Delay
        } else {
            TnuaActionInitiationDirective::Reject
        }
    }

    fn apply(
        &self,
        config: &Self::Config,
        memory: &mut Self::Memory,
        _sensors: &B::Sensors<'_>,
        ctx: TnuaActionContext<B>,
        _lifecycle_status: TnuaActionLifecycleStatus,
        motor: &mut TnuaMotor,
    ) -> TnuaActionLifecycleDirective {
        // TODO: Once `std::mem::variant_count` gets stabilized, use that instead.
        for _ in 0..3 {
            return match memory {
                TnuaBuiltinDashMemory::PreDash => {
                    let horizontal_displacement = self
                        .displacement
                        .reject_from(ctx.up_direction.adjust_precision());
                    let vertical_displacement = self
                        .displacement
                        .project_onto(ctx.up_direction.adjust_precision());
                    let displacement = config.horizontal_distance * horizontal_displacement
                        + config.vertical_distance * vertical_displacement;
                    let Ok(direction) = Dir3::new(displacement.f32()) else {
                        // Probably unneeded because of the `initiation_decision`, but still
                        return TnuaActionLifecycleDirective::Finished;
                    };
                    *memory = TnuaBuiltinDashMemory::During {
                        direction,
                        destination: ctx.tracker.translation + displacement,
                        desired_forward: self.desired_forward,
                        consider_blocked_if_speed_is_less_than: Float::NEG_INFINITY,
                    };
                    continue;
                }
                TnuaBuiltinDashMemory::During {
                    direction,
                    destination,
                    desired_forward,
                    consider_blocked_if_speed_is_less_than,
                } => {
                    let distance_to_destination = direction
                        .adjust_precision()
                        .dot(*destination - ctx.tracker.translation);
                    if distance_to_destination < 0.0 {
                        *memory = TnuaBuiltinDashMemory::Braking {
                            direction: *direction,
                        };
                        continue;
                    }

                    let current_speed = direction.adjust_precision().dot(ctx.tracker.velocity);
                    if current_speed < *consider_blocked_if_speed_is_less_than {
                        return TnuaActionLifecycleDirective::Finished;
                    }

                    motor.lin = Default::default();
                    motor.lin.acceleration = -ctx.tracker.gravity;
                    motor.lin.boost = (direction.adjust_precision() * config.speed
                        - ctx.tracker.velocity)
                        .clamp_length_max(ctx.frame_duration * config.acceleration);
                    let expected_speed = direction
                        .adjust_precision()
                        .dot(ctx.tracker.velocity + motor.lin.boost);
                    *consider_blocked_if_speed_is_less_than = if current_speed < expected_speed {
                        0.5 * (current_speed + expected_speed)
                    } else {
                        0.5 * current_speed
                    };

                    if let Some(desired_forward) = desired_forward {
                        motor
                            .ang
                            .cancel_on_axis(ctx.up_direction.adjust_precision());
                        motor.ang += ctx.turn_to_direction(*desired_forward, ctx.up_direction);
                    }

                    TnuaActionLifecycleDirective::StillActive
                }
                TnuaBuiltinDashMemory::Braking { direction } => {
                    let remaining_speed = direction.adjust_precision().dot(ctx.tracker.velocity);
                    if remaining_speed <= config.brake_to_speed {
                        TnuaActionLifecycleDirective::Finished
                    } else {
                        motor.lin.boost = -direction.adjust_precision()
                            * (remaining_speed - config.brake_to_speed)
                                .min(config.brake_acceleration);
                        TnuaActionLifecycleDirective::StillActive
                    }
                }
            };
        }
        error!("Tnua could not decide on dash state");
        TnuaActionLifecycleDirective::Finished
    }

    fn influence_basis(
        &self,
        _config: &Self::Config,
        _memory: &Self::Memory,
        _ctx: crate::TnuaBasisContext,
        _basis_input: &B,
        _basis_config: &<B as TnuaBasis>::Config,
        basis_memory: &mut <B as TnuaBasis>::Memory,
    ) {
        B::violate_coyote_time(basis_memory);
    }
}

#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
pub enum TnuaBuiltinDashMemory {
    #[default]
    PreDash,
    During {
        direction: Dir3,
        destination: Vector3,
        desired_forward: Option<Dir3>,
        consider_blocked_if_speed_is_less_than: Float,
    },
    Braking {
        direction: Dir3,
    },
}