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 sfx_trigger_item = triggers.get_key_value(&mapped_event);
106 audio.emit_sfx(
107 sfx_trigger_item,
108 pos.0,
109 Some(Self::get_volume_for_body_type(body)),
110 );
111 internal_state.time = Instant::now();
112 internal_state.steps_taken = 0.0;
113 }
114
115 internal_state.event = mapped_event;
118 internal_state.on_ground = physics.on_ground.is_some();
119 internal_state.in_water = physics.in_liquid().is_some();
120 let dt = ecs.fetch::<DeltaTime>().0;
121 internal_state.steps_taken +=
122 vel.0.magnitude() * dt / (body.stride_length() * scale.map_or(1.0, |s| s.0));
123 }
124 }
125
126 self.cleanup(player_entity);
127 }
128}
129
130impl MovementEventMapper {
131 pub fn new() -> Self {
132 Self {
133 event_history: HashMap::new(),
134 }
135 }
136
137 fn cleanup(&mut self, player: EcsEntity) {
143 const TRACKING_TIMEOUT: u64 = 10;
144
145 let now = Instant::now();
146 self.event_history.retain(|entity, event| {
147 now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
148 || entity.id() == player.id()
149 });
150 }
151
152 fn should_emit(
160 previous_state: &PreviousEntityState,
161 sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
162 ) -> bool {
163 if let Some((event, item)) = sfx_trigger_item {
164 if &previous_state.event == event {
165 match event {
166 SfxEvent::Run(_) => previous_state.steps_taken >= item.threshold,
167 SfxEvent::Climb => previous_state.steps_taken >= item.threshold,
168 SfxEvent::QuadRun(_) => previous_state.steps_taken >= item.threshold,
169 _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold,
170 }
171 } else {
172 true
173 }
174 } else {
175 false
176 }
177 }
178
179 fn map_movement_event(
185 character_state: &CharacterState,
186 physics_state: &PhysicsState,
187 previous_state: &PreviousEntityState,
188 vel: Vec3<f32>,
189 underfoot_block_kind: BlockKind,
190 ) -> SfxEvent {
191 if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0
193 || !previous_state.in_water && physics_state.in_liquid().is_some()
194 {
195 return SfxEvent::Swim;
196 } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1
197 || !previous_state.on_ground && physics_state.on_ground.is_some()
198 {
199 return if let CharacterState::Roll(data) = character_state {
200 if data.static_data.was_cancel {
201 SfxEvent::RollCancel
202 } else {
203 SfxEvent::Roll
204 }
205 } else if character_state.is_stealthy() {
206 SfxEvent::Sneak
207 } else {
208 match underfoot_block_kind {
209 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
210 BlockKind::Rock
211 | BlockKind::WeakRock
212 | BlockKind::GlowingRock
213 | BlockKind::GlowingWeakRock
214 | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
215 BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
216 BlockKind::Air => SfxEvent::Idle,
218 _ => SfxEvent::Run(BlockKind::Grass),
219 }
220 };
221 }
222
223 match (previous_state.event.clone(), character_state) {
225 (_, CharacterState::Climb { .. }) => SfxEvent::Climb,
226 (_, CharacterState::Glide(glide))
227 if matches!(glide.booster, Some(states::glide::Boost::Forward(_))) =>
228 {
229 if thread_rng().gen_bool(0.5) {
230 SfxEvent::FlameThrower
231 } else {
232 SfxEvent::Idle
233 }
234 },
235 (_, CharacterState::Glide(_)) => SfxEvent::Glide,
236 _ => SfxEvent::Idle,
237 }
238 }
239
240 fn map_non_humanoid_movement_event(
242 physics_state: &PhysicsState,
243 vel: Vec3<f32>,
244 underfoot_block_kind: BlockKind,
245 ) -> SfxEvent {
246 if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0 {
247 SfxEvent::Swim
248 } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1 {
249 match underfoot_block_kind {
250 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
251 BlockKind::Rock
252 | BlockKind::WeakRock
253 | BlockKind::GlowingRock
254 | BlockKind::GlowingWeakRock
255 | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
256 BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
258 BlockKind::Air => SfxEvent::Idle,
259 _ => SfxEvent::Run(BlockKind::Grass),
260 }
261 } else {
262 SfxEvent::Idle
263 }
264 }
265
266 fn map_quadruped_movement_event(
268 physics_state: &PhysicsState,
269 vel: Vec3<f32>,
270 underfoot_block_kind: BlockKind,
271 ) -> SfxEvent {
272 if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0 {
273 SfxEvent::Swim
274 } else if physics_state.on_ground.is_some() && vel.magnitude() > 0.1 {
275 match underfoot_block_kind {
276 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::QuadRun(BlockKind::Snow),
277 BlockKind::Rock
278 | BlockKind::WeakRock
279 | BlockKind::GlowingRock
280 | BlockKind::GlowingWeakRock
281 | BlockKind::Ice => SfxEvent::QuadRun(BlockKind::Rock),
282 BlockKind::Earth => SfxEvent::QuadRun(BlockKind::Earth),
284 BlockKind::Air => SfxEvent::Idle,
285 _ => SfxEvent::QuadRun(BlockKind::Grass),
286 }
287 } else {
288 SfxEvent::Idle
289 }
290 }
291
292 fn get_volume_for_body_type(body: &Body) -> f32 {
295 match body {
296 Body::Humanoid(_) => 0.9,
297 Body::QuadrupedSmall(_) => 0.3,
298 Body::QuadrupedMedium(_) => 0.7,
299 Body::QuadrupedLow(_) => 0.7,
300 Body::BirdMedium(_) => 0.3,
301 Body::BirdLarge(_) => 0.2,
302 Body::BipedLarge(_) => 1.0,
303 _ => 0.9,
304 }
305 }
306}
307
308#[cfg(test)] mod tests;