Merge branch 'shandley/character-exp-sfx' into 'master'

Update SFX structure to add experience / levelling SFX

See merge request veloren/veloren!693
This commit is contained in:
Imbris 2020-01-01 02:55:48 +00:00
commit 7313ec69cb
6 changed files with 239 additions and 78 deletions

View File

@ -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,
),
],
}
)

View File

@ -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),

View File

@ -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);
}
}

View File

@ -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<EcsEntity, LastSfxEvent>,
}
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::<EventBus<SfxEventItem>>()
.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);
}
}

View File

@ -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::<Stats>()
.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::<EventBus<SfxEventItem>>()
.emitter()
.emit(SfxEventItem::at_player_position(mapped_event));
}
}
self.state = next_state;
}
}
fn map_event(&mut self, next_state: &ProgressionState) -> Option<SfxEvent> {
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)
);
}
}

View File

@ -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<String>,
pub threshold: f64,
}
#[derive(Deserialize)]
pub struct SfxTriggers {
pub items: Vec<SfxTriggerItem>,
pub struct SfxTriggers(HashMap<SfxEvent, SfxTriggerItem>);
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()
}
}
}
}