foxtrot 0.1.0

A 3D reference project and tech demo for the Bevy Engine.
use std::time::Duration;

use avian3d::prelude::LinearVelocity;
use bevy::prelude::*;
#[cfg(feature = "hot_patch")]
use bevy_simple_subsecond_system::hot;
use bevy_tnua::{builtins::TnuaBuiltinJumpState, prelude::*};

use crate::{PostPhysicsAppSystems, audio::sound_effect, screens::Screen};

use super::{Player, assets::PlayerAssets};

pub(super) fn plugin(app: &mut App) {
    app.add_systems(
        Update,
        (play_jump_grunt, play_step_sound, play_land_sound)
            .run_if(in_state(Screen::Gameplay))
            .in_set(PostPhysicsAppSystems::PlaySounds),
    );
}

#[cfg_attr(feature = "hot_patch", hot)]
fn play_jump_grunt(
    mut commands: Commands,
    player: Single<&TnuaController, With<Player>>,
    mut player_assets: ResMut<PlayerAssets>,
    mut is_jumping: Local<bool>,
    mut sound_cooldown: Local<Option<Timer>>,
    time: Res<Time>,
) {
    let sound_cooldown = sound_cooldown
        .get_or_insert_with(|| Timer::new(Duration::from_millis(1000), TimerMode::Once));
    sound_cooldown.tick(time.delta());

    if player
        .concrete_action::<TnuaBuiltinJump>()
        .is_none_or(|x| matches!(x, (_, TnuaBuiltinJumpState::FallSection)))
    {
        *is_jumping = false;
        return;
    }
    if *is_jumping {
        return;
    }
    *is_jumping = true;

    if sound_cooldown.finished() {
        let rng = &mut rand::thread_rng();
        let grunt = player_assets.jump_grunts.pick(rng).clone();
        let jump_start = player_assets.jump_start_sounds.pick(rng).clone();

        commands.spawn(sound_effect(grunt));
        commands.spawn(sound_effect(jump_start));
        sound_cooldown.reset();
    }
}

#[cfg_attr(feature = "hot_patch", hot)]
fn play_step_sound(
    mut commands: Commands,
    player: Single<(&TnuaController, &LinearVelocity), With<Player>>,
    mut player_assets: ResMut<PlayerAssets>,
    time: Res<Time>,
    mut timer: Local<Option<Timer>>,
) {
    let timer =
        timer.get_or_insert_with(|| Timer::new(Duration::from_millis(300), TimerMode::Repeating));
    timer.tick(time.delta());
    if !timer.finished() {
        return;
    }

    let (controller, linear_velocity) = player.into_inner();
    if controller.is_airborne().unwrap_or(true) {
        return;
    }
    if linear_velocity.length_squared() < 5.0 {
        return;
    }
    let rng = &mut rand::thread_rng();
    let sound = player_assets.steps.pick(rng).clone();
    commands.spawn(sound_effect(sound));
}

#[cfg_attr(feature = "hot_patch", hot)]
fn play_land_sound(
    mut commands: Commands,
    player: Single<&TnuaController, With<Player>>,
    mut player_assets: ResMut<PlayerAssets>,
    mut was_airborne: Local<bool>,
) {
    let is_airborne = player.is_airborne().unwrap_or(true);
    if is_airborne {
        *was_airborne = true;
        return;
    }
    if !*was_airborne {
        return;
    }
    *was_airborne = false;

    let rng = &mut rand::thread_rng();
    let sound = player_assets.land_sounds.pick(rng).clone();
    commands.spawn(sound_effect(sound));
}