1use super::EventMapper;
5use crate::{
6 AudioFrontend,
7 audio::sfx::{SFX_DIST_LIMIT_SQR, SfxEvent, SfxTag, SfxTriggerItem, SfxTriggers},
8 ecs::comp::{Footsteps, Interpolated},
9 scene::{Camera, FigureMgr, Terrain},
10};
11use client::Client;
12use common::{
13 comp::{Body, CharacterState, PhysicsState, Scale, Vel},
14 resources::DeltaTime,
15 states,
16 terrain::{BlockKind, TerrainChunk},
17};
18use common_state::State;
19use hashbrown::HashMap;
20use rand::prelude::*;
21use specs::{Entity as EcsEntity, Join, LendJoin, WorldExt};
22use std::time::{Duration, Instant};
23use vek::*;
24
25#[derive(Clone)]
26struct PreviousEntityState {
27 event: SfxEvent,
28 time: Instant,
29 on_ground: bool,
30 in_water: bool,
31 steps_taken: f32,
32 is_stepping: Option<bool>,
33}
34
35impl Default for PreviousEntityState {
36 fn default() -> Self {
37 Self {
38 event: SfxEvent::Idle,
39 time: Instant::now(),
40 on_ground: true,
41 in_water: false,
42 steps_taken: 0.0,
43 is_stepping: None,
44 }
45 }
46}
47
48pub struct MovementEventMapper {
49 event_history: HashMap<EcsEntity, PreviousEntityState>,
50}
51
52impl EventMapper for MovementEventMapper {
53 fn maintain(
54 &mut self,
55 audio: &mut AudioFrontend,
56 state: &State,
57 player_entity: specs::Entity,
58 camera: &Camera,
59 triggers: &SfxTriggers,
60 _terrain: &Terrain<TerrainChunk>,
61 _client: &Client,
62 figure_mgr: &FigureMgr,
63 ) {
64 let ecs = state.ecs();
65
66 let cam_pos = camera.get_pos_with_focus();
67
68 let mut footsteps = ecs.write_storage::<Footsteps>();
69 for (entity, interpolated, vel, body, scale, physics, character) in (
70 &ecs.entities(),
71 &ecs.read_storage::<Interpolated>(),
72 &ecs.read_storage::<Vel>(),
73 &ecs.read_storage::<Body>(),
74 ecs.read_storage::<Scale>().maybe(),
75 &ecs.read_storage::<PhysicsState>(),
76 ecs.read_storage::<CharacterState>().maybe(),
77 )
78 .join()
79 {
80 if physics.on_ground.is_some()
83 && let Ok(footsteps) = footsteps.entry(entity)
84 {
85 macro_rules! update_footsteps {
87 ($($name:ident: $body:ident => [$($foot:ident),* $(,)?]),* $(,)?) => {
88 match body {
89 $(Body::$body(_) => {
90 if let Some(state) = figure_mgr.states.$name.get(&entity) {
91 let feet_pos = [$(state.computed_skeleton.$foot,)*].map(|f| {
92 -interpolated.ori.global_to_local(f.mul_point(Vec3::new(0.0, 1.0, -1.0))).y
93 });
94 footsteps
95 .or_insert_with(Default::default)
96 .update(&feet_pos);
97 }
98 },)*
99 _ => {},
100 }
101 };
102 }
103
104 update_footsteps!(
105 character_states: Humanoid => [foot_l, foot_r],
106 biped_large_states: BipedLarge => [foot_l, foot_r],
107 quadruped_medium_states: QuadrupedMedium => [foot_fl, foot_fr, foot_bl, foot_br],
108 quadruped_small_states: QuadrupedSmall => [leg_fl, leg_fr, leg_bl, leg_br],
109 quadruped_low_states: QuadrupedLow => [foot_fl, foot_fr, foot_bl, foot_br],
110 bird_medium_states: BirdMedium => [leg_l, leg_r],
111 bird_large_states: BirdLarge => [foot_l, foot_r],
112 arthropod_states: Arthropod => [
113 leg_fl,
114 leg_fr,
115 leg_fcl,
116 leg_fcr,
117 leg_bcl,
118 leg_bcr,
119 leg_bl,
120 leg_br,
121 ],
122 );
123 }
124
125 if interpolated.pos.distance_squared(cam_pos) < SFX_DIST_LIMIT_SQR
126 && let Some(character) = character
127 {
128 let internal_state = self.event_history.entry(entity).or_default();
129
130 let mapped_event = match body {
131 Body::Humanoid(_) => {
132 Self::map_movement_event(character, physics, internal_state, vel.0)
133 },
134 Body::QuadrupedMedium(_) | Body::QuadrupedSmall(_) | Body::QuadrupedLow(_) => {
135 Self::map_quadruped_movement_event(physics, internal_state, vel.0)
136 },
137 Body::BirdMedium(_) | Body::BirdLarge(_) | Body::BipedLarge(_) => {
138 Self::map_non_humanoid_movement_event(physics, internal_state, vel.0)
139 },
140 Body::Arthropod(_) => {
141 Self::map_arthropod_movement_event(physics, internal_state, vel.0)
142 },
143 _ => SfxEvent::Idle, };
145
146 if Self::should_emit(
148 internal_state,
149 triggers.0.get_key_value(&mapped_event),
150 physics,
151 ) {
152 let sfx_trigger_item = triggers.0.get_key_value(&mapped_event);
153 audio.emit_sfx(
154 sfx_trigger_item,
155 interpolated.pos,
156 Some(Self::get_volume_for_body_type(body)),
157 SfxTag::Footstep,
158 );
159 internal_state.time = Instant::now();
160 internal_state.steps_taken = 0.0;
161 }
162
163 internal_state.event = mapped_event;
166 internal_state.on_ground = physics.on_ground.is_some();
167 internal_state.in_water = physics.in_liquid().is_some();
168 let dt = ecs.fetch::<DeltaTime>().0;
169 internal_state.steps_taken +=
170 vel.0.magnitude() * dt / (body.stride_length() * scale.map_or(1.0, |s| s.0));
171 internal_state.is_stepping = footsteps.get(entity).map(|f| f.is_any_stepping());
172 }
173 }
174
175 self.cleanup(player_entity);
176 }
177}
178
179impl MovementEventMapper {
180 pub fn new() -> Self {
181 Self {
182 event_history: HashMap::new(),
183 }
184 }
185
186 fn cleanup(&mut self, player: EcsEntity) {
192 const TRACKING_TIMEOUT: u64 = 10;
193
194 let now = Instant::now();
195 self.event_history.retain(|entity, event| {
196 now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
197 || entity.id() == player.id()
198 });
199 }
200
201 fn should_emit(
209 previous_state: &PreviousEntityState,
210 sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
211 physics: &PhysicsState,
212 ) -> bool {
213 if let Some((event, item)) = sfx_trigger_item {
214 let landed = physics.on_ground.is_some() && !previous_state.on_ground;
215 landed
217 || previous_state.is_stepping
218 .filter(|_| matches!(event, SfxEvent::Run(_) | SfxEvent::OctoRun(_) | SfxEvent::QuadRun(_)))
219 .unwrap_or_else(|| match event {
221 SfxEvent::Climb => previous_state.steps_taken >= item.threshold,
222 _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold
223 })
224 } else {
225 false
226 }
227 }
228
229 fn map_movement_event(
235 character_state: &CharacterState,
236 physics_state: &PhysicsState,
237 previous_state: &PreviousEntityState,
238 vel: Vec3<f32>,
239 ) -> SfxEvent {
240 if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0
242 || !previous_state.in_water && physics_state.in_liquid().is_some()
243 {
244 return SfxEvent::Swim;
245 } else if let Some(block) = physics_state.on_ground
246 && (vel.magnitude() > 0.1 || !previous_state.on_ground)
247 {
248 return if let CharacterState::Roll(data) = character_state {
249 if data.static_data.was_cancel {
250 SfxEvent::RollCancel
251 } else {
252 SfxEvent::Roll
253 }
254 } else if character_state.is_stealthy() {
255 SfxEvent::Sneak
256 } else {
257 match block.kind() {
258 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
259 BlockKind::Rock
260 | BlockKind::WeakRock
261 | BlockKind::GlowingRock
262 | BlockKind::GlowingWeakRock
263 | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
264 BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
265 BlockKind::Air => SfxEvent::Idle,
267 _ => SfxEvent::Run(BlockKind::Grass),
268 }
269 };
270 }
271
272 match (previous_state.event.clone(), character_state) {
274 (_, CharacterState::Climb { .. }) => SfxEvent::Climb,
275 (_, CharacterState::Glide(glide))
276 if matches!(glide.booster, Some(states::glide::Boost::Forward(_))) =>
277 {
278 if rand::rng().random_bool(0.5) {
279 SfxEvent::FlameThrower
280 } else {
281 SfxEvent::Idle
282 }
283 },
284 (_, CharacterState::Glide(_)) => SfxEvent::Glide,
285 _ => SfxEvent::Idle,
286 }
287 }
288
289 fn map_non_humanoid_movement_event(
291 physics_state: &PhysicsState,
292 previous_state: &PreviousEntityState,
293 vel: Vec3<f32>,
294 ) -> SfxEvent {
295 if physics_state.in_liquid().is_some()
296 && (vel.magnitude() > 2.0 || !previous_state.on_ground)
297 {
298 SfxEvent::Swim
299 } else if let Some(block) = physics_state.on_ground
300 && vel.magnitude() > 0.1
301 {
302 match block.kind() {
303 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
304 BlockKind::Rock
305 | BlockKind::WeakRock
306 | BlockKind::GlowingRock
307 | BlockKind::GlowingWeakRock
308 | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
309 BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
311 BlockKind::Air => SfxEvent::Idle,
312 _ => SfxEvent::Run(BlockKind::Grass),
313 }
314 } else {
315 SfxEvent::Idle
316 }
317 }
318
319 fn map_quadruped_movement_event(
321 physics_state: &PhysicsState,
322 previous_state: &PreviousEntityState,
323 vel: Vec3<f32>,
324 ) -> SfxEvent {
325 if physics_state.in_liquid().is_some()
326 && (vel.magnitude() > 2.0 || !previous_state.on_ground)
327 {
328 SfxEvent::Swim
329 } else if let Some(block) = physics_state.on_ground
330 && vel.magnitude() > 0.1
331 {
332 match block.kind() {
333 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::QuadRun(BlockKind::Snow),
334 BlockKind::Rock
335 | BlockKind::WeakRock
336 | BlockKind::GlowingRock
337 | BlockKind::GlowingWeakRock
338 | BlockKind::Ice => SfxEvent::QuadRun(BlockKind::Rock),
339 BlockKind::Earth => SfxEvent::QuadRun(BlockKind::Earth),
341 BlockKind::Air => SfxEvent::Idle,
342 _ => SfxEvent::QuadRun(BlockKind::Grass),
343 }
344 } else {
345 SfxEvent::Idle
346 }
347 }
348
349 fn map_arthropod_movement_event(
351 physics_state: &PhysicsState,
352 previous_state: &PreviousEntityState,
353 vel: Vec3<f32>,
354 ) -> SfxEvent {
355 if physics_state.in_liquid().is_some()
356 && (vel.magnitude() > 2.0 || !previous_state.on_ground)
357 {
358 SfxEvent::Swim
359 } else if let Some(block) = physics_state.on_ground
360 && vel.magnitude() > 0.1
361 {
362 match block.kind() {
363 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::OctoRun(BlockKind::Snow),
364 BlockKind::Rock
365 | BlockKind::WeakRock
366 | BlockKind::GlowingRock
367 | BlockKind::GlowingWeakRock
368 | BlockKind::Ice => SfxEvent::OctoRun(BlockKind::Rock),
369 BlockKind::Earth => SfxEvent::OctoRun(BlockKind::Earth),
371 BlockKind::Air => SfxEvent::Idle,
372 _ => SfxEvent::OctoRun(BlockKind::Grass),
373 }
374 } else {
375 SfxEvent::Idle
376 }
377 }
378
379 fn get_volume_for_body_type(body: &Body) -> f32 {
382 match body {
383 Body::Humanoid(_) => 0.5,
384 Body::QuadrupedSmall(_) => 0.2,
385 Body::QuadrupedMedium(_) => 0.7,
386 Body::QuadrupedLow(_) => 0.7,
387 Body::BirdMedium(_) => 0.3,
388 Body::BirdLarge(_) => 0.5,
389 Body::BipedLarge(_) => 1.0,
390 _ => 0.9,
391 }
392 }
393}
394
395#[cfg(test)] mod tests;