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
|
- Added inventory, armour and weapon saving
|
||||||
- Show where screenshots are saved to in the chat
|
- Show where screenshots are saved to in the chat
|
||||||
- Added basic auto walk
|
- Added basic auto walk
|
||||||
|
- Added weapon/attack sound effects
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -23,19 +23,18 @@
|
|||||||
],
|
],
|
||||||
threshold: 0.5,
|
threshold: 0.5,
|
||||||
),
|
),
|
||||||
Wield(Sword(BasicSword)): (
|
Wield(Sword): (
|
||||||
files: [
|
files: [
|
||||||
"voxygen.audio.sfx.weapon.sword_out",
|
"voxygen.audio.sfx.weapon.sword_out",
|
||||||
],
|
],
|
||||||
threshold: 0.5,
|
threshold: 0.5,
|
||||||
),
|
),
|
||||||
Unwield(Sword(BasicSword)): (
|
Unwield(Sword): (
|
||||||
files: [
|
files: [
|
||||||
"voxygen.audio.sfx.weapon.sword_in",
|
"voxygen.audio.sfx.weapon.sword_in",
|
||||||
],
|
],
|
||||||
threshold: 0.5,
|
threshold: 0.5,
|
||||||
),
|
),
|
||||||
|
|
||||||
Inventory(Collected): (
|
Inventory(Collected): (
|
||||||
files: [
|
files: [
|
||||||
"voxygen.audio.sfx.inventory.add_item",
|
"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::{
|
use crate::{
|
||||||
comp::{
|
comp::{
|
||||||
item::Item, Body, CharacterState, EnergySource, Gravity, LightEmitter, Projectile,
|
ability::Stage, item::Item, Body, CharacterState, EnergySource, Gravity, LightEmitter,
|
||||||
StateUpdate,
|
Projectile, StateUpdate,
|
||||||
},
|
},
|
||||||
states::{triple_strike::*, *},
|
states::{triple_strike::*, *},
|
||||||
sys::character_behavior::JoinData,
|
sys::character_behavior::JoinData,
|
||||||
@ -10,6 +10,30 @@ use specs::{Component, FlaggedStorage};
|
|||||||
use specs_idvs::IDVStorage;
|
use specs_idvs::IDVStorage;
|
||||||
use std::time::Duration;
|
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)]
|
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
pub enum CharacterAbility {
|
pub enum CharacterAbility {
|
||||||
BasicMelee {
|
BasicMelee {
|
||||||
|
@ -2,7 +2,7 @@ pub mod armor;
|
|||||||
pub mod tool;
|
pub mod tool;
|
||||||
|
|
||||||
// Reexports
|
// Reexports
|
||||||
pub use tool::{DebugKind, SwordKind, Tool, ToolKind};
|
pub use tool::{DebugKind, SwordKind, Tool, ToolCategory, ToolKind};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
assets::{self, Asset},
|
assets::{self, Asset},
|
||||||
|
@ -115,6 +115,37 @@ pub enum ToolKind {
|
|||||||
Empty,
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct Tool {
|
pub struct Tool {
|
||||||
pub kind: ToolKind,
|
pub kind: ToolKind,
|
||||||
|
@ -16,7 +16,7 @@ mod stats;
|
|||||||
mod visual;
|
mod visual;
|
||||||
|
|
||||||
// Reexports
|
// Reexports
|
||||||
pub use ability::{CharacterAbility, ItemConfig, Loadout};
|
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
|
||||||
pub use admin::Admin;
|
pub use admin::Admin;
|
||||||
pub use agent::{Agent, Alignment, SpeechBubble, SPEECH_BUBBLE_DURATION};
|
pub use agent::{Agent, Alignment, SpeechBubble, SPEECH_BUBBLE_DURATION};
|
||||||
pub use body::{
|
pub use body::{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{comp, sync::Uid, util::Dir};
|
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 parking_lot::Mutex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use specs::Entity as EcsEntity;
|
use specs::Entity as EcsEntity;
|
||||||
@ -39,8 +39,9 @@ pub enum SfxEvent {
|
|||||||
Fall,
|
Fall,
|
||||||
ExperienceGained,
|
ExperienceGained,
|
||||||
LevelUp,
|
LevelUp,
|
||||||
Wield(ToolKind),
|
Attack(CharacterAbilityType, ToolCategory),
|
||||||
Unwield(ToolKind),
|
Wield(ToolCategory),
|
||||||
|
Unwield(ToolCategory),
|
||||||
Inventory(InventoryUpdateEvent),
|
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 movement;
|
||||||
mod progression;
|
mod progression;
|
||||||
|
|
||||||
use common::state::State;
|
use common::state::State;
|
||||||
|
|
||||||
|
use combat::CombatEventMapper;
|
||||||
use movement::MovementEventMapper;
|
use movement::MovementEventMapper;
|
||||||
use progression::ProgressionEventMapper;
|
use progression::ProgressionEventMapper;
|
||||||
|
|
||||||
use super::SfxTriggers;
|
use super::SfxTriggers;
|
||||||
|
|
||||||
|
trait EventMapper {
|
||||||
|
fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers);
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SfxEventMapper {
|
pub struct SfxEventMapper {
|
||||||
progression_event_mapper: ProgressionEventMapper,
|
mappers: Vec<Box<dyn EventMapper>>,
|
||||||
movement_event_mapper: MovementEventMapper,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SfxEventMapper {
|
impl SfxEventMapper {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
progression_event_mapper: ProgressionEventMapper::new(),
|
mappers: vec![
|
||||||
movement_event_mapper: MovementEventMapper::new(),
|
Box::new(CombatEventMapper::new()),
|
||||||
|
Box::new(MovementEventMapper::new()),
|
||||||
|
Box::new(ProgressionEventMapper::new()),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,9 +35,8 @@ impl SfxEventMapper {
|
|||||||
player_entity: specs::Entity,
|
player_entity: specs::Entity,
|
||||||
triggers: &SfxTriggers,
|
triggers: &SfxTriggers,
|
||||||
) {
|
) {
|
||||||
self.progression_event_mapper
|
for mapper in &mut self.mappers {
|
||||||
.maintain(state, player_entity, triggers);
|
mapper.maintain(state, player_entity, triggers);
|
||||||
self.movement_event_mapper
|
}
|
||||||
.maintain(state, player_entity, triggers);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
/// event_mapper::movement watches all local entities movements and determines
|
/// EventMapper::Movement watches the movement states of surrounding entities,
|
||||||
/// which sfx to emit, and the position at which the sound should be emitted
|
/// and triggers sfx related to running, climbing and gliding, at a volume
|
||||||
/// from
|
/// proportionate to the extity's size
|
||||||
use crate::audio::sfx::{SfxTriggerItem, SfxTriggers};
|
use super::EventMapper;
|
||||||
|
|
||||||
|
use crate::audio::sfx::{SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{
|
comp::{Body, CharacterState, PhysicsState, Pos, Vel},
|
||||||
item::{Item, ItemKind},
|
|
||||||
Body, CharacterState, ItemConfig, Loadout, PhysicsState, Pos, Vel,
|
|
||||||
},
|
|
||||||
event::{EventBus, SfxEvent, SfxEventItem},
|
event::{EventBus, SfxEvent, SfxEventItem},
|
||||||
state::State,
|
state::State,
|
||||||
};
|
};
|
||||||
@ -20,7 +18,6 @@ use vek::*;
|
|||||||
struct PreviousEntityState {
|
struct PreviousEntityState {
|
||||||
event: SfxEvent,
|
event: SfxEvent,
|
||||||
time: Instant,
|
time: Instant,
|
||||||
weapon_drawn: bool,
|
|
||||||
on_ground: bool,
|
on_ground: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +26,6 @@ impl Default for PreviousEntityState {
|
|||||||
Self {
|
Self {
|
||||||
event: SfxEvent::Idle,
|
event: SfxEvent::Idle,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: true,
|
on_ground: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,15 +35,8 @@ pub struct MovementEventMapper {
|
|||||||
event_history: HashMap<EcsEntity, PreviousEntityState>,
|
event_history: HashMap<EcsEntity, PreviousEntityState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MovementEventMapper {
|
impl EventMapper for MovementEventMapper {
|
||||||
pub fn new() -> Self {
|
fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) {
|
||||||
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;
|
|
||||||
let ecs = state.ecs();
|
let ecs = state.ecs();
|
||||||
|
|
||||||
let sfx_event_bus = ecs.read_resource::<EventBus<SfxEventItem>>();
|
let sfx_event_bus = ecs.read_resource::<EventBus<SfxEventItem>>();
|
||||||
@ -58,13 +47,12 @@ impl MovementEventMapper {
|
|||||||
.get(player_entity)
|
.get(player_entity)
|
||||||
.map_or(Vec3::zero(), |pos| pos.0);
|
.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.entities(),
|
||||||
&ecs.read_storage::<Pos>(),
|
&ecs.read_storage::<Pos>(),
|
||||||
&ecs.read_storage::<Vel>(),
|
&ecs.read_storage::<Vel>(),
|
||||||
&ecs.read_storage::<Body>(),
|
&ecs.read_storage::<Body>(),
|
||||||
&ecs.read_storage::<PhysicsState>(),
|
&ecs.read_storage::<PhysicsState>(),
|
||||||
ecs.read_storage::<Loadout>().maybe(),
|
|
||||||
ecs.read_storage::<CharacterState>().maybe(),
|
ecs.read_storage::<CharacterState>().maybe(),
|
||||||
)
|
)
|
||||||
.join()
|
.join()
|
||||||
@ -79,9 +67,7 @@ impl MovementEventMapper {
|
|||||||
.or_insert_with(|| PreviousEntityState::default());
|
.or_insert_with(|| PreviousEntityState::default());
|
||||||
|
|
||||||
let mapped_event = match body {
|
let mapped_event = match body {
|
||||||
Body::Humanoid(_) => {
|
Body::Humanoid(_) => Self::map_movement_event(character, physics, state, vel.0),
|
||||||
Self::map_movement_event(character, physics, state, vel.0, loadout)
|
|
||||||
},
|
|
||||||
Body::QuadrupedMedium(_)
|
Body::QuadrupedMedium(_)
|
||||||
| Body::QuadrupedSmall(_)
|
| Body::QuadrupedSmall(_)
|
||||||
| Body::BirdMedium(_)
|
| Body::BirdMedium(_)
|
||||||
@ -104,13 +90,20 @@ impl MovementEventMapper {
|
|||||||
// update state to determine the next event. We only record the time (above) if
|
// update state to determine the next event. We only record the time (above) if
|
||||||
// it was dispatched
|
// it was dispatched
|
||||||
state.event = mapped_event;
|
state.event = mapped_event;
|
||||||
state.weapon_drawn = Self::weapon_drawn(character);
|
|
||||||
state.on_ground = physics.on_ground;
|
state.on_ground = physics.on_ground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cleanup(player_entity);
|
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
|
/// 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
|
/// entities to determine the correct SFX item to play next based on
|
||||||
@ -157,33 +150,7 @@ impl MovementEventMapper {
|
|||||||
physics_state: &PhysicsState,
|
physics_state: &PhysicsState,
|
||||||
previous_state: &PreviousEntityState,
|
previous_state: &PreviousEntityState,
|
||||||
vel: Vec3<f32>,
|
vel: Vec3<f32>,
|
||||||
loadout: Option<&Loadout>,
|
|
||||||
) -> SfxEvent {
|
) -> 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
|
// Match run state
|
||||||
if physics_state.on_ground && vel.magnitude() > 0.1
|
if physics_state.on_ground && vel.magnitude() > 0.1
|
||||||
|| !previous_state.on_ground && physics_state.on_ground
|
|| !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
|
/// 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
|
/// at a volume appropriate fot the entity we are emitting the event for
|
||||||
fn get_volume_for_body_type(body: &Body) -> f32 {
|
fn get_volume_for_body_type(body: &Body) -> f32 {
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use common::{
|
use common::{
|
||||||
assets,
|
|
||||||
comp::{
|
comp::{
|
||||||
bird_small, humanoid,
|
bird_small, humanoid, quadruped_medium, quadruped_small, Body, CharacterState, PhysicsState,
|
||||||
item::tool::{AxeKind, BowKind, ToolKind},
|
|
||||||
quadruped_medium, quadruped_small, Body, CharacterState, ItemConfig, Loadout, PhysicsState,
|
|
||||||
},
|
},
|
||||||
event::SfxEvent,
|
event::SfxEvent,
|
||||||
states,
|
states,
|
||||||
@ -30,7 +27,6 @@ fn config_but_played_since_threshold_no_emit() {
|
|||||||
let previous_state = PreviousEntityState {
|
let previous_state = PreviousEntityState {
|
||||||
event: SfxEvent::Run,
|
event: SfxEvent::Run,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: true,
|
on_ground: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,7 +46,6 @@ fn config_and_not_played_since_threshold_emits() {
|
|||||||
let previous_state = PreviousEntityState {
|
let previous_state = PreviousEntityState {
|
||||||
event: SfxEvent::Idle,
|
event: SfxEvent::Idle,
|
||||||
time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(),
|
time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: true,
|
on_ground: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,7 +67,6 @@ fn same_previous_event_elapsed_emits() {
|
|||||||
time: Instant::now()
|
time: Instant::now()
|
||||||
.checked_sub(Duration::from_millis(500))
|
.checked_sub(Duration::from_millis(500))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: true,
|
on_ground: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,11 +90,9 @@ fn maps_idle() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Idle,
|
event: SfxEvent::Idle,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: true,
|
on_ground: true,
|
||||||
},
|
},
|
||||||
Vec3::zero(),
|
Vec3::zero(),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::Idle);
|
assert_eq!(result, SfxEvent::Idle);
|
||||||
@ -120,11 +112,9 @@ fn maps_run_with_sufficient_velocity() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Idle,
|
event: SfxEvent::Idle,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: true,
|
on_ground: true,
|
||||||
},
|
},
|
||||||
Vec3::new(0.5, 0.8, 0.0),
|
Vec3::new(0.5, 0.8, 0.0),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::Run);
|
assert_eq!(result, SfxEvent::Run);
|
||||||
@ -144,11 +134,9 @@ fn does_not_map_run_with_insufficient_velocity() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Idle,
|
event: SfxEvent::Idle,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: true,
|
on_ground: true,
|
||||||
},
|
},
|
||||||
Vec3::new(0.02, 0.0001, 0.0),
|
Vec3::new(0.02, 0.0001, 0.0),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::Idle);
|
assert_eq!(result, SfxEvent::Idle);
|
||||||
@ -168,11 +156,9 @@ fn does_not_map_run_with_sufficient_velocity_but_not_on_ground() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Idle,
|
event: SfxEvent::Idle,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: false,
|
on_ground: false,
|
||||||
},
|
},
|
||||||
Vec3::new(0.5, 0.8, 0.0),
|
Vec3::new(0.5, 0.8, 0.0),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::Idle);
|
assert_eq!(result, SfxEvent::Idle);
|
||||||
@ -195,11 +181,9 @@ fn maps_roll() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Run,
|
event: SfxEvent::Run,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: true,
|
on_ground: true,
|
||||||
},
|
},
|
||||||
Vec3::zero(),
|
Vec3::zero(),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::Roll);
|
assert_eq!(result, SfxEvent::Roll);
|
||||||
@ -219,11 +203,9 @@ fn maps_land_on_ground_to_run() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Idle,
|
event: SfxEvent::Idle,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: false,
|
on_ground: false,
|
||||||
},
|
},
|
||||||
Vec3::zero(),
|
Vec3::zero(),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::Run);
|
assert_eq!(result, SfxEvent::Run);
|
||||||
@ -243,11 +225,9 @@ fn maps_glider_open() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Jump,
|
event: SfxEvent::Jump,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: false,
|
on_ground: false,
|
||||||
},
|
},
|
||||||
Vec3::zero(),
|
Vec3::zero(),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::GliderOpen);
|
assert_eq!(result, SfxEvent::GliderOpen);
|
||||||
@ -267,11 +247,9 @@ fn maps_glide() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Glide,
|
event: SfxEvent::Glide,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: false,
|
on_ground: false,
|
||||||
},
|
},
|
||||||
Vec3::zero(),
|
Vec3::zero(),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::Glide);
|
assert_eq!(result, SfxEvent::Glide);
|
||||||
@ -291,11 +269,9 @@ fn maps_glider_close_when_closing_mid_flight() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Glide,
|
event: SfxEvent::Glide,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: false,
|
on_ground: false,
|
||||||
},
|
},
|
||||||
Vec3::zero(),
|
Vec3::zero(),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::GliderClose);
|
assert_eq!(result, SfxEvent::GliderClose);
|
||||||
@ -316,88 +292,14 @@ fn maps_glider_close_when_landing() {
|
|||||||
&PreviousEntityState {
|
&PreviousEntityState {
|
||||||
event: SfxEvent::Glide,
|
event: SfxEvent::Glide,
|
||||||
time: Instant::now(),
|
time: Instant::now(),
|
||||||
weapon_drawn: false,
|
|
||||||
on_ground: false,
|
on_ground: false,
|
||||||
},
|
},
|
||||||
Vec3::zero(),
|
Vec3::zero(),
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(result, SfxEvent::GliderClose);
|
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]
|
#[test]
|
||||||
fn maps_quadrupeds_running() {
|
fn maps_quadrupeds_running() {
|
||||||
let result = MovementEventMapper::map_non_humanoid_movement_event(
|
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
|
//! Manages individual sfx event system, listens for sfx events, and requests
|
||||||
/// SFX events and plays the sound at the requested position, or the current
|
//! playback at the requested position and volume
|
||||||
/// player position
|
//!
|
||||||
|
//! 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;
|
mod event_mapper;
|
||||||
|
|
||||||
use crate::audio::AudioFrontend;
|
use crate::audio::AudioFrontend;
|
||||||
@ -16,19 +95,22 @@ use serde::Deserialize;
|
|||||||
use specs::WorldExt;
|
use specs::WorldExt;
|
||||||
use vek::*;
|
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)]
|
#[derive(Deserialize)]
|
||||||
pub struct SfxTriggerItem {
|
pub struct SfxTriggerItem {
|
||||||
pub files: Vec<String>,
|
pub files: Vec<String>,
|
||||||
pub threshold: f64,
|
pub threshold: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct SfxTriggers(HashMap<SfxEvent, SfxTriggerItem>);
|
pub struct SfxTriggers(HashMap<SfxEvent, SfxTriggerItem>);
|
||||||
|
|
||||||
impl Default for SfxTriggers {
|
|
||||||
fn default() -> Self { Self(HashMap::new()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SfxTriggers {
|
impl SfxTriggers {
|
||||||
pub fn get_trigger(&self, trigger: &SfxEvent) -> Option<&SfxTriggerItem> { self.0.get(trigger) }
|
pub fn get_trigger(&self, trigger: &SfxEvent) -> Option<&SfxTriggerItem> { self.0.get(trigger) }
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user