mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'shandley/attack-sfx' into 'master'
Attack sfx See merge request veloren/veloren!927
This commit is contained in:
commit
fd21fecd35
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
BIN
assets/voxygen/audio/sfx/weapon/sword_in.wav
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/weapon/sword_in.wav
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/audio/sfx/weapon/sword_out.wav
(Stored with Git LFS)
BIN
assets/voxygen/audio/sfx/weapon/sword_out.wav
(Stored with Git LFS)
Binary file not shown.
@ -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 {
|
||||
|
@ -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},
|
||||
|
@ -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,
|
||||
|
@ -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::{
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
188
voxygen/src/audio/sfx/event_mapper/combat/mod.rs
Normal file
188
voxygen/src/audio/sfx/event_mapper/combat/mod.rs
Normal 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;
|
180
voxygen/src/audio/sfx/event_mapper/combat/tests.rs
Normal file
180
voxygen/src/audio/sfx/event_mapper/combat/tests.rs
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
75
voxygen/src/audio/sfx/event_mapper/progression/mod.rs
Normal file
75
voxygen/src/audio/sfx/event_mapper/progression/mod.rs
Normal 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;
|
43
voxygen/src/audio/sfx/event_mapper/progression/tests.rs
Normal file
43
voxygen/src/audio/sfx/event_mapper/progression/tests.rs
Normal 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)
|
||||
);
|
||||
}
|
@ -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) }
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user