mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
commit
7313ec69cb
@ -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,
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
@ -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),
|
||||
|
27
voxygen/src/audio/sfx/event_mapper/mod.rs
Normal file
27
voxygen/src/audio/sfx/event_mapper/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
120
voxygen/src/audio/sfx/event_mapper/progression.rs
Normal file
120
voxygen/src/audio/sfx/event_mapper/progression.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user