veloren_voxygen/audio/sfx/event_mapper/movement/
mod.rs1use super::EventMapper;
5use crate::{
6 AudioFrontend,
7 audio::sfx::{SFX_DIST_LIMIT_SQR, SfxEvent, SfxTriggerItem, SfxTriggers},
8 scene::{Camera, Terrain},
9};
10use client::Client;
11use common::{
12 comp::{Body, CharacterState, PhysicsState, Pos, Scale, Vel},
13 resources::DeltaTime,
14 states,
15 terrain::{BlockKind, TerrainChunk},
16};
17use common_state::State;
18use hashbrown::HashMap;
19use rand::prelude::*;
20use specs::{Entity as EcsEntity, Join, LendJoin, WorldExt};
21use std::time::{Duration, Instant};
22use vek::*;
23
24#[derive(Clone)]
25struct PreviousEntityState {
26 event: SfxEvent,
27 time: Instant,
28 on_ground: bool,
29 in_water: bool,
30 steps_taken: f32,
31}
32
33impl Default for PreviousEntityState {
34 fn default() -> Self {
35 Self {
36 event: SfxEvent::Idle,
37 time: Instant::now(),
38 on_ground: true,
39 in_water: false,
40 steps_taken: 0.0,
41 }
42 }
43}
44
45pub struct MovementEventMapper {
46 event_history: HashMap<EcsEntity, PreviousEntityState>,
47}
48
49impl EventMapper for MovementEventMapper {
50 fn maintain(
51 &mut self,
52 audio: &mut AudioFrontend,
53 state: &State,
54 player_entity: specs::Entity,
55 camera: &Camera,
56 triggers: &SfxTriggers,
57 _terrain: &Terrain<TerrainChunk>,
58 _client: &Client,
59 ) {
60 let ecs = state.ecs();
61
62 let cam_pos = camera.get_pos_with_focus();
63
64 for (entity, pos, vel, body, scale, physics, character) in (
65 &ecs.entities(),
66 &ecs.read_storage::<Pos>(),
67 &ecs.read_storage::<Vel>(),
68 &ecs.read_storage::<Body>(),
69 ecs.read_storage::<Scale>().maybe(),
70 &ecs.read_storage::<PhysicsState>(),
71 ecs.read_storage::<CharacterState>().maybe(),
72 )
73 .join()
74 .filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
75 {
76 if let Some(character) = character {
77 let internal_state = self.event_history.entry(entity).or_default();
78
79 let block_position = Vec3::new(pos.0.x, pos.0.y, pos.0.z - 1.0).map(|x| x as i32);
81 let underfoot_block_kind = match state.get_block(block_position) {
82 Some(block) => block.kind(),
83 None => BlockKind::Air,
84 };
85
86 let mapped_event = match body {
87 Body::Humanoid(_) => Self::map_movement_event(
88 character,
89 physics,
90 internal_state,
91 vel.0,
92 underfoot_block_kind,
93 ),
94 Body::QuadrupedMedium(_) | Body::QuadrupedSmall(_) | Body::QuadrupedLow(_) => {
95 Self::map_quadruped_movement_event(physics, vel.0, underfoot_block_kind)
96 },
97 Body::BirdMedium(_) | Body::BirdLarge(_) | Body::BipedLarge(_) => {
98 Self::map_non_humanoid_movement_event(physics, vel.0, underfoot_block_kind)
99 },
100 _ => SfxEvent::Idle, };
102
103 if Self::should_emit(internal_state, triggers.get_key_value(&mapped_event))
105 && let Some(player_pos) = state.read_component_copied::<Pos>(player_entity)
106 {
107 let sfx_trigger_item = triggers.get_key_value(&mapped_event);
108 audio.emit_sfx(
109 sfx_trigger_item,
110 pos.0,
111 Some(Self::get_volume_for_body_type(body)),
112 player_pos.0,
113 );
114 internal_state.time = Instant::now();
115 internal_state.steps_taken = 0.0;
116 }
117
118 internal_state.event = mapped_event;
121 internal_state.on_ground = physics.on_ground.is_some();
122 internal_state.in_water = physics.in_liquid().is_some();
123 let dt = ecs.fetch::<DeltaTime>().0;
124 internal_state.steps_taken +=
125 vel.0.magnitude() * dt / (body.stride_length() * scale.map_or(1.0, |s| s.0));
126 }
127 }
128
129 self.cleanup(player_entity);
130 }
131}
132
133impl MovementEventMapper {
134 pub fn new() -> Self {
135 Self {
136 event_history: HashMap::new(),
137 }
138 }
139
140 fn cleanup(&mut self, player: EcsEntity) {
146 const TRACKING_TIMEOUT: u64 = 10;
147
148 let now = Instant::now();
149 self.event_history.retain(|entity, event| {
150 now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
151 || entity.id() == player.id()
152 });
153 }
154
155 fn should_emit(
163 previous_state: &PreviousEntityState,
164 sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
165 ) -> bool {
166 if let Some((event, item)) = sfx_trigger_item {
167 if &previous_state.event == event {
168 match event {
169 SfxEvent::Run(_) => previous_state.steps_taken >= item.threshold,
170 SfxEvent::Climb => previous_state.steps_taken >= item.threshold,
171 SfxEvent::QuadRun(_) => previous_state.steps_taken >= item.threshold,
172 _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold,
173 }
174 } else {
175 true
176 }
177 } else {
178 false
179 }
180 }
181
182 fn map_movement_event(
188 character_state: &CharacterState,
189 physics_state: &PhysicsState,
190 previous_state: &PreviousEntityState,
191 vel: Vec3<f32>,
192 underfoot_block_kind: BlockKind,
193 ) -> SfxEvent {
194 if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0
196 || !previous_state.in_water && physics_state.in_liquid().is_some()
197 {
198 return SfxEvent::Swim;
199 } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1
200 || !previous_state.on_ground && physics_state.on_ground.is_some()
201 {
202 return if let CharacterState::Roll(data) = character_state {
203 if data.static_data.was_cancel {
204 SfxEvent::RollCancel
205 } else {
206 SfxEvent::Roll
207 }
208 } else if character_state.is_stealthy() {
209 SfxEvent::Sneak
210 } else {
211 match underfoot_block_kind {
212 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
213 BlockKind::Rock
214 | BlockKind::WeakRock
215 | BlockKind::GlowingRock
216 | BlockKind::GlowingWeakRock
217 | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
218 BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
219 BlockKind::Air => SfxEvent::Idle,
221 _ => SfxEvent::Run(BlockKind::Grass),
222 }
223 };
224 }
225
226 match (previous_state.event.clone(), character_state) {
228 (_, CharacterState::Climb { .. }) => SfxEvent::Climb,
229 (_, CharacterState::Glide(glide))
230 if matches!(glide.booster, Some(states::glide::Boost::Forward(_))) =>
231 {
232 if thread_rng().gen_bool(0.5) {
233 SfxEvent::FlameThrower
234 } else {
235 SfxEvent::Idle
236 }
237 },
238 (_, CharacterState::Glide(_)) => SfxEvent::Glide,
239 _ => SfxEvent::Idle,
240 }
241 }
242
243 fn map_non_humanoid_movement_event(
245 physics_state: &PhysicsState,
246 vel: Vec3<f32>,
247 underfoot_block_kind: BlockKind,
248 ) -> SfxEvent {
249 if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0 {
250 SfxEvent::Swim
251 } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1 {
252 match underfoot_block_kind {
253 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
254 BlockKind::Rock
255 | BlockKind::WeakRock
256 | BlockKind::GlowingRock
257 | BlockKind::GlowingWeakRock
258 | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
259 BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
261 BlockKind::Air => SfxEvent::Idle,
262 _ => SfxEvent::Run(BlockKind::Grass),
263 }
264 } else {
265 SfxEvent::Idle
266 }
267 }
268
269 fn map_quadruped_movement_event(
271 physics_state: &PhysicsState,
272 vel: Vec3<f32>,
273 underfoot_block_kind: BlockKind,
274 ) -> SfxEvent {
275 if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0 {
276 SfxEvent::Swim
277 } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1 {
278 match underfoot_block_kind {
279 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::QuadRun(BlockKind::Snow),
280 BlockKind::Rock
281 | BlockKind::WeakRock
282 | BlockKind::GlowingRock
283 | BlockKind::GlowingWeakRock
284 | BlockKind::Ice => SfxEvent::QuadRun(BlockKind::Rock),
285 BlockKind::Earth => SfxEvent::QuadRun(BlockKind::Earth),
287 BlockKind::Air => SfxEvent::Idle,
288 _ => SfxEvent::QuadRun(BlockKind::Grass),
289 }
290 } else {
291 SfxEvent::Idle
292 }
293 }
294
295 fn get_volume_for_body_type(body: &Body) -> f32 {
298 match body {
299 Body::Humanoid(_) => 0.9,
300 Body::QuadrupedSmall(_) => 0.3,
301 Body::QuadrupedMedium(_) => 0.7,
302 Body::QuadrupedLow(_) => 0.7,
303 Body::BirdMedium(_) => 0.3,
304 Body::BirdLarge(_) => 0.2,
305 Body::BipedLarge(_) => 1.0,
306 _ => 0.9,
307 }
308 }
309}
310
311#[cfg(test)] mod tests;