1use super::EventMapper;
5use crate::{
6 AudioFrontend,
7 audio::sfx::{SFX_DIST_LIMIT_SQR, SfxEvent, 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 );
158 internal_state.time = Instant::now();
159 internal_state.steps_taken = 0.0;
160 }
161
162 internal_state.event = mapped_event;
165 internal_state.on_ground = physics.on_ground.is_some();
166 internal_state.in_water = physics.in_liquid().is_some();
167 let dt = ecs.fetch::<DeltaTime>().0;
168 internal_state.steps_taken +=
169 vel.0.magnitude() * dt / (body.stride_length() * scale.map_or(1.0, |s| s.0));
170 internal_state.is_stepping = footsteps.get(entity).map(|f| f.is_any_stepping());
171 }
172 }
173
174 self.cleanup(player_entity);
175 }
176}
177
178impl MovementEventMapper {
179 pub fn new() -> Self {
180 Self {
181 event_history: HashMap::new(),
182 }
183 }
184
185 fn cleanup(&mut self, player: EcsEntity) {
191 const TRACKING_TIMEOUT: u64 = 10;
192
193 let now = Instant::now();
194 self.event_history.retain(|entity, event| {
195 now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
196 || entity.id() == player.id()
197 });
198 }
199
200 fn should_emit(
208 previous_state: &PreviousEntityState,
209 sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
210 physics: &PhysicsState,
211 ) -> bool {
212 if let Some((event, item)) = sfx_trigger_item {
213 let landed = physics.on_ground.is_some() && !previous_state.on_ground;
214 landed
216 || previous_state.is_stepping
217 .filter(|_| matches!(event, SfxEvent::Run(_) | SfxEvent::OctoRun(_) | SfxEvent::QuadRun(_)))
218 .unwrap_or_else(|| match event {
220 SfxEvent::Climb => previous_state.steps_taken >= item.threshold,
221 _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold
222 })
223 } else {
224 false
225 }
226 }
227
228 fn map_movement_event(
234 character_state: &CharacterState,
235 physics_state: &PhysicsState,
236 previous_state: &PreviousEntityState,
237 vel: Vec3<f32>,
238 ) -> SfxEvent {
239 if physics_state.in_liquid().is_some() && vel.magnitude() > 2.0
241 || !previous_state.in_water && physics_state.in_liquid().is_some()
242 {
243 return SfxEvent::Swim;
244 } else if let Some(block) = physics_state.on_ground
245 && (vel.magnitude() > 0.1 || !previous_state.on_ground)
246 {
247 return if let CharacterState::Roll(data) = character_state {
248 if data.static_data.was_cancel {
249 SfxEvent::RollCancel
250 } else {
251 SfxEvent::Roll
252 }
253 } else if character_state.is_stealthy() {
254 SfxEvent::Sneak
255 } else {
256 match block.kind() {
257 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
258 BlockKind::Rock
259 | BlockKind::WeakRock
260 | BlockKind::GlowingRock
261 | BlockKind::GlowingWeakRock
262 | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
263 BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
264 BlockKind::Air => SfxEvent::Idle,
266 _ => SfxEvent::Run(BlockKind::Grass),
267 }
268 };
269 }
270
271 match (previous_state.event.clone(), character_state) {
273 (_, CharacterState::Climb { .. }) => SfxEvent::Climb,
274 (_, CharacterState::Glide(glide))
275 if matches!(glide.booster, Some(states::glide::Boost::Forward(_))) =>
276 {
277 if rand::rng().random_bool(0.5) {
278 SfxEvent::FlameThrower
279 } else {
280 SfxEvent::Idle
281 }
282 },
283 (_, CharacterState::Glide(_)) => SfxEvent::Glide,
284 _ => SfxEvent::Idle,
285 }
286 }
287
288 fn map_non_humanoid_movement_event(
290 physics_state: &PhysicsState,
291 previous_state: &PreviousEntityState,
292 vel: Vec3<f32>,
293 ) -> SfxEvent {
294 if physics_state.in_liquid().is_some()
295 && (vel.magnitude() > 2.0 || !previous_state.on_ground)
296 {
297 SfxEvent::Swim
298 } else if let Some(block) = physics_state.on_ground
299 && vel.magnitude() > 0.1
300 {
301 match block.kind() {
302 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::Run(BlockKind::Snow),
303 BlockKind::Rock
304 | BlockKind::WeakRock
305 | BlockKind::GlowingRock
306 | BlockKind::GlowingWeakRock
307 | BlockKind::Ice => SfxEvent::Run(BlockKind::Rock),
308 BlockKind::Earth => SfxEvent::Run(BlockKind::Earth),
310 BlockKind::Air => SfxEvent::Idle,
311 _ => SfxEvent::Run(BlockKind::Grass),
312 }
313 } else {
314 SfxEvent::Idle
315 }
316 }
317
318 fn map_quadruped_movement_event(
320 physics_state: &PhysicsState,
321 previous_state: &PreviousEntityState,
322 vel: Vec3<f32>,
323 ) -> SfxEvent {
324 if physics_state.in_liquid().is_some()
325 && (vel.magnitude() > 2.0 || !previous_state.on_ground)
326 {
327 SfxEvent::Swim
328 } else if let Some(block) = physics_state.on_ground
329 && vel.magnitude() > 0.1
330 {
331 match block.kind() {
332 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::QuadRun(BlockKind::Snow),
333 BlockKind::Rock
334 | BlockKind::WeakRock
335 | BlockKind::GlowingRock
336 | BlockKind::GlowingWeakRock
337 | BlockKind::Ice => SfxEvent::QuadRun(BlockKind::Rock),
338 BlockKind::Earth => SfxEvent::QuadRun(BlockKind::Earth),
340 BlockKind::Air => SfxEvent::Idle,
341 _ => SfxEvent::QuadRun(BlockKind::Grass),
342 }
343 } else {
344 SfxEvent::Idle
345 }
346 }
347
348 fn map_arthropod_movement_event(
350 physics_state: &PhysicsState,
351 previous_state: &PreviousEntityState,
352 vel: Vec3<f32>,
353 ) -> SfxEvent {
354 if physics_state.in_liquid().is_some()
355 && (vel.magnitude() > 2.0 || !previous_state.on_ground)
356 {
357 SfxEvent::Swim
358 } else if let Some(block) = physics_state.on_ground
359 && vel.magnitude() > 0.1
360 {
361 match block.kind() {
362 BlockKind::Snow | BlockKind::ArtSnow => SfxEvent::OctoRun(BlockKind::Snow),
363 BlockKind::Rock
364 | BlockKind::WeakRock
365 | BlockKind::GlowingRock
366 | BlockKind::GlowingWeakRock
367 | BlockKind::Ice => SfxEvent::OctoRun(BlockKind::Rock),
368 BlockKind::Earth => SfxEvent::OctoRun(BlockKind::Earth),
370 BlockKind::Air => SfxEvent::Idle,
371 _ => SfxEvent::OctoRun(BlockKind::Grass),
372 }
373 } else {
374 SfxEvent::Idle
375 }
376 }
377
378 fn get_volume_for_body_type(body: &Body) -> f32 {
381 match body {
382 Body::Humanoid(_) => 0.5,
383 Body::QuadrupedSmall(_) => 0.2,
384 Body::QuadrupedMedium(_) => 0.7,
385 Body::QuadrupedLow(_) => 0.7,
386 Body::BirdMedium(_) => 0.3,
387 Body::BirdLarge(_) => 0.5,
388 Body::BipedLarge(_) => 1.0,
389 _ => 0.9,
390 }
391 }
392}
393
394#[cfg(test)] mod tests;