Docs, make adding event mappers easier for sfx, remove placeholder

sounds.
This commit is contained in:
Shane Handley 2020-06-08 19:50:19 +10:00
parent 673f6a4b17
commit 4714f8ddc7
24 changed files with 293 additions and 221 deletions

View File

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added inventory, armour and weapon saving
- Show where screenshots are saved to in the chat
- Added basic auto walk
- Added weapon/attack sound effects
### Changed

View File

@ -23,64 +23,6 @@
],
threshold: 0.5,
),
Attack(DashMelee, Sword): (
files: [
"voxygen.audio.sfx.weapon.whoosh_normal_01",
"voxygen.audio.sfx.weapon.whoosh_normal_02",
"voxygen.audio.sfx.weapon.whoosh_normal_03",
"voxygen.audio.sfx.weapon.whoosh_normal_04",
],
threshold: 1.2,
),
Attack(TripleStrike(First), Sword): (
files: [
"voxygen.audio.sfx.weapon.whoosh_normal_01",
"voxygen.audio.sfx.weapon.whoosh_normal_02",
"voxygen.audio.sfx.weapon.whoosh_normal_03",
"voxygen.audio.sfx.weapon.whoosh_normal_04",
],
threshold: 0.5,
),
Attack(TripleStrike(Second), Sword): (
files: [
"voxygen.audio.sfx.weapon.whoosh_low_01",
"voxygen.audio.sfx.weapon.whoosh_low_02",
"voxygen.audio.sfx.weapon.whoosh_low_03",
],
threshold: 0.5,
),
Attack(TripleStrike(Third), Sword): (
files: [
"voxygen.audio.sfx.weapon.whoosh_low_01",
"voxygen.audio.sfx.weapon.whoosh_low_02",
"voxygen.audio.sfx.weapon.whoosh_low_03",
],
threshold: 0.5,
),
Attack(BasicRanged, Bow): (
files: [
"voxygen.audio.sfx.weapon.bow_attack_01",
"voxygen.audio.sfx.weapon.bow_attack_02",
],
threshold: 0.5,
),
Attack(BasicMelee, Hammer): (
files: [
"voxygen.audio.sfx.weapon.whoosh_low_01",
"voxygen.audio.sfx.weapon.whoosh_low_02",
"voxygen.audio.sfx.weapon.whoosh_low_03",
],
threshold: 0.5,
),
Attack(BasicMelee, Staff): (
files: [
"voxygen.audio.sfx.weapon.whoosh_normal_01",
"voxygen.audio.sfx.weapon.whoosh_normal_02",
"voxygen.audio.sfx.weapon.whoosh_normal_03",
"voxygen.audio.sfx.weapon.whoosh_normal_04",
],
threshold: 0.5,
),
Wield(Sword): (
files: [
"voxygen.audio.sfx.weapon.sword_out",

View File

@ -1,7 +1,9 @@
/// event_mapper::combat watches the combat state of entities and emits
/// associated sfx events
/// EventMapper::Combat watches the combat states of surrounding entities' and
/// emits sfx related to weapons and attacks/abilities
use crate::audio::sfx::{SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR};
use super::EventMapper;
use common::{
comp::{
item::{Item, ItemKind, ToolCategory},
@ -36,14 +38,8 @@ pub struct CombatEventMapper {
event_history: HashMap<EcsEntity, PreviousEntityState>,
}
impl CombatEventMapper {
pub fn new() -> Self {
Self {
event_history: HashMap::new(),
}
}
pub fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) {
impl EventMapper for CombatEventMapper {
fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) {
let ecs = state.ecs();
let sfx_event_bus = ecs.read_resource::<EventBus<SfxEventItem>>();
@ -89,6 +85,14 @@ impl CombatEventMapper {
self.cleanup(player_entity);
}
}
impl CombatEventMapper {
pub fn new() -> Self {
Self {
event_history: HashMap::new(),
}
}
/// 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
@ -105,11 +109,10 @@ impl CombatEventMapper {
});
}
/// 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
/// Ensures that:
/// 1. An sfx.ron entry exists for an SFX event
/// 2. The sfx has not been played since it's timeout threshold has elapsed,
/// which prevents firing every tick
fn should_emit(
previous_state: &PreviousEntityState,
sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,

View File

@ -98,7 +98,7 @@ fn maps_basic_melee() {
}
#[test]
fn maps_triple_strike() {
fn matches_ability_stage() {
let mut loadout = Loadout::default();
loadout.active_item = Some(ItemConfig {
@ -131,6 +131,50 @@ fn maps_triple_strike() {
assert_eq!(
result,
SfxEvent::Attack(CharacterAbilityType::TripleStrike, ToolCategory::Sword)
SfxEvent::Attack(
CharacterAbilityType::TripleStrike(states::triple_strike::Stage::First),
ToolCategory::Sword
)
);
}
#[test]
fn ignores_different_ability_stage() {
let mut loadout = Loadout::default();
loadout.active_item = Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.sword.starter_sword"),
ability1: None,
ability2: None,
ability3: None,
block_ability: None,
dodge_ability: None,
});
let result = CombatEventMapper::map_event(
&CharacterState::TripleStrike(states::triple_strike::Data {
base_damage: 10,
stage: states::triple_strike::Stage::Second,
stage_time_active: Duration::default(),
stage_exhausted: false,
initialized: true,
transition_style: states::triple_strike::TransitionStyle::Hold(
states::triple_strike::HoldingState::Released,
),
}),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: true,
},
Some(&loadout),
);
assert_ne!(
result,
SfxEvent::Attack(
CharacterAbilityType::TripleStrike(states::triple_strike::Stage::First),
ToolCategory::Sword
)
);
}

View File

@ -10,18 +10,22 @@ use progression::ProgressionEventMapper;
use super::SfxTriggers;
trait EventMapper {
fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers);
}
pub struct SfxEventMapper {
progression: ProgressionEventMapper,
movement: MovementEventMapper,
combat: CombatEventMapper,
mappers: Vec<Box<dyn EventMapper>>,
}
impl SfxEventMapper {
pub fn new() -> Self {
Self {
progression: ProgressionEventMapper::new(),
combat: CombatEventMapper::new(),
movement: MovementEventMapper::new(),
mappers: vec![
Box::new(CombatEventMapper::new()),
Box::new(MovementEventMapper::new()),
Box::new(ProgressionEventMapper::new()),
],
}
}
@ -31,8 +35,8 @@ impl SfxEventMapper {
player_entity: specs::Entity,
triggers: &SfxTriggers,
) {
self.progression.maintain(state, player_entity, triggers);
self.movement.maintain(state, player_entity, triggers);
self.combat.maintain(state, player_entity, triggers);
for mapper in &mut self.mappers {
mapper.maintain(state, player_entity, triggers);
}
}
}

View File

@ -1,8 +1,9 @@
/// 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, SFX_DIST_LIMIT_SQR};
/// EventMapper::Movement watches the movement states of surrounding entities,
/// and triggers sfx related to running, climbing and gliding, at a volume
/// proportionate to the extity's size
use super::EventMapper;
use crate::audio::sfx::{SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR};
use common::{
comp::{Body, CharacterState, PhysicsState, Pos, Vel},
event::{EventBus, SfxEvent, SfxEventItem},
@ -34,14 +35,8 @@ pub struct MovementEventMapper {
event_history: HashMap<EcsEntity, PreviousEntityState>,
}
impl MovementEventMapper {
pub fn new() -> Self {
Self {
event_history: HashMap::new(),
}
}
pub fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) {
impl EventMapper for MovementEventMapper {
fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) {
let ecs = state.ecs();
let sfx_event_bus = ecs.read_resource::<EventBus<SfxEventItem>>();
@ -101,6 +96,14 @@ impl MovementEventMapper {
self.cleanup(player_entity);
}
}
impl MovementEventMapper {
pub fn new() -> Self {
Self {
event_history: HashMap::new(),
}
}
/// 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

View File

@ -1,122 +0,0 @@
/// event_mapper::progression watches the the current player's level
/// and experience and emits associated SFX
use crate::audio::sfx::SfxTriggers;
use common::{
comp::Stats,
event::{EventBus, SfxEvent, SfxEventItem},
state::State,
};
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,
state: &State,
player_entity: specs::Entity,
triggers: &SfxTriggers,
) {
let ecs = state.ecs();
// level and exp changes
let next_state =
ecs.read_storage::<Stats>()
.get(player_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>>()
.emit_now(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

@ -0,0 +1,75 @@
/// EventMapper::Progress watches the player entity's stats
/// and triggers sfx for gaining experience and levelling up
use super::EventMapper;
use crate::audio::sfx::SfxTriggers;
use common::{
comp::Stats,
event::{EventBus, SfxEvent, SfxEventItem},
state::State,
};
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 EventMapper for ProgressionEventMapper {
fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers) {
let ecs = state.ecs();
let next_state = ecs.read_storage::<Stats>().get(player_entity).map_or(
ProgressionState::default(),
|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>>()
.emit_now(SfxEventItem::at_player_position(mapped_event));
}
}
self.state = next_state;
}
}
}
impl ProgressionEventMapper {
pub fn new() -> Self {
Self {
state: ProgressionState::default(),
}
}
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;

View File

@ -0,0 +1,43 @@
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,6 +1,85 @@
/// 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
//! Manages individual sfx event system, listens for sfx events, and requests
//! playback at the requested position and volume
//!
//! Veloren's sfx are managed through a configuration which lives in the
//! codebase under `/assets/voxygen/audio/sfx.ron`.
//!
//! Each entry in the configuration consists of an
//! [SfxEvent](../../../veloren_common/event/enum.SfxEvent.html) item, with some
//! additional information to allow playback:
//! - `files` - the paths to the `.wav` files to be played for the sfx. minus
//! the file extension. This can be a single item if the same sound can be
//! played each time, or a list of files from which one is chosen at random to
//! be played.
//! - `threshold` - the time that the system should wait between successive
//! plays.
//!
//! The following snippet details some entries in the configuration and how they
//! map to the sound files:
//! ```ignore
//! Run: (
//! files: [
//! "voxygen.audio.sfx.footsteps.stepgrass_1",
//! "voxygen.audio.sfx.footsteps.stepgrass_2",
//! "voxygen.audio.sfx.footsteps.stepgrass_3",
//! "voxygen.audio.sfx.footsteps.stepgrass_4",
//! "voxygen.audio.sfx.footsteps.stepgrass_5",
//! "voxygen.audio.sfx.footsteps.stepgrass_6",
//! ],
//! threshold: 0.25, // wait 0.25s between plays
//! ),
//! Wield(Sword): ( // depends on the player's weapon
//! files: [
//! "voxygen.audio.sfx.weapon.sword_out",
//! ],
//! threshold: 0.5,
//! ),
//! ...
//! ```
//!
//! These items (for example, the `Wield(Sword)` occasionally depend on some
//! property which varies in game. The
//! [SfxEvent](../../../veloren_common/event/enum.SfxEvent.html) documentation
//! provides links to those variables, some examples are provided her for longer
//! items:
//!
//! ```ignore
//! // An inventory action
//! Inventory(Dropped): (
//! files: [
//! "voxygen.audio.sfx.footsteps.stepgrass_4",
//! ],
//! threshold: 0.5,
//! ),
//! // An inventory action which depends upon the item
//! Inventory(Consumed(Apple)): (
//! files: [
//! "voxygen.audio.sfx.inventory.consumable.apple",
//! ],
//! threshold: 0.5
//! ),
//! // An attack ability which depends on the weapon
//! Attack(DashMelee, Sword): (
//! files: [
//! "voxygen.audio.sfx.weapon.whoosh_normal_01",
//! "voxygen.audio.sfx.weapon.whoosh_normal_02",
//! "voxygen.audio.sfx.weapon.whoosh_normal_03",
//! "voxygen.audio.sfx.weapon.whoosh_normal_04",
//! ],
//! threshold: 1.2,
//! ),
//! // A multi-stage attack ability which depends on the weapon
//! Attack(TripleStrike(First), Sword): (
//! files: [
//! "voxygen.audio.sfx.weapon.whoosh_normal_01",
//! "voxygen.audio.sfx.weapon.whoosh_normal_02",
//! "voxygen.audio.sfx.weapon.whoosh_normal_03",
//! "voxygen.audio.sfx.weapon.whoosh_normal_04",
//! ],
//! threshold: 0.5,
//! ),
//! ```
mod event_mapper;
use crate::audio::AudioFrontend;