// ▄████████▄ + ███ + ▄█████████ ███ + // ███▀ ▀███ + + ███ ███▀ + ███ + + // ███ + ███ ███ ███ █████████ ███ ███ ███ ███ // ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███ // ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███ // ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███ // ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████ // + + + ███ // + ▀████████████████████████████████████████████████████▀ // // This module manages sound effects and music. use crate::prelude::*; use bevy::audio::{PlaybackMode, Volume}; use bevy::prelude::*; use std::collections::HashMap; pub struct AudioPlugin; impl Plugin for AudioPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup); app.add_systems( Update, ( play_zoom_sfx, play_gasp_sfx, respawn_sinks.run_if(on_event::()), pause_all.run_if(on_event::()), ), ); app.add_systems( PostUpdate, ( play_sfx, toggle_music.run_if(on_event::()), ), ); app.add_event::(); app.add_event::(); app.add_event::(); app.add_event::(); app.insert_resource(ZoomTimer(Timer::from_seconds(0.09, TimerMode::Repeating))); } } #[derive(Resource)] pub struct ZoomTimer(Timer); const PATHS: &[(SfxType, Sfx, &str)] = &[ ( SfxType::Radio, Sfx::BGM, "music/Aleksey Chistilin - Cinematic Cello.ogg", ), ( SfxType::Radio, Sfx::BGMActualJupiterRecording, "music/JupiterRecording.ogg", ), ( SfxType::LoopSfx, Sfx::ElectricMotor, "sounds/electricmotor.ogg", ), (SfxType::LoopSfx, Sfx::Ion, "sounds/ion.ogg"), (SfxType::LoopSfx, Sfx::Thruster, "sounds/thruster.ogg"), (SfxType::LoopSfx, Sfx::Gasp, "sounds/gasp.ogg"), (SfxType::OneOff, Sfx::GaspRelief, "sounds/gasprelief.ogg"), (SfxType::OneOff, Sfx::Achieve, "sounds/achieve.ogg"), (SfxType::OneOff, Sfx::Click, "sounds/click.ogg"), (SfxType::OneOff, Sfx::Connect, "sounds/connect.ogg"), (SfxType::OneOff, Sfx::Crash, "sounds/crash.ogg"), (SfxType::OneOff, Sfx::EnterVehicle, "sounds/bikestart.ogg"), ( SfxType::OneOff, Sfx::IncomingChatMessage, "sounds/connect.ogg", ), (SfxType::OneOff, Sfx::Ping, "sounds/connect.ogg"), ( SfxType::OneOff, Sfx::Switch, "sounds/typosonic-typing-192811-crop.ogg", ), (SfxType::OneOff, Sfx::WakeUp, "sounds/wakeup.ogg"), (SfxType::OneOff, Sfx::Woosh, "sounds/woosh.ogg"), (SfxType::OneOff, Sfx::Zoom, "sounds/zoom.ogg"), ]; #[derive(Component, PartialEq, Hash, Eq, Copy, Clone)] pub enum Sfx { Achieve, BGM, BGMActualJupiterRecording, Click, Connect, Crash, ElectricMotor, EnterVehicle, Gasp, GaspRelief, IncomingChatMessage, Ion, Ping, Switch, Thruster, WakeUp, Woosh, Zoom, } pub fn str2sfx(sfx_label: &str) -> Sfx { return match sfx_label { "achieve" => Sfx::Achieve, "switch" => Sfx::Switch, "click" => Sfx::Click, "woosh" => Sfx::Woosh, "zoom" => Sfx::Zoom, "chat" => Sfx::IncomingChatMessage, "ping" => Sfx::Ping, "connect" => Sfx::Connect, "entervehicle" => Sfx::EnterVehicle, "crash" => Sfx::Ping, _ => Sfx::Click, }; } pub enum SfxType { Radio, LoopSfx, OneOff, } #[derive(Event)] pub struct PlaySfxEvent(pub Sfx); #[derive(Event)] pub struct PauseAllSfxEvent; #[derive(Event)] pub struct RespawnSinksEvent; #[derive(Event)] pub struct ToggleMusicEvent(); #[derive(Resource)] pub struct Sounds(HashMap>); pub fn setup( mut commands: Commands, asset_server: Res, mut ew_respawnsinks: EventWriter, ) { let mut map = HashMap::new(); for (_, sfx, path) in PATHS { let source = asset_server.load(*path); map.insert(*sfx, source.clone()); } commands.insert_resource(Sounds(map)); ew_respawnsinks.send(RespawnSinksEvent); } pub fn respawn_sinks( mut commands: Commands, asset_server: Res, q_audiosinks: Query, With)>, settings: Res, ) { for sink in &q_audiosinks { commands.entity(sink).despawn(); } for (sfxtype, sfx, path) in PATHS { let source = asset_server.load(*path); match sfxtype { SfxType::Radio => { commands.spawn(( *sfx, AudioBundle { source, settings: PlaybackSettings { mode: PlaybackMode::Loop, paused: !settings.is_radio_playing(*sfx).unwrap_or(true), ..default() }, }, )); } SfxType::LoopSfx => { commands.spawn(( *sfx, AudioBundle { source, settings: PlaybackSettings { mode: PlaybackMode::Loop, volume: Volume::new(0.0), paused: true, ..default() }, }, )); } SfxType::OneOff => (), } } } pub fn play_sfx( mut commands: Commands, settings: Res, mut events_sfx: EventReader, sounds: Res, ) { if settings.mute_sfx && !events_sfx.is_empty() { events_sfx.clear(); } for sfx in events_sfx.read() { if let Some(source) = sounds.0.get(&sfx.0) { commands.spawn(AudioBundle { source: source.clone(), settings: PlaybackSettings::DESPAWN, }); } } } pub fn toggle_music(q_audiosinks: Query<(&AudioSink, &Sfx)>, settings: Res) { for (bgm_sink, sfx) in &q_audiosinks { if let Some(play) = settings.is_radio_playing(*sfx) { if play { bgm_sink.play(); } else { bgm_sink.pause(); } } } } pub fn play_zoom_sfx( time: Res