diff --git a/assets/voxygen/audio/sfx.ron b/assets/voxygen/audio/sfx.ron new file mode 100644 index 0000000000..cc91cb96ac --- /dev/null +++ b/assets/voxygen/audio/sfx.ron @@ -0,0 +1,67 @@ +( + items: [ + ( + trigger: Run, + files: [ + "voxygen.audio.sfx.footsteps.stepdirt_1", + "voxygen.audio.sfx.footsteps.stepdirt_2", + "voxygen.audio.sfx.footsteps.stepdirt_3", + "voxygen.audio.sfx.footsteps.stepdirt_4", + "voxygen.audio.sfx.footsteps.stepdirt_5", + "voxygen.audio.sfx.footsteps.stepdirt_6", + "voxygen.audio.sfx.footsteps.stepdirt_7", + "voxygen.audio.sfx.footsteps.stepdirt_8", + ], + threshold: 0.25, + ), + ( + trigger: GliderOpen, + files: [ + "voxygen.audio.sfx.glider_open", + ], + threshold: 0.5, + ), + ( + trigger: GliderClose, + files: [ + "voxygen.audio.sfx.glider_close", + ], + threshold: 0.5, + ), + ( + trigger: Attack(Sword), + files: [ + "voxygen.audio.sfx.weapon.sword", + ], + threshold: 0.5, + ), + ( + trigger: Attack(Hammer), + files: [ + "voxygen.audio.sfx.weapon.sword", + ], + threshold: 0.5, + ), + ( + trigger: Attack(Bow), + files: [ + "voxygen.audio.sfx.weapon.bow", + ], + threshold: 0.5, + ), + ( + trigger: LevelUp, + files: [ + "voxygen.audio.sfx.chat_message_received" + ], + threshold: 0.2, + ), + ( + trigger: InventoryAdd, + files: [ + "voxygen.audio.sfx.inventory_add" + ], + threshold: 0.2, + ), + ], +) \ No newline at end of file diff --git a/assets/voxygen/audio/sfx/blank.wav b/assets/voxygen/audio/sfx/blank.wav new file mode 100644 index 0000000000..15de22c6c6 Binary files /dev/null and b/assets/voxygen/audio/sfx/blank.wav differ diff --git a/assets/voxygen/audio/sfx/chat_message_received.wav b/assets/voxygen/audio/sfx/chat_message_received.wav new file mode 100644 index 0000000000..fcf4ed8324 Binary files /dev/null and b/assets/voxygen/audio/sfx/chat_message_received.wav differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepsnow_1.wav b/assets/voxygen/audio/sfx/footsteps/stepsnow_1.wav deleted file mode 100644 index f6eaf8693c..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepsnow_1.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepsnow_2.wav b/assets/voxygen/audio/sfx/footsteps/stepsnow_2.wav deleted file mode 100644 index ed69cebfbb..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepsnow_2.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepstone_1.wav b/assets/voxygen/audio/sfx/footsteps/stepstone_1.wav deleted file mode 100644 index ca4de9dfcc..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepstone_1.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepstone_2.wav b/assets/voxygen/audio/sfx/footsteps/stepstone_2.wav deleted file mode 100644 index af0aa5f780..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepstone_2.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepstone_3.wav b/assets/voxygen/audio/sfx/footsteps/stepstone_3.wav deleted file mode 100644 index 08328a7588..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepstone_3.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepstone_4.wav b/assets/voxygen/audio/sfx/footsteps/stepstone_4.wav deleted file mode 100644 index 5362d93137..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepstone_4.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepstone_5.wav b/assets/voxygen/audio/sfx/footsteps/stepstone_5.wav deleted file mode 100644 index db6a92a652..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepstone_5.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepstone_6.wav b/assets/voxygen/audio/sfx/footsteps/stepstone_6.wav deleted file mode 100644 index d436b94694..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepstone_6.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepstone_7.wav b/assets/voxygen/audio/sfx/footsteps/stepstone_7.wav deleted file mode 100644 index 0af921a7de..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepstone_7.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepstone_8.wav b/assets/voxygen/audio/sfx/footsteps/stepstone_8.wav deleted file mode 100644 index 78fdf40e0f..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepstone_8.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepwater_1.wav b/assets/voxygen/audio/sfx/footsteps/stepwater_1.wav deleted file mode 100644 index 584dc599a4..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepwater_1.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepwater_2.wav b/assets/voxygen/audio/sfx/footsteps/stepwater_2.wav deleted file mode 100644 index 526f613eac..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepwater_2.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepwood_1.wav b/assets/voxygen/audio/sfx/footsteps/stepwood_1.wav deleted file mode 100644 index a81439012c..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepwood_1.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/footsteps/stepwood_2.wav b/assets/voxygen/audio/sfx/footsteps/stepwood_2.wav deleted file mode 100644 index 9dae7e0189..0000000000 Binary files a/assets/voxygen/audio/sfx/footsteps/stepwood_2.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/glider_close.wav b/assets/voxygen/audio/sfx/glider_close.wav new file mode 100644 index 0000000000..bb3d8e2289 Binary files /dev/null and b/assets/voxygen/audio/sfx/glider_close.wav differ diff --git a/assets/voxygen/audio/sfx/glider_open.wav b/assets/voxygen/audio/sfx/glider_open.wav new file mode 100644 index 0000000000..dabdadc87b Binary files /dev/null and b/assets/voxygen/audio/sfx/glider_open.wav differ diff --git a/assets/voxygen/audio/sfx/inventory_add.wav b/assets/voxygen/audio/sfx/inventory_add.wav new file mode 100644 index 0000000000..49e9a3a9c6 Binary files /dev/null and b/assets/voxygen/audio/sfx/inventory_add.wav differ diff --git a/assets/voxygen/audio/sfx/steps/step_1.wav b/assets/voxygen/audio/sfx/steps/step_1.wav deleted file mode 100644 index 0a911700b7..0000000000 Binary files a/assets/voxygen/audio/sfx/steps/step_1.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/steps/step_2.wav b/assets/voxygen/audio/sfx/steps/step_2.wav deleted file mode 100644 index b485df31d2..0000000000 Binary files a/assets/voxygen/audio/sfx/steps/step_2.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/steps/step_3.wav b/assets/voxygen/audio/sfx/steps/step_3.wav deleted file mode 100644 index 13eb6679e6..0000000000 Binary files a/assets/voxygen/audio/sfx/steps/step_3.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/steps/step_4.wav b/assets/voxygen/audio/sfx/steps/step_4.wav deleted file mode 100644 index 786a042e8d..0000000000 Binary files a/assets/voxygen/audio/sfx/steps/step_4.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/steps/step_5.wav b/assets/voxygen/audio/sfx/steps/step_5.wav deleted file mode 100644 index e4a23dbb6c..0000000000 Binary files a/assets/voxygen/audio/sfx/steps/step_5.wav and /dev/null differ diff --git a/assets/voxygen/audio/sfx/weapon/bow.wav b/assets/voxygen/audio/sfx/weapon/bow.wav new file mode 100644 index 0000000000..eea273ee1b Binary files /dev/null and b/assets/voxygen/audio/sfx/weapon/bow.wav differ diff --git a/assets/voxygen/audio/sfx/weapon/sword.wav b/assets/voxygen/audio/sfx/weapon/sword.wav new file mode 100644 index 0000000000..8ee2a0d70c Binary files /dev/null and b/assets/voxygen/audio/sfx/weapon/sword.wav differ diff --git a/common/src/event.rs b/common/src/event.rs index 70b09d9b37..a075ba4cdd 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -1,10 +1,53 @@ use crate::comp; +use comp::item::Tool; use parking_lot::Mutex; +use serde::Deserialize; use specs::Entity as EcsEntity; use sphynx::Uid; use std::{collections::VecDeque, ops::DerefMut}; use vek::*; +pub struct SfxEventItem { + pub sfx: SfxEvent, + pub pos: Option>, +} + +impl SfxEventItem { + pub fn new(sfx: SfxEvent, pos: Option>) -> Self { + Self { sfx, pos } + } + + pub fn at_player_position(sfx: SfxEvent) -> Self { + Self { sfx, pos: None } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)] +pub enum SfxEvent { + Idle, + PlaceBlock, + RemoveBlock, + OpenChest, + ChatMessageReceived, + OpenBag, + LevelUp, + Roll, + Climb, + Swim, + Run, + GliderOpen, + Glide, + GliderClose, + Jump, + Fall, + InventoryAdd, + InventoryDrop, + LightLantern, + ExtinguishLantern, + Attack(Tool), + AttackWolf, +} + pub enum LocalEvent { Jump(EcsEntity), WallLeap { diff --git a/common/src/npc.rs b/common/src/npc.rs index 0c0ebe25d5..161217e5e7 100644 --- a/common/src/npc.rs +++ b/common/src/npc.rs @@ -5,7 +5,7 @@ use serde_json; use std::str::FromStr; use std::sync::Arc; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] pub enum NpcKind { Humanoid, Wolf, diff --git a/common/src/state.rs b/common/src/state.rs index 640b283520..9a3b5cd9ee 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -3,7 +3,7 @@ pub use sphynx::Uid; use crate::{ comp, - event::{EventBus, LocalEvent, ServerEvent}, + event::{EventBus, LocalEvent, ServerEvent, SfxEventItem}, msg::{EcsCompPacket, EcsResPacket}, region::RegionMap, sys, @@ -173,6 +173,7 @@ impl State { ecs.add_resource(TerrainChanges::default()); ecs.add_resource(EventBus::::default()); ecs.add_resource(EventBus::::default()); + ecs.add_resource(EventBus::::default()); ecs.add_resource(RegionMap::new()); } diff --git a/common/src/sys/stats.rs b/common/src/sys/stats.rs index e05b3f9e67..448bb375a0 100644 --- a/common/src/sys/stats.rs +++ b/common/src/sys/stats.rs @@ -1,26 +1,31 @@ use crate::{ comp::{HealthSource, Stats}, - event::{EventBus, ServerEvent}, + event::{EventBus, ServerEvent, SfxEvent, SfxEventItem}, state::DeltaTime, }; use specs::{Entities, Join, Read, System, WriteStorage}; /// This system kills players +/// and handles players levelling up pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( Entities<'a>, Read<'a, DeltaTime>, Read<'a, EventBus>, + Read<'a, EventBus>, WriteStorage<'a, Stats>, ); - fn run(&mut self, (entities, dt, event_bus, mut stats): Self::SystemData) { - let mut event_emitter = event_bus.emitter(); + fn run( + &mut self, + (entities, dt, server_event_bus, audio_event_bus, mut stats): Self::SystemData, + ) { + let mut server_event_emitter = server_event_bus.emitter(); for (entity, mut stat) in (&entities, &mut stats).join() { if stat.should_die() && !stat.is_dead { - event_emitter.emit(ServerEvent::Destroy { + server_event_emitter.emit(ServerEvent::Destroy { entity, cause: stat.health.last_change.1.cause, }); @@ -36,6 +41,11 @@ impl<'a> System<'a> for Sys { stat.exp.change_maximum_by(25); stat.level.change_by(1); } + + audio_event_bus + .emitter() + .emit(SfxEventItem::at_player_position(SfxEvent::LevelUp)); + stat.update_max_hp(); stat.health .set_to(stat.health.maximum(), HealthSource::LevelUp) diff --git a/voxygen/src/audio/mod.rs b/voxygen/src/audio/mod.rs index 3ec8119924..4aa56d4426 100644 --- a/voxygen/src/audio/mod.rs +++ b/voxygen/src/audio/mod.rs @@ -1,6 +1,8 @@ pub mod channel; pub mod fader; +pub mod sfx; pub mod soundcache; + use channel::{AudioType, Channel}; use fader::Fader; use soundcache::SoundCache; diff --git a/voxygen/src/audio/sfx/event_mapper.rs b/voxygen/src/audio/sfx/event_mapper.rs new file mode 100644 index 0000000000..656c303eea --- /dev/null +++ b/voxygen/src/audio/sfx/event_mapper.rs @@ -0,0 +1,443 @@ +/// sfx::event_mapper watches the local entities and determines which sfx to emit, +/// and the position at which the sound should be emitted from +use crate::audio::sfx::{SfxTriggerItem, SfxTriggers}; + +use client::Client; +use common::{ + comp::{ActionState, Body, CharacterState, ItemKind, MovementState, Pos, Stats, Vel}, + event::{EventBus, SfxEvent, SfxEventItem}, +}; +use hashbrown::HashMap; +use specs::{Entity as EcsEntity, Join}; +use std::time::{Duration, Instant}; +use vek::*; + +#[derive(Clone)] +struct LastSfxEvent { + event: SfxEvent, + time: Instant, +} + +pub struct SfxEventMapper { + event_history: HashMap, +} + +impl SfxEventMapper { + pub fn new() -> Self { + Self { + event_history: HashMap::new(), + } + } + + pub fn maintain(&mut self, client: &Client, triggers: &SfxTriggers) { + const SFX_DIST_LIMIT_SQR: f32 = 22500.0; + let ecs = client.state().ecs(); + + let player_position = ecs + .read_storage::() + .get(client.entity()) + .map_or(Vec3::zero(), |pos| pos.0); + + for (entity, pos, body, vel, stats, character) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ecs.read_storage::().maybe(), + ) + .join() + .filter(|(_, e_pos, ..)| { + (e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR + }) + { + if let (pos, body, Some(character), stats, vel) = (pos, body, character, stats, vel) { + let state = self + .event_history + .entry(entity) + .or_insert_with(|| LastSfxEvent { + event: SfxEvent::Idle, + time: Instant::now(), + }); + + let mapped_event = match body { + Body::Humanoid(_) => { + Self::map_character_event(character, state.event.clone(), vel.0, stats) + } + Body::QuadrupedMedium(_) => { + Self::map_quadriped_event(character, state.event.clone(), vel.0, stats) + } + _ => SfxEvent::Idle, + }; + + // Check for SFX config entry for this movement + let sfx_trigger_item: Option<&SfxTriggerItem> = triggers + .items + .iter() + .find(|item| item.trigger == mapped_event); + + if Self::should_emit(state, sfx_trigger_item) { + ecs.read_resource::>() + .emitter() + .emit(SfxEventItem::new(mapped_event, Some(pos.0))); + + // Update the last play time + state.event = mapped_event; + state.time = Instant::now(); + } else { + // Keep the last event, it may not have an SFX trigger but it helps us determine the next one + state.event = mapped_event; + } + } + } + + self.cleanup(client.entity()); + } + + /// As the player explores the world, we track the last event of the nearby entities to determine the correct + /// SFX item to play next based on their activity. `cleanup` will remove entities from event tracking if they + /// have not triggered an event for > n seconds. This prevents stale records from bloating the Map size. + fn cleanup(&mut self, player: EcsEntity) { + const TRACKING_TIMEOUT: u64 = 15; + + let now = Instant::now(); + self.event_history.retain(|entity, event| { + now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT) + || entity.id() == player.id() + }); + } + + /// When specific entity movements are detected, the associated sound (if any) needs to satisfy two conditions to + /// be allowed to play: + /// 1. An sfx.ron entry exists for the movement (we need to know which sound file(s) to play) + /// 2. The sfx has not been played since it's timeout threshold has elapsed, which prevents firing every tick + fn should_emit( + last_play_entry: &LastSfxEvent, + sfx_trigger_item: Option<&SfxTriggerItem>, + ) -> bool { + if let Some(item) = sfx_trigger_item { + if last_play_entry.event == item.trigger { + last_play_entry.time.elapsed().as_secs_f64() >= item.threshold + } else { + true + } + } else { + false + } + } + + /// Voxygen has an existing list of character states via `MovementState::*` and `ActivityState::*` + /// however that list does not provide enough resolution to target specific entity events, such + /// as opening or closing the glider. These methods translate those entity states with some additional + /// data into more specific `SfxEvent`'s which we attach sounds to + fn map_quadriped_event( + current_event: &CharacterState, + previous_event: SfxEvent, + vel: Vec3, + stats: &Stats, + ) -> SfxEvent { + match ( + current_event.movement, + current_event.action, + previous_event, + vel, + stats, + ) { + (_, ActionState::Attack { .. }, _, _, stats) => match stats.name.as_ref() { + "Wolf" => SfxEvent::AttackWolf, + _ => SfxEvent::Idle, + }, + _ => SfxEvent::Idle, + } + } + + fn map_character_event( + current_event: &CharacterState, + previous_event: SfxEvent, + vel: Vec3, + stats: &Stats, + ) -> SfxEvent { + match ( + current_event.movement, + current_event.action, + previous_event, + vel, + stats, + ) { + (MovementState::Roll { .. }, ..) => SfxEvent::Roll, + (MovementState::Climb, ..) => SfxEvent::Climb, + (MovementState::Swim, ..) => SfxEvent::Swim, + (MovementState::Run, ..) => SfxEvent::Run, + (MovementState::Jump, _, previous_event, vel, _) => { + // MovementState::Jump only indicates !on_ground + if previous_event != SfxEvent::Glide { + if vel.z > 0.0 { + SfxEvent::Jump + } else { + SfxEvent::Fall + } + } else { + SfxEvent::GliderClose + } + } + (MovementState::Glide, _, previous_event, ..) => { + if previous_event != SfxEvent::GliderOpen && previous_event != SfxEvent::Glide { + SfxEvent::GliderOpen + } else { + SfxEvent::Glide + } + } + (_, ActionState::Attack { .. }, _, _, stats) => { + match &stats.equipment.main.as_ref().map(|i| &i.kind) { + Some(ItemKind::Tool { kind, .. }) => SfxEvent::Attack(*kind), + _ => SfxEvent::Idle, + } + } + _ => SfxEvent::Idle, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use common::{ + assets, + comp::{item::Tool, ActionState, MovementState, Stats}, + event::SfxEvent, + }; + use std::time::{Duration, Instant}; + + #[test] + fn no_item_config_no_emit() { + let last_sfx_event = LastSfxEvent { + event: SfxEvent::Idle, + time: Instant::now(), + }; + + let result = SfxEventMapper::should_emit(&last_sfx_event, None); + + assert_eq!(result, false); + } + + #[test] + fn config_but_played_since_threshold_no_emit() { + let trigger_item = SfxTriggerItem { + trigger: SfxEvent::Run, + files: vec![String::from("some.path.to.sfx.file")], + threshold: 1.0, + }; + + // Triggered a 'Run' 0 seconds ago + let last_sfx_event = LastSfxEvent { + event: SfxEvent::Run, + time: Instant::now(), + }; + + let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item)); + + assert_eq!(result, false); + } + + #[test] + fn config_and_not_played_since_threshold_emits() { + let trigger_item = SfxTriggerItem { + trigger: SfxEvent::Run, + files: vec![String::from("some.path.to.sfx.file")], + threshold: 0.5, + }; + + let last_sfx_event = LastSfxEvent { + event: SfxEvent::Idle, + time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(), + }; + + let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item)); + + assert_eq!(result, true); + } + + #[test] + fn same_previous_event_elapsed_emits() { + let trigger_item = SfxTriggerItem { + trigger: SfxEvent::Run, + files: vec![String::from("some.path.to.sfx.file")], + threshold: 0.5, + }; + + let last_sfx_event = LastSfxEvent { + event: SfxEvent::Run, + time: Instant::now() + .checked_sub(Duration::from_millis(500)) + .unwrap(), + }; + + let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item)); + + assert_eq!(result, true); + } + + #[test] + fn maps_idle() { + let stats = Stats::new(String::from("Test"), None); + + let result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Stand, + action: ActionState::Idle, + }, + SfxEvent::Idle, + Vec3::zero(), + &stats, + ); + + assert_eq!(result, SfxEvent::Idle); + } + + #[test] + fn maps_run() { + let stats = Stats::new(String::from("Test"), None); + + let result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Run, + action: ActionState::Idle, + }, + SfxEvent::Idle, + Vec3::zero(), + &stats, + ); + + assert_eq!(result, SfxEvent::Run); + } + + #[test] + fn maps_roll() { + let stats = Stats::new(String::from("Test"), None); + + let result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Roll { + time_left: Duration::new(1, 0), + }, + action: ActionState::Idle, + }, + SfxEvent::Run, + Vec3::zero(), + &stats, + ); + + assert_eq!(result, SfxEvent::Roll); + } + + #[test] + fn maps_jump_or_fall() { + let stats = Stats::new(String::from("Test"), None); + + // positive z velocity, the character is on the rise (jumping) + let vel_jumping = Vec3::new(0.0, 0.0, 1.0); + + let positive_result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Jump, + action: ActionState::Idle, + }, + SfxEvent::Idle, + vel_jumping, + &stats, + ); + + assert_eq!(positive_result, SfxEvent::Jump); + + // negative z velocity, the character is on the way down (!jumping) + let vel_falling = Vec3::new(0.0, 0.0, -1.0); + + let negative_result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Jump, + action: ActionState::Idle, + }, + SfxEvent::Idle, + vel_falling, + &stats, + ); + + assert_eq!(negative_result, SfxEvent::Fall); + } + + #[test] + fn maps_glider_open() { + let stats = Stats::new(String::from("Test"), None); + + let result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Glide, + action: ActionState::Idle, + }, + SfxEvent::Jump, + Vec3::zero(), + &stats, + ); + + assert_eq!(result, SfxEvent::GliderOpen); + } + + #[test] + fn maps_glide() { + let stats = Stats::new(String::from("Test"), None); + + let result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Glide, + action: ActionState::Idle, + }, + SfxEvent::Glide, + Vec3::zero(), + &stats, + ); + + assert_eq!(result, SfxEvent::Glide); + } + + #[test] + fn maps_glider_close() { + let stats = Stats::new(String::from("Test"), None); + + let result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Jump, + action: ActionState::Idle, + }, + SfxEvent::Glide, + Vec3::zero(), + &stats, + ); + + assert_eq!(result, SfxEvent::GliderClose); + } + + #[test] + fn maps_attack() { + let stats = Stats::new( + String::from("Test"), + Some(assets::load_expect_cloned( + "common.items.weapons.starter_sword", + )), + ); + + let result = SfxEventMapper::map_character_event( + &CharacterState { + movement: MovementState::Stand, + action: ActionState::Attack { + time_left: Duration::new(1, 0), + applied: true, + }, + }, + SfxEvent::Idle, + Vec3::zero(), + &stats, + ); + + assert_eq!(result, SfxEvent::Attack(Tool::Sword)); + } +} diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs new file mode 100644 index 0000000000..a4007ccfa0 --- /dev/null +++ b/voxygen/src/audio/sfx/mod.rs @@ -0,0 +1,95 @@ +/// The SfxManager listens for SFX events and plays the sound at the provided position +mod event_mapper; + +use crate::audio::AudioFrontend; +use client::Client; +use common::{ + assets, + comp::{Ori, Pos}, + event::{EventBus, SfxEvent, SfxEventItem}, +}; +use serde::Deserialize; +use vek::*; + +#[derive(Deserialize)] +pub struct SfxTriggerItem { + pub trigger: SfxEvent, + pub files: Vec, + pub threshold: f64, +} + +#[derive(Deserialize)] +pub struct SfxTriggers { + pub items: Vec, +} + +pub struct SfxMgr { + triggers: SfxTriggers, + event_mapper: event_mapper::SfxEventMapper, +} + +impl SfxMgr { + pub fn new() -> Self { + Self { + triggers: Self::load_sfx_items(), + event_mapper: event_mapper::SfxEventMapper::new(), + } + } + + pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) { + self.event_mapper.maintain(client, &self.triggers); + let ecs = client.state().ecs(); + + let player_position = ecs + .read_storage::() + .get(client.entity()) + .map_or(Vec3::zero(), |pos| pos.0); + + let player_ori = ecs + .read_storage::() + .get(client.entity()) + .map_or(Vec3::zero(), |pos| pos.0); + + audio.set_listener_pos(&player_position, &player_ori); + + let events = ecs.read_resource::>().recv_all(); + + for event in events { + let position = match event.pos { + Some(pos) => pos, + _ => player_position, + }; + + // Get the SFX config entry for this movement + let sfx_trigger_item: Option<&SfxTriggerItem> = self + .triggers + .items + .iter() + .find(|item| item.trigger == event.sfx); + + if sfx_trigger_item.is_some() { + let item = sfx_trigger_item.expect("Invalid sfx item"); + + let sfx_file = match item.files.len() { + 1 => item + .files + .last() + .expect("Failed to determine sound file for this trigger item."), + _ => { + let rand_step = rand::random::() % item.files.len(); + &item.files[rand_step] + } + }; + + audio.play_sound(sfx_file, position); + } + } + } + + fn load_sfx_items() -> SfxTriggers { + let file = assets::load_file("voxygen.audio.sfx", &["ron"]) + .expect("Failed to load the sfx config file"); + + ron::de::from_reader(file).expect("Error parsing sfx manifest") + } +} diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 41f86ed39a..cbe6e570c8 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1,16 +1,14 @@ pub mod camera; pub mod figure; -pub mod sound; pub mod terrain; use self::{ camera::{Camera, CameraMode}, figure::FigureMgr, - sound::SoundMgr, terrain::Terrain, }; use crate::{ - audio::AudioFrontend, + audio::{sfx::SfxMgr, AudioFrontend}, render::{ create_pp_mesh, create_skybox_mesh, Consts, Globals, Light, Model, PostProcessLocals, PostProcessPipeline, Renderer, Shadow, SkyboxLocals, SkyboxPipeline, @@ -58,7 +56,7 @@ pub struct Scene { select_pos: Option>, figure_mgr: FigureMgr, - sound_mgr: SoundMgr, + sfx_mgr: SfxMgr, } impl Scene { @@ -91,7 +89,7 @@ impl Scene { select_pos: None, figure_mgr: FigureMgr::new(), - sound_mgr: SoundMgr::new(), + sfx_mgr: SfxMgr::new(), } } @@ -287,8 +285,8 @@ impl Scene { // Remove unused figures. self.figure_mgr.clean(client.get_tick()); - // Maintain audio - self.sound_mgr.maintain(audio, client); + // Maintain sfx + self.sfx_mgr.maintain(audio, client); } /// Render the scene using the provided `Renderer`. diff --git a/voxygen/src/scene/sound.rs b/voxygen/src/scene/sound.rs deleted file mode 100644 index 975c12da2d..0000000000 --- a/voxygen/src/scene/sound.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::audio::AudioFrontend; -use client::Client; -use common::comp::{Body, CharacterState, MovementState::*, Ori, Pos}; -use hashbrown::HashMap; -use specs::{Entity as EcsEntity, Join}; -use std::time::Instant; -use vek::*; - -pub struct AnimState { - last_step_sound: Instant, -} - -pub struct SoundMgr { - character_states: HashMap, -} - -impl SoundMgr { - pub fn new() -> Self { - Self { - character_states: HashMap::new(), - } - } - - pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) { - const SFX_DIST_LIMIT_SQR: f32 = 22500.0; - let ecs = client.state().ecs(); - // Get player position. - let player_pos = ecs - .read_storage::() - .get(client.entity()) - .map_or(Vec3::zero(), |pos| pos.0); - - let player_ori = ecs - .read_storage::() - .get(client.entity()) - .map_or(Vec3::zero(), |pos| pos.0); - - audio.set_listener_pos(&player_pos, &player_ori); - - for (entity, pos, body, character) in ( - &ecs.entities(), - &ecs.read_storage::(), - &ecs.read_storage::(), - ecs.read_storage::().maybe(), - ) - .join() - .filter(|(_, e_pos, _, _)| (e_pos.0.distance_squared(player_pos)) < SFX_DIST_LIMIT_SQR) - { - if let (Body::Humanoid(_), Some(character)) = (body, character) { - let state = self - .character_states - .entry(entity) - .or_insert_with(|| AnimState { - last_step_sound: Instant::now(), - }); - - if character.movement == Run && state.last_step_sound.elapsed().as_secs_f64() > 0.25 - { - let rand_step = (rand::random::() % 5) + 1; - audio.play_sound( - &format!("voxygen.audio.sfx.steps.step_{}", rand_step), - pos.0, - ); - state.last_step_sound = Instant::now(); - } - } - } - } -}