Add initial attack sfx code with bow shot sounds.

This commit is contained in:
Shane Handley 2020-04-03 19:13:30 +11:00
parent 54960142e2
commit d5cc5c8537
12 changed files with 315 additions and 160 deletions

View File

@ -23,6 +23,15 @@
],
threshold: 0.5,
),
Attack(Bow(BasicBow)): (
files: [
"voxygen.audio.sfx.weapon.bow_attack_01",
"voxygen.audio.sfx.weapon.bow_attack_02",
"voxygen.audio.sfx.weapon.bow_attack_03",
"voxygen.audio.sfx.weapon.bow_attack_04",
],
threshold: 0.5,
),
Wield(Sword(BasicSword)): (
files: [
"voxygen.audio.sfx.weapon.sword_out",

BIN
assets/voxygen/audio/sfx/weapon/bow_attack_01.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/weapon/bow_attack_02.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/weapon/bow_attack_03.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/weapon/bow_attack_04.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -39,6 +39,7 @@ pub enum SfxEvent {
Fall,
ExperienceGained,
LevelUp,
Attack(ToolKind),
Wield(ToolKind),
Unwield(ToolKind),
Inventory(InventoryUpdateEvent),

View File

@ -0,0 +1,179 @@
/// event_mapper::combat watches the combat state of entities and emits
/// associated sfx events
use crate::audio::sfx::{SfxTriggerItem, SfxTriggers};
use common::{
comp::{
item::{Item, ItemKind},
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 CombatEventMapper {
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;
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);
}
/// 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()
});
}
/// When specific entity movements are detected, the associated sound (if
/// any) needs to satisfy two conditions to be allowed to play:
/// 1. An sfx.ron entry exists for the movement (we need to know which sound
/// file(s) to play) 2. The sfx has not been played since it's timeout
/// threshold has elapsed, which prevents firing every tick
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(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(data.kind)),
(true, false, false) => Some(SfxEvent::Unwield(data.kind)),
_ => None,
} {
return wield_event;
}
}
}
}
SfxEvent::Idle
}
/// This helps us determine whether we should be emitting the Wield/Unwield
/// events. For now, consider either CharacterState::Wielding or
/// ::Equipping to mean the weapon is drawn. This will need updating if the
/// animations change to match the wield_duration associated with the weapon
fn weapon_drawn(character: &CharacterState) -> bool {
character.is_wield()
|| match character {
CharacterState::Equipping { .. } => true,
_ => false,
}
}
}
#[cfg(test)] mod tests;

View File

@ -0,0 +1,98 @@
use super::*;
use common::{
assets,
comp::{
item::tool::{AxeKind, BowKind, ToolKind},
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.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(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.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(ToolKind::Bow(BowKind::BasicBow)));
}
#[test]
fn maps_basic_melee() {
let mut loadout = Loadout::default();
loadout.active_item = Some(ItemConfig {
item: assets::load_expect_cloned("common.items.weapons.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(ToolKind::Axe(AxeKind::BasicAxe)));
}

View File

@ -1,23 +1,27 @@
mod combat;
mod movement;
mod progression;
use common::state::State;
use combat::CombatEventMapper;
use movement::MovementEventMapper;
use progression::ProgressionEventMapper;
use super::SfxTriggers;
pub struct SfxEventMapper {
progression_event_mapper: ProgressionEventMapper,
movement_event_mapper: MovementEventMapper,
progression: ProgressionEventMapper,
movement: MovementEventMapper,
combat: CombatEventMapper,
}
impl SfxEventMapper {
pub fn new() -> Self {
Self {
progression_event_mapper: ProgressionEventMapper::new(),
movement_event_mapper: MovementEventMapper::new(),
progression: ProgressionEventMapper::new(),
combat: CombatEventMapper::new(),
movement: MovementEventMapper::new(),
}
}
@ -27,9 +31,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);
self.progression.maintain(state, player_entity, triggers);
self.movement.maintain(state, player_entity, triggers);
self.combat.maintain(state, player_entity, triggers);
}
}

View File

@ -5,8 +5,7 @@ use crate::audio::sfx::{SfxTriggerItem, SfxTriggers};
use common::{
comp::{
item::{Item, ItemKind},
Body, CharacterState, ItemConfig, Loadout, PhysicsState, Pos, Vel,
Body, CharacterState, PhysicsState, Pos, Vel,
},
event::{EventBus, SfxEvent, SfxEventItem},
state::State,
@ -20,7 +19,6 @@ use vek::*;
struct PreviousEntityState {
event: SfxEvent,
time: Instant,
weapon_drawn: bool,
on_ground: bool,
}
@ -29,7 +27,6 @@ impl Default for PreviousEntityState {
Self {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
on_ground: true,
}
}
@ -58,13 +55,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 +75,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,7 +98,6 @@ 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;
}
}
@ -157,33 +150,7 @@ impl MovementEventMapper {
physics_state: &PhysicsState,
previous_state: &PreviousEntityState,
vel: Vec3<f32>,
loadout: Option<&Loadout>,
) -> SfxEvent {
// Handle wield state changes
if let Some(active_loadout) = loadout {
if let Some(ItemConfig {
item:
Item {
kind: ItemKind::Tool(data),
..
},
..
}) = active_loadout.active_item
{
if let Some(wield_event) = match (
previous_state.weapon_drawn,
character_state.is_dodge(),
Self::weapon_drawn(character_state),
) {
(false, false, true) => Some(SfxEvent::Wield(data.kind)),
(true, false, false) => Some(SfxEvent::Unwield(data.kind)),
_ => None,
} {
return wield_event;
}
}
}
// Match run state
if physics_state.on_ground && vel.magnitude() > 0.1
|| !previous_state.on_ground && physics_state.on_ground
@ -216,18 +183,6 @@ impl MovementEventMapper {
}
}
/// This helps us determine whether we should be emitting the Wield/Unwield
/// events. For now, consider either CharacterState::Wielding or
/// ::Equipping to mean the weapon is drawn. This will need updating if the
/// animations change to match the wield_duration associated with the weapon
fn weapon_drawn(character: &CharacterState) -> bool {
character.is_wield()
|| match character {
CharacterState::Equipping { .. } => true,
_ => false,
}
}
/// Returns a relative volume value for a body type. This helps us emit sfx
/// at a volume appropriate fot the entity we are emitting the event for
fn get_volume_for_body_type(body: &Body) -> f32 {

View File

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

View File

@ -22,13 +22,9 @@ pub struct SfxTriggerItem {
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) }