Merge branch 'shandley/attack-sfx' into 'master'

Attack sfx

See merge request veloren/veloren!927
This commit is contained in:
Joshua Barretto 2020-06-08 15:37:23 +00:00
commit fd21fecd35
18 changed files with 676 additions and 316 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,19 +23,18 @@
],
threshold: 0.5,
),
Wield(Sword(BasicSword)): (
Wield(Sword): (
files: [
"voxygen.audio.sfx.weapon.sword_out",
],
threshold: 0.5,
),
Unwield(Sword(BasicSword)): (
Unwield(Sword): (
files: [
"voxygen.audio.sfx.weapon.sword_in",
],
threshold: 0.5,
),
Inventory(Collected): (
files: [
"voxygen.audio.sfx.inventory.add_item",

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,7 @@
use crate::{
comp::{
item::Item, Body, CharacterState, EnergySource, Gravity, LightEmitter, Projectile,
StateUpdate,
ability::Stage, item::Item, Body, CharacterState, EnergySource, Gravity, LightEmitter,
Projectile, StateUpdate,
},
states::{triple_strike::*, *},
sys::character_behavior::JoinData,
@ -10,6 +10,30 @@ use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage;
use std::time::Duration;
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub enum CharacterAbilityType {
BasicMelee,
BasicRanged,
Boost,
DashMelee,
BasicBlock,
TripleStrike(Stage),
}
impl From<&CharacterState> for CharacterAbilityType {
fn from(state: &CharacterState) -> Self {
match state {
CharacterState::BasicMelee(_) => Self::BasicMelee,
CharacterState::BasicRanged(_) => Self::BasicRanged,
CharacterState::Boost(_) => Self::Boost,
CharacterState::DashMelee(_) => Self::DashMelee,
CharacterState::BasicBlock => Self::BasicBlock,
CharacterState::TripleStrike(data) => Self::TripleStrike(data.stage),
_ => Self::BasicMelee,
}
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub enum CharacterAbility {
BasicMelee {

View File

@ -2,7 +2,7 @@ pub mod armor;
pub mod tool;
// Reexports
pub use tool::{DebugKind, SwordKind, Tool, ToolKind};
pub use tool::{DebugKind, SwordKind, Tool, ToolCategory, ToolKind};
use crate::{
assets::{self, Asset},

View File

@ -115,6 +115,37 @@ pub enum ToolKind {
Empty,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ToolCategory {
Sword,
Axe,
Hammer,
Bow,
Dagger,
Staff,
Shield,
Debug,
Farming,
Empty,
}
impl From<ToolKind> for ToolCategory {
fn from(kind: ToolKind) -> ToolCategory {
match kind {
ToolKind::Sword(_) => ToolCategory::Sword,
ToolKind::Axe(_) => ToolCategory::Axe,
ToolKind::Hammer(_) => ToolCategory::Hammer,
ToolKind::Bow(_) => ToolCategory::Bow,
ToolKind::Dagger(_) => ToolCategory::Dagger,
ToolKind::Staff(_) => ToolCategory::Staff,
ToolKind::Shield(_) => ToolCategory::Shield,
ToolKind::Debug(_) => ToolCategory::Debug,
ToolKind::Farming(_) => ToolCategory::Farming,
ToolKind::Empty => ToolCategory::Empty,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Tool {
pub kind: ToolKind,

View File

@ -16,7 +16,7 @@ mod stats;
mod visual;
// Reexports
pub use ability::{CharacterAbility, ItemConfig, Loadout};
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
pub use admin::Admin;
pub use agent::{Agent, Alignment, SpeechBubble, SPEECH_BUBBLE_DURATION};
pub use body::{

View File

@ -1,5 +1,5 @@
use crate::{comp, sync::Uid, util::Dir};
use comp::{item::ToolKind, InventoryUpdateEvent, Item};
use comp::{item::ToolCategory, CharacterAbilityType, InventoryUpdateEvent, Item};
use parking_lot::Mutex;
use serde::Deserialize;
use specs::Entity as EcsEntity;
@ -39,8 +39,9 @@ pub enum SfxEvent {
Fall,
ExperienceGained,
LevelUp,
Wield(ToolKind),
Unwield(ToolKind),
Attack(CharacterAbilityType, ToolCategory),
Wield(ToolCategory),
Unwield(ToolCategory),
Inventory(InventoryUpdateEvent),
}

View File

@ -0,0 +1,188 @@
/// 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},
CharacterAbilityType, CharacterState, ItemConfig, Loadout, Pos,
},
event::{EventBus, SfxEvent, SfxEventItem},
state::State,
};
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join, WorldExt};
use std::time::{Duration, Instant};
use vek::*;
#[derive(Clone)]
struct PreviousEntityState {
event: SfxEvent,
time: Instant,
weapon_drawn: bool,
}
impl Default for PreviousEntityState {
fn default() -> Self {
Self {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
}
}
}
pub struct CombatEventMapper {
event_history: HashMap<EcsEntity, PreviousEntityState>,
}
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>>();
let mut sfx_emitter = sfx_event_bus.emitter();
let player_position = ecs
.read_storage::<Pos>()
.get(player_entity)
.map_or(Vec3::zero(), |pos| pos.0);
for (entity, pos, loadout, character) in (
&ecs.entities(),
&ecs.read_storage::<Pos>(),
ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<CharacterState>().maybe(),
)
.join()
.filter(|(_, e_pos, ..)| {
(e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR
})
{
if let Some(character) = character {
let state = self
.event_history
.entry(entity)
.or_insert_with(|| PreviousEntityState::default());
let mapped_event = Self::map_event(character, state, loadout);
// Check for SFX config entry for this movement
if Self::should_emit(state, triggers.get_key_value(&mapped_event)) {
sfx_emitter.emit(SfxEventItem::new(mapped_event, Some(pos.0), None));
state.time = Instant::now();
}
// update state to determine the next event. We only record the time (above) if
// it was dispatched
state.event = mapped_event;
state.weapon_drawn = Self::weapon_drawn(character);
}
}
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
/// their activity. `cleanup` will remove entities from event tracking if
/// they have not triggered an event for > n seconds. This prevents
/// stale records from bloating the Map size.
fn cleanup(&mut self, player: EcsEntity) {
const TRACKING_TIMEOUT: u64 = 10;
let now = Instant::now();
self.event_history.retain(|entity, event| {
now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
|| entity.id() == player.id()
});
}
/// 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)>,
) -> bool {
if let Some((event, item)) = sfx_trigger_item {
if &previous_state.event == event {
previous_state.time.elapsed().as_secs_f64() >= item.threshold
} else {
true
}
} else {
false
}
}
fn map_event(
character_state: &CharacterState,
previous_state: &PreviousEntityState,
loadout: Option<&Loadout>,
) -> SfxEvent {
if let Some(active_loadout) = loadout {
if let Some(ItemConfig {
item:
Item {
kind: ItemKind::Tool(data),
..
},
..
}) = active_loadout.active_item
{
// Check for attacking states
if character_state.is_attack() {
return SfxEvent::Attack(
CharacterAbilityType::from(character_state),
ToolCategory::from(data.kind),
);
} else {
if let Some(wield_event) = match (
previous_state.weapon_drawn,
character_state.is_dodge(),
Self::weapon_drawn(character_state),
) {
(false, false, true) => {
Some(SfxEvent::Wield(ToolCategory::from(data.kind)))
},
(true, false, false) => {
Some(SfxEvent::Unwield(ToolCategory::from(data.kind)))
},
_ => None,
} {
return wield_event;
}
}
}
}
SfxEvent::Idle
}
/// This helps us determine whether we should be emitting the Wield/Unwield
/// events. For now, consider either CharacterState::Wielding or
/// ::Equipping to mean the weapon is drawn. This will need updating if the
/// animations change to match the wield_duration associated with the weapon
fn weapon_drawn(character: &CharacterState) -> bool {
character.is_wield()
|| match character {
CharacterState::Equipping { .. } => true,
_ => false,
}
}
}
#[cfg(test)] mod tests;

View File

@ -0,0 +1,180 @@
use super::*;
use common::{
assets,
comp::{item::tool::ToolCategory, CharacterAbilityType, CharacterState, ItemConfig, Loadout},
event::SfxEvent,
states,
};
use std::time::{Duration, Instant};
#[test]
fn maps_wield_while_equipping() {
let mut loadout = Loadout::default();
loadout.active_item = Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.axe.starter_axe"),
ability1: None,
ability2: None,
ability3: None,
block_ability: None,
dodge_ability: None,
});
let result = CombatEventMapper::map_event(
&CharacterState::Equipping(states::equipping::Data {
time_left: Duration::from_millis(10),
}),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
},
Some(&loadout),
);
assert_eq!(result, SfxEvent::Wield(ToolCategory::Axe));
}
#[test]
fn maps_unwield() {
let mut loadout = Loadout::default();
loadout.active_item = Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.bow.starter_bow"),
ability1: None,
ability2: None,
ability3: None,
block_ability: None,
dodge_ability: None,
});
let result = CombatEventMapper::map_event(
&CharacterState::default(),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: true,
},
Some(&loadout),
);
assert_eq!(result, SfxEvent::Unwield(ToolCategory::Bow));
}
#[test]
fn maps_basic_melee() {
let mut loadout = Loadout::default();
loadout.active_item = Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.axe.starter_axe"),
ability1: None,
ability2: None,
ability3: None,
block_ability: None,
dodge_ability: None,
});
let result = CombatEventMapper::map_event(
&CharacterState::BasicMelee(states::basic_melee::Data {
buildup_duration: Duration::default(),
recover_duration: Duration::default(),
base_healthchange: 1,
range: 1.0,
max_angle: 1.0,
exhausted: false,
}),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: true,
},
Some(&loadout),
);
assert_eq!(
result,
SfxEvent::Attack(CharacterAbilityType::BasicMelee, ToolCategory::Axe)
);
}
#[test]
fn matches_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::First,
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_eq!(
result,
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

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

View File

@ -1,13 +1,11 @@
/// 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};
/// 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::{
item::{Item, ItemKind},
Body, CharacterState, ItemConfig, Loadout, PhysicsState, Pos, Vel,
},
comp::{Body, CharacterState, PhysicsState, Pos, Vel},
event::{EventBus, SfxEvent, SfxEventItem},
state::State,
};
@ -20,7 +18,6 @@ use vek::*;
struct PreviousEntityState {
event: SfxEvent,
time: Instant,
weapon_drawn: bool,
on_ground: bool,
}
@ -29,7 +26,6 @@ impl Default for PreviousEntityState {
Self {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
on_ground: true,
}
}
@ -39,15 +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) {
const SFX_DIST_LIMIT_SQR: f32 = 20000.0;
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>>();
@ -58,13 +47,12 @@ impl MovementEventMapper {
.get(player_entity)
.map_or(Vec3::zero(), |pos| pos.0);
for (entity, pos, vel, body, physics, loadout, character) in (
for (entity, pos, vel, body, physics, character) in (
&ecs.entities(),
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<Vel>(),
&ecs.read_storage::<Body>(),
&ecs.read_storage::<PhysicsState>(),
ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<CharacterState>().maybe(),
)
.join()
@ -79,9 +67,7 @@ impl MovementEventMapper {
.or_insert_with(|| PreviousEntityState::default());
let mapped_event = match body {
Body::Humanoid(_) => {
Self::map_movement_event(character, physics, state, vel.0, loadout)
},
Body::Humanoid(_) => Self::map_movement_event(character, physics, state, vel.0),
Body::QuadrupedMedium(_)
| Body::QuadrupedSmall(_)
| Body::BirdMedium(_)
@ -104,13 +90,20 @@ impl MovementEventMapper {
// update state to determine the next event. We only record the time (above) if
// it was dispatched
state.event = mapped_event;
state.weapon_drawn = Self::weapon_drawn(character);
state.on_ground = physics.on_ground;
}
}
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
@ -157,33 +150,7 @@ impl MovementEventMapper {
physics_state: &PhysicsState,
previous_state: &PreviousEntityState,
vel: Vec3<f32>,
loadout: Option<&Loadout>,
) -> SfxEvent {
// Handle wield state changes
if let Some(active_loadout) = loadout {
if let Some(ItemConfig {
item:
Item {
kind: ItemKind::Tool(data),
..
},
..
}) = active_loadout.active_item
{
if let Some(wield_event) = match (
previous_state.weapon_drawn,
character_state.is_dodge(),
Self::weapon_drawn(character_state),
) {
(false, false, true) => Some(SfxEvent::Wield(data.kind)),
(true, false, false) => Some(SfxEvent::Unwield(data.kind)),
_ => None,
} {
return wield_event;
}
}
}
// Match run state
if physics_state.on_ground && vel.magnitude() > 0.1
|| !previous_state.on_ground && physics_state.on_ground
@ -216,18 +183,6 @@ impl MovementEventMapper {
}
}
/// This helps us determine whether we should be emitting the Wield/Unwield
/// events. For now, consider either CharacterState::Wielding or
/// ::Equipping to mean the weapon is drawn. This will need updating if the
/// animations change to match the wield_duration associated with the weapon
fn weapon_drawn(character: &CharacterState) -> bool {
character.is_wield()
|| match character {
CharacterState::Equipping { .. } => true,
_ => false,
}
}
/// Returns a relative volume value for a body type. This helps us emit sfx
/// at a volume appropriate fot the entity we are emitting the event for
fn get_volume_for_body_type(body: &Body) -> f32 {

View File

@ -1,10 +1,7 @@
use super::*;
use common::{
assets,
comp::{
bird_small, humanoid,
item::tool::{AxeKind, BowKind, ToolKind},
quadruped_medium, quadruped_small, Body, CharacterState, ItemConfig, Loadout, PhysicsState,
bird_small, humanoid, quadruped_medium, quadruped_small, Body, CharacterState, PhysicsState,
},
event::SfxEvent,
states,
@ -30,7 +27,6 @@ fn config_but_played_since_threshold_no_emit() {
let previous_state = PreviousEntityState {
event: SfxEvent::Run,
time: Instant::now(),
weapon_drawn: false,
on_ground: true,
};
@ -50,7 +46,6 @@ fn config_and_not_played_since_threshold_emits() {
let previous_state = PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(),
weapon_drawn: false,
on_ground: true,
};
@ -72,7 +67,6 @@ fn same_previous_event_elapsed_emits() {
time: Instant::now()
.checked_sub(Duration::from_millis(500))
.unwrap(),
weapon_drawn: false,
on_ground: true,
};
@ -96,11 +90,9 @@ fn maps_idle() {
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
on_ground: true,
},
Vec3::zero(),
None,
);
assert_eq!(result, SfxEvent::Idle);
@ -120,11 +112,9 @@ fn maps_run_with_sufficient_velocity() {
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
on_ground: true,
},
Vec3::new(0.5, 0.8, 0.0),
None,
);
assert_eq!(result, SfxEvent::Run);
@ -144,11 +134,9 @@ fn does_not_map_run_with_insufficient_velocity() {
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
on_ground: true,
},
Vec3::new(0.02, 0.0001, 0.0),
None,
);
assert_eq!(result, SfxEvent::Idle);
@ -168,11 +156,9 @@ fn does_not_map_run_with_sufficient_velocity_but_not_on_ground() {
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
on_ground: false,
},
Vec3::new(0.5, 0.8, 0.0),
None,
);
assert_eq!(result, SfxEvent::Idle);
@ -195,11 +181,9 @@ fn maps_roll() {
&PreviousEntityState {
event: SfxEvent::Run,
time: Instant::now(),
weapon_drawn: false,
on_ground: true,
},
Vec3::zero(),
None,
);
assert_eq!(result, SfxEvent::Roll);
@ -219,11 +203,9 @@ fn maps_land_on_ground_to_run() {
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
on_ground: false,
},
Vec3::zero(),
None,
);
assert_eq!(result, SfxEvent::Run);
@ -243,11 +225,9 @@ fn maps_glider_open() {
&PreviousEntityState {
event: SfxEvent::Jump,
time: Instant::now(),
weapon_drawn: false,
on_ground: false,
},
Vec3::zero(),
None,
);
assert_eq!(result, SfxEvent::GliderOpen);
@ -267,11 +247,9 @@ fn maps_glide() {
&PreviousEntityState {
event: SfxEvent::Glide,
time: Instant::now(),
weapon_drawn: false,
on_ground: false,
},
Vec3::zero(),
None,
);
assert_eq!(result, SfxEvent::Glide);
@ -291,11 +269,9 @@ fn maps_glider_close_when_closing_mid_flight() {
&PreviousEntityState {
event: SfxEvent::Glide,
time: Instant::now(),
weapon_drawn: false,
on_ground: false,
},
Vec3::zero(),
None,
);
assert_eq!(result, SfxEvent::GliderClose);
@ -316,88 +292,14 @@ fn maps_glider_close_when_landing() {
&PreviousEntityState {
event: SfxEvent::Glide,
time: Instant::now(),
weapon_drawn: false,
on_ground: false,
},
Vec3::zero(),
None,
);
assert_eq!(result, SfxEvent::GliderClose);
}
#[test]
fn maps_wield_while_equipping() {
let mut loadout = Loadout::default();
loadout.active_item = Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.axe.starter_axe"),
ability1: None,
ability2: None,
ability3: None,
block_ability: None,
dodge_ability: None,
});
let result = MovementEventMapper::map_movement_event(
&CharacterState::Equipping(states::equipping::Data {
time_left: Duration::from_millis(10),
}),
&PhysicsState {
on_ground: true,
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
},
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
on_ground: true,
},
Vec3::zero(),
Some(&loadout),
);
assert_eq!(result, SfxEvent::Wield(ToolKind::Axe(AxeKind::BasicAxe)));
}
#[test]
fn maps_unwield() {
let mut loadout = Loadout::default();
loadout.active_item = Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.bow.starter_bow"),
ability1: None,
ability2: None,
ability3: None,
block_ability: None,
dodge_ability: None,
});
let result = MovementEventMapper::map_movement_event(
&CharacterState::default(),
&PhysicsState {
on_ground: true,
on_ceiling: false,
on_wall: None,
touch_entity: None,
in_fluid: false,
},
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: true,
on_ground: true,
},
Vec3::zero(),
Some(&loadout),
);
assert_eq!(result, SfxEvent::Unwield(ToolKind::Bow(BowKind::ShortBow0)));
}
#[test]
fn maps_quadrupeds_running() {
let result = MovementEventMapper::map_non_humanoid_movement_event(

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;
@ -16,19 +95,22 @@ use serde::Deserialize;
use specs::WorldExt;
use vek::*;
/// We watch the states of nearby entities in order to emit SFX at their
/// position based on their state. This constant limits the radius that we
/// observe to prevent tracking distant entities. It approximates the distance
/// at which the volume of the sfx emitted is too quiet to be meaningful for the
/// player.
const SFX_DIST_LIMIT_SQR: f32 = 20000.0;
#[derive(Deserialize)]
pub struct SfxTriggerItem {
pub files: Vec<String>,
pub threshold: f64,
}
#[derive(Deserialize)]
#[derive(Deserialize, Default)]
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) }