diff --git a/assets/voxygen/audio/sfx.ron b/assets/voxygen/audio/sfx.ron index 3a3ca23a26..8310f26c09 100644 --- a/assets/voxygen/audio/sfx.ron +++ b/assets/voxygen/audio/sfx.ron @@ -1,7 +1,6 @@ ( - items: [ - ( - trigger: Run, + { + Run: ( files: [ "voxygen.audio.sfx.footsteps.stepgrass_1", "voxygen.audio.sfx.footsteps.stepgrass_2", @@ -12,40 +11,17 @@ ], threshold: 0.25, ), - ( - trigger: GliderOpen, + GliderOpen: ( files: [ "voxygen.audio.sfx.glider_open", ], threshold: 0.5, ), - ( - trigger: GliderClose, + 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, - ), - ], + } ) \ No newline at end of file diff --git a/common/src/event.rs b/common/src/event.rs index ecdc180fe0..1eebf3f08e 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -29,18 +29,17 @@ pub enum SfxEvent { OpenChest, ChatTellReceived, OpenBag, - LevelUp, + Run, Roll, Climb, Swim, - Run, GliderOpen, Glide, GliderClose, Jump, Fall, - InventoryAdd, - InventoryDrop, + ExperienceGained, + LevelUp, LightLantern, ExtinguishLantern, Attack(Tool), diff --git a/voxygen/src/audio/sfx/event_mapper/mod.rs b/voxygen/src/audio/sfx/event_mapper/mod.rs new file mode 100644 index 0000000000..e195d1cba2 --- /dev/null +++ b/voxygen/src/audio/sfx/event_mapper/mod.rs @@ -0,0 +1,27 @@ +pub mod movement; +pub mod progression; + +use movement::MovementEventMapper; +use progression::ProgressionEventMapper; + +use super::SfxTriggers; +use client::Client; + +pub struct SfxEventMapper { + progression_event_mapper: ProgressionEventMapper, + movement_event_mapper: MovementEventMapper, +} + +impl SfxEventMapper { + pub fn new() -> Self { + Self { + progression_event_mapper: ProgressionEventMapper::new(), + movement_event_mapper: MovementEventMapper::new(), + } + } + + pub fn maintain(&mut self, client: &Client, triggers: &SfxTriggers) { + self.progression_event_mapper.maintain(client, triggers); + self.movement_event_mapper.maintain(client, triggers); + } +} diff --git a/voxygen/src/audio/sfx/event_mapper.rs b/voxygen/src/audio/sfx/event_mapper/movement.rs similarity index 81% rename from voxygen/src/audio/sfx/event_mapper.rs rename to voxygen/src/audio/sfx/event_mapper/movement.rs index 9a6f7c3d92..114a785390 100644 --- a/voxygen/src/audio/sfx/event_mapper.rs +++ b/voxygen/src/audio/sfx/event_mapper/movement.rs @@ -1,5 +1,5 @@ -/// sfx::event_mapper watches the local entities and determines which sfx to emit, -/// and the position at which the sound should be emitted from +/// event_mapper::movement watches all local entities movements 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; @@ -18,11 +18,11 @@ struct LastSfxEvent { time: Instant, } -pub struct SfxEventMapper { +pub struct MovementEventMapper { event_history: HashMap, } -impl SfxEventMapper { +impl MovementEventMapper { pub fn new() -> Self { Self { event_history: HashMap::new(), @@ -68,12 +68,7 @@ impl SfxEventMapper { }; // 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) { + if Self::should_emit(state, triggers.get_key_value(&mapped_event)) { ecs.read_resource::>() .emitter() .emit(SfxEventItem::new(mapped_event, Some(pos.0))); @@ -110,10 +105,10 @@ impl SfxEventMapper { /// 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>, + sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>, ) -> bool { - if let Some(item) = sfx_trigger_item { - if last_play_entry.event == item.trigger { + if let Some((event, item)) = sfx_trigger_item { + if &last_play_entry.event == event { last_play_entry.time.elapsed().as_secs_f64() >= item.threshold } else { true @@ -147,6 +142,13 @@ impl SfxEventMapper { SfxEvent::Glide } } + (MovementState::Stand, _, previous_event) => { + if previous_event == SfxEvent::Glide { + SfxEvent::GliderClose + } else { + SfxEvent::Idle + } + } _ => SfxEvent::Idle, } } @@ -168,15 +170,16 @@ mod tests { time: Instant::now(), }; - let result = SfxEventMapper::should_emit(&last_sfx_event, None); + let result = MovementEventMapper::should_emit(&last_sfx_event, None); assert_eq!(result, false); } #[test] fn config_but_played_since_threshold_no_emit() { + let event = SfxEvent::Run; + let trigger_item = SfxTriggerItem { - trigger: SfxEvent::Run, files: vec![String::from("some.path.to.sfx.file")], threshold: 1.0, }; @@ -187,15 +190,17 @@ mod tests { time: Instant::now(), }; - let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item)); + let result = + MovementEventMapper::should_emit(&last_sfx_event, Some((&event, &trigger_item))); assert_eq!(result, false); } #[test] fn config_and_not_played_since_threshold_emits() { + let event = SfxEvent::Run; + let trigger_item = SfxTriggerItem { - trigger: SfxEvent::Run, files: vec![String::from("some.path.to.sfx.file")], threshold: 0.5, }; @@ -205,15 +210,17 @@ mod tests { time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(), }; - let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item)); + let result = + MovementEventMapper::should_emit(&last_sfx_event, Some((&event, &trigger_item))); assert_eq!(result, true); } #[test] fn same_previous_event_elapsed_emits() { + let event = SfxEvent::Run; + let trigger_item = SfxTriggerItem { - trigger: SfxEvent::Run, files: vec![String::from("some.path.to.sfx.file")], threshold: 0.5, }; @@ -225,14 +232,15 @@ mod tests { .unwrap(), }; - let result = SfxEventMapper::should_emit(&last_sfx_event, Some(&trigger_item)); + let result = + MovementEventMapper::should_emit(&last_sfx_event, Some((&event, &trigger_item))); assert_eq!(result, true); } #[test] fn maps_idle() { - let result = SfxEventMapper::map_movement_event( + let result = MovementEventMapper::map_movement_event( &CharacterState { movement: MovementState::Stand, action: ActionState::Idle, @@ -245,7 +253,7 @@ mod tests { #[test] fn maps_run() { - let result = SfxEventMapper::map_movement_event( + let result = MovementEventMapper::map_movement_event( &CharacterState { movement: MovementState::Run, action: ActionState::Idle, @@ -258,7 +266,7 @@ mod tests { #[test] fn maps_roll() { - let result = SfxEventMapper::map_movement_event( + let result = MovementEventMapper::map_movement_event( &CharacterState { action: ActionState::Roll { time_left: Duration::new(1, 0), @@ -274,7 +282,7 @@ mod tests { #[test] fn maps_fall() { - let result = SfxEventMapper::map_movement_event( + let result = MovementEventMapper::map_movement_event( &CharacterState { movement: MovementState::Fall, action: ActionState::Idle, @@ -287,7 +295,7 @@ mod tests { #[test] fn maps_glider_open() { - let result = SfxEventMapper::map_movement_event( + let result = MovementEventMapper::map_movement_event( &CharacterState { movement: MovementState::Glide, action: ActionState::Idle, @@ -300,7 +308,7 @@ mod tests { #[test] fn maps_glide() { - let result = SfxEventMapper::map_movement_event( + let result = MovementEventMapper::map_movement_event( &CharacterState { movement: MovementState::Glide, action: ActionState::Idle, @@ -312,8 +320,8 @@ mod tests { } #[test] - fn maps_glider_close() { - let result = SfxEventMapper::map_movement_event( + fn maps_glider_close_when_closing_mid_flight() { + let result = MovementEventMapper::map_movement_event( &CharacterState { movement: MovementState::Fall, action: ActionState::Idle, @@ -323,4 +331,17 @@ mod tests { assert_eq!(result, SfxEvent::GliderClose); } + + #[test] + fn maps_glider_close_when_landing() { + let result = MovementEventMapper::map_movement_event( + &CharacterState { + movement: MovementState::Stand, + action: ActionState::Idle, + }, + SfxEvent::Glide, + ); + + assert_eq!(result, SfxEvent::GliderClose); + } } diff --git a/voxygen/src/audio/sfx/event_mapper/progression.rs b/voxygen/src/audio/sfx/event_mapper/progression.rs new file mode 100644 index 0000000000..a264c8bb23 --- /dev/null +++ b/voxygen/src/audio/sfx/event_mapper/progression.rs @@ -0,0 +1,120 @@ +/// event_mapper::progression watches the the current player's level +/// and experience and emits associated SFX +use crate::audio::sfx::SfxTriggers; + +use client::Client; +use common::{ + comp::Stats, + event::{EventBus, SfxEvent, SfxEventItem}, +}; +use specs::WorldExt; + +#[derive(Clone, PartialEq)] +struct ProgressionState { + level: u32, + exp: u32, +} + +impl Default for ProgressionState { + fn default() -> Self { + Self { level: 1, exp: 0 } + } +} + +pub struct ProgressionEventMapper { + state: ProgressionState, +} + +impl ProgressionEventMapper { + pub fn new() -> Self { + Self { + state: ProgressionState::default(), + } + } + + pub fn maintain(&mut self, client: &Client, triggers: &SfxTriggers) { + let ecs = client.state().ecs(); + + // level and exp changes + let next_state = + ecs.read_storage::() + .get(client.entity()) + .map_or(self.state.clone(), |stats| ProgressionState { + level: stats.level.level(), + exp: stats.exp.current(), + }); + + if &self.state != &next_state { + if let Some(mapped_event) = self.map_event(&next_state) { + let sfx_trigger_item = triggers.get_trigger(&mapped_event); + + if sfx_trigger_item.is_some() { + ecs.read_resource::>() + .emitter() + .emit(SfxEventItem::at_player_position(mapped_event)); + } + } + + self.state = next_state; + } + } + + fn map_event(&mut self, next_state: &ProgressionState) -> Option { + let sfx_event = if next_state.level > self.state.level { + Some(SfxEvent::LevelUp) + } else if next_state.exp > self.state.exp { + Some(SfxEvent::ExperienceGained) + } else { + None + }; + + sfx_event + } +} + +#[cfg(test)] +mod tests { + use super::*; + use common::event::SfxEvent; + + #[test] + fn no_change_returns_none() { + let mut mapper = ProgressionEventMapper::new(); + let next_client_state = ProgressionState::default(); + + assert_eq!(mapper.map_event(&next_client_state), None); + } + + #[test] + fn change_level_returns_levelup() { + let mut mapper = ProgressionEventMapper::new(); + let next_client_state = ProgressionState { level: 2, exp: 0 }; + + assert_eq!( + mapper.map_event(&next_client_state), + Some(SfxEvent::LevelUp) + ); + } + + #[test] + fn change_exp_returns_expup() { + let mut mapper = ProgressionEventMapper::new(); + let next_client_state = ProgressionState { level: 1, exp: 100 }; + + assert_eq!( + mapper.map_event(&next_client_state), + Some(SfxEvent::ExperienceGained) + ); + } + + #[test] + fn level_up_and_gained_exp_prioritises_levelup() { + let mut mapper = ProgressionEventMapper::new(); + let next_client_state = ProgressionState { level: 2, exp: 100 }; + + assert_eq!( + mapper.map_event(&next_client_state), + Some(SfxEvent::LevelUp) + ); + } +} diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index 52ab02614c..ad37a5fb46 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -1,4 +1,5 @@ -/// The SfxManager listens for SFX events and plays the sound at the provided position +/// The Sfx Manager manages individual sfx event system, listens for +/// SFX events and plays the sound at the requested position, or the current player position mod event_mapper; use crate::audio::AudioFrontend; @@ -8,37 +9,53 @@ use common::{ comp::{Ori, Pos}, event::{EventBus, SfxEvent, SfxEventItem}, }; +use event_mapper::SfxEventMapper; +use hashbrown::HashMap; use serde::Deserialize; use specs::WorldExt; 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 SfxTriggers(HashMap); + +impl Default for SfxTriggers { + fn default() -> Self { + Self(HashMap::new()) + } +} + +impl SfxTriggers { + pub fn get_trigger(&self, trigger: &SfxEvent) -> Option<&SfxTriggerItem> { + self.0.get(trigger) + } + + pub fn get_key_value(&self, trigger: &SfxEvent) -> Option<(&SfxEvent, &SfxTriggerItem)> { + self.0.get_key_value(trigger) + } } pub struct SfxMgr { triggers: SfxTriggers, - event_mapper: event_mapper::SfxEventMapper, + event_mapper: SfxEventMapper, } impl SfxMgr { pub fn new() -> Self { Self { triggers: Self::load_sfx_items(), - event_mapper: event_mapper::SfxEventMapper::new(), + 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 @@ -61,16 +78,7 @@ impl SfxMgr { _ => 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"); - + if let Some(item) = self.triggers.get_trigger(&event.sfx) { let sfx_file = match item.files.len() { 1 => item .files @@ -91,6 +99,16 @@ impl SfxMgr { 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") + match ron::de::from_reader(file) { + Ok(config) => config, + Err(e) => { + log::warn!( + "Error parsing sfx config file, sfx will not be available: {}", + format!("{:#?}", e) + ); + + SfxTriggers::default() + } + } } }