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 467c154582
commit 6a1cec8860
24 changed files with 293 additions and 263 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",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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;