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 if let Some(player_pos) = state.read_component_copied::<Pos>(player_entity) {
64 for (entity, body, pos, vel) in (
65 &ecs.entities(),
66 &ecs.read_storage::<Body>(),
67 &ecs.read_storage::<Pos>(),
68 &ecs.read_storage::<Vel>(),
69 )
70 .join()
71 .filter(|(_, _, e_pos, _)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
72 {
73 if let Body::Ship(ship::Body::Train) = body {
74 let internal_state = self.event_history.entry(entity).or_default();
75
76 let speed = vel.0.magnitude();
77
78 let chugg_lerp = ((speed - 20.0) / 25.0).clamp(0.0, 1.0);
80
81 if let Some((event, item)) = triggers.get_key_value(&SfxEvent::TrainChugg)
83 && internal_state.last_chugg.elapsed().as_secs_f32()
84 >= 7.5 / speed.min(50.0)
85 && chugg_lerp < 1.0
86 {
87 audio.emit_sfx_ext(
88 Some((event, item)),
89 pos.0,
90 Some(((1.0 - chugg_lerp) * 4.0).min(3.0)),
91 player_pos.0,
92 );
93 internal_state.last_chugg = Instant::now();
94 }
95 if let Some((event, item)) = triggers.get_key_value(&SfxEvent::TrainChuggSteam)
97 && internal_state.last_chugg_steam.elapsed().as_secs_f32()
98 >= 10.0 / speed.min(50.0)
99 && chugg_lerp < 1.0
100 {
101 audio.emit_sfx_ext(
102 Some((event, item)),
103 pos.0,
104 Some((1.0 - chugg_lerp) * 4.0),
105 player_pos.0,
106 );
107 internal_state.last_chugg_steam = Instant::now();
108 }
109 if let Some((event, item)) = triggers.get_key_value(&SfxEvent::TrainSpeed) {
111 let volume = chugg_lerp * 8.0;
112
113 if internal_state.last_speed.0.elapsed().as_secs_f32() >= item.threshold
114 && chugg_lerp > 0.0
115 {
116 internal_state.last_speed = (
117 Instant::now(),
118 audio.emit_sfx_ext(Some((event, item)), pos.0, None, player_pos.0),
119 );
120 }
121
122 if let Some(chan) = internal_state
123 .last_speed
124 .1
125 .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
126 {
127 chan.set_volume(volume);
128 chan.set_pos(pos.0);
129 }
130 }
131 if let Some((event, item)) = triggers.get_key_value(&SfxEvent::TrainAmbience) {
133 let volume = speed.clamp(20.0, 50.0) / 10.0;
134
135 if internal_state.last_ambience.0.elapsed().as_secs_f32() >= item.threshold
136 {
137 internal_state.last_ambience = (
138 Instant::now(),
139 audio.emit_sfx_ext(Some((event, item)), pos.0, None, player_pos.0),
140 );
141 }
142
143 if let Some(chan) = internal_state
144 .last_ambience
145 .1
146 .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
147 {
148 chan.set_volume(volume);
149 chan.set_pos(pos.0);
150 }
151 }
152 if let Some((event, item)) = triggers.get_key_value(&SfxEvent::TrainClack)
154 && internal_state.last_clack.elapsed().as_secs_f32() >= 48.0 / speed
155 && speed > 25.0
156 {
157 audio.emit_sfx_ext(
158 Some((event, item)),
159 pos.0,
160 Some(speed.clamp(25.0, 50.0) / 18.0),
161 player_pos.0,
162 );
163 internal_state.last_clack = Instant::now();
164 }
165 }
166 }
167 }
168 self.cleanup(player_entity);
169 }
170}
171
172impl VehicleEventMapper {
173 pub fn new() -> Self {
174 Self {
175 event_history: HashMap::new(),
176 }
177 }
178
179 fn cleanup(&mut self, player: EcsEntity) {
185 const TRACKING_TIMEOUT: u64 = 10;
186
187 let now = Instant::now();
188 self.event_history.retain(|entity, event| {
189 now.duration_since(
190 event
191 .last_chugg
192 .max(event.last_ambience.0)
193 .max(event.last_clack)
194 .max(event.last_speed.0),
195 ) < Duration::from_secs(TRACKING_TIMEOUT)
196 || entity.id() == player.id()
197 });
198 }
199}