veloren_voxygen/audio/sfx/event_mapper/
vehicle.rs1use crate::{
3 AudioFrontend,
4 audio::{
5 SfxHandle,
6 sfx::{SFX_DIST_LIMIT_SQR, SfxEvent, SfxTriggers},
7 },
8 scene::{Camera, Terrain},
9};
10
11use super::EventMapper;
12
13use client::Client;
14use common::{
15 comp::{Body, Pos, Vel, ship},
16 terrain::TerrainChunk,
17};
18use common_state::State;
19use hashbrown::HashMap;
20use specs::{Entity as EcsEntity, Join, WorldExt};
21use std::time::{Duration, Instant};
22
23#[derive(Clone)]
24struct PreviousEntityState {
25 last_chugg: Instant,
26 last_chugg_steam: Instant,
27 last_speed: (Instant, Option<SfxHandle>),
28 last_ambience: (Instant, Option<SfxHandle>),
29 last_clack: Instant,
30}
31
32impl Default for PreviousEntityState {
33 fn default() -> Self {
34 Self {
35 last_chugg: Instant::now(),
36 last_chugg_steam: Instant::now(),
37 last_speed: (Instant::now(), None),
38 last_ambience: (Instant::now(), None),
39 last_clack: Instant::now(),
40 }
41 }
42}
43
44pub struct VehicleEventMapper {
45 event_history: HashMap<EcsEntity, PreviousEntityState>,
46}
47
48impl EventMapper for VehicleEventMapper {
49 fn maintain(
50 &mut self,
51 audio: &mut AudioFrontend,
52 state: &State,
53 player_entity: specs::Entity,
54 camera: &Camera,
55 triggers: &SfxTriggers,
56 _terrain: &Terrain<TerrainChunk>,
57 _client: &Client,
58 ) {
59 let ecs = state.ecs();
60
61 let cam_pos = camera.get_pos_with_focus();
62
63 for (entity, body, pos, vel) in (
64 &ecs.entities(),
65 &ecs.read_storage::<Body>(),
66 &ecs.read_storage::<Pos>(),
67 &ecs.read_storage::<Vel>(),
68 )
69 .join()
70 .filter(|(_, _, e_pos, _)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
71 {
72 if let Body::Ship(ship::Body::Train) = body {
73 let internal_state = self.event_history.entry(entity).or_default();
74
75 let speed = vel.0.magnitude();
76
77 let chugg_lerp = ((speed - 20.0) / 25.0).clamp(0.0, 1.0);
79
80 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainChugg)
82 && internal_state.last_chugg.elapsed().as_secs_f32() >= 7.5 / speed.min(50.0)
83 && chugg_lerp < 1.0
84 {
85 audio.emit_sfx(
86 Some((event, item)),
87 pos.0,
88 Some(((1.0 - chugg_lerp) * 4.0).min(3.0)),
89 );
90 internal_state.last_chugg = Instant::now();
91 }
92 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainChuggSteam)
94 && internal_state.last_chugg_steam.elapsed().as_secs_f32()
95 >= 10.0 / speed.min(50.0)
96 && chugg_lerp < 1.0
97 {
98 audio.emit_sfx(Some((event, item)), pos.0, Some((1.0 - chugg_lerp) * 4.0));
99 internal_state.last_chugg_steam = Instant::now();
100 }
101 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainSpeed) {
103 let volume = chugg_lerp * 8.0;
104
105 if internal_state.last_speed.0.elapsed().as_secs_f32() >= item.threshold
106 && chugg_lerp > 0.0
107 {
108 internal_state.last_speed = (
109 Instant::now(),
110 audio.emit_sfx(Some((event, item)), pos.0, None),
111 );
112 }
113
114 if let Some(chan) = internal_state
115 .last_speed
116 .1
117 .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
118 {
119 chan.set_volume(volume, Some(0.1));
120 chan.set_pos(pos.0);
121 }
122 }
123 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainAmbience) {
125 let volume = speed.clamp(20.0, 50.0) / 10.0;
126
127 if internal_state.last_ambience.0.elapsed().as_secs_f32() >= item.threshold {
128 internal_state.last_ambience = (
129 Instant::now(),
130 audio.emit_sfx(Some((event, item)), pos.0, None),
131 );
132 }
133
134 if let Some(chan) = internal_state
135 .last_ambience
136 .1
137 .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
138 {
139 chan.set_volume(volume, Some(0.1));
140 chan.set_pos(pos.0);
141 }
142 }
143 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainClack)
145 && internal_state.last_clack.elapsed().as_secs_f32() >= 48.0 / speed
146 && speed > 25.0
147 {
148 audio.emit_sfx(
149 Some((event, item)),
150 pos.0,
151 Some(speed.clamp(25.0, 50.0) / 18.0),
152 );
153 internal_state.last_clack = Instant::now();
154 }
155 }
156 }
157
158 self.cleanup(player_entity);
159 }
160}
161
162impl VehicleEventMapper {
163 pub fn new() -> Self {
164 Self {
165 event_history: HashMap::new(),
166 }
167 }
168
169 fn cleanup(&mut self, player: EcsEntity) {
175 const TRACKING_TIMEOUT: u64 = 10;
176
177 let now = Instant::now();
178 self.event_history.retain(|entity, event| {
179 now.duration_since(
180 event
181 .last_chugg
182 .max(event.last_ambience.0)
183 .max(event.last_clack)
184 .max(event.last_speed.0),
185 ) < Duration::from_secs(TRACKING_TIMEOUT)
186 || entity.id() == player.id()
187 });
188 }
189}