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, FigureMgr, 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 _figure_mgr: &FigureMgr,
59 ) {
60 let ecs = state.ecs();
61
62 let cam_pos = camera.get_pos_with_focus();
63
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.0.get_key_value(&SfxEvent::TrainChugg)
83 && internal_state.last_chugg.elapsed().as_secs_f32() >= 7.5 / speed.min(50.0)
84 && chugg_lerp < 1.0
85 {
86 audio.emit_sfx(
87 Some((event, item)),
88 pos.0,
89 Some(((1.0 - chugg_lerp) * 4.0).min(3.0)),
90 );
91 internal_state.last_chugg = Instant::now();
92 }
93 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainChuggSteam)
95 && internal_state.last_chugg_steam.elapsed().as_secs_f32()
96 >= 10.0 / speed.min(50.0)
97 && chugg_lerp < 1.0
98 {
99 audio.emit_sfx(Some((event, item)), pos.0, Some((1.0 - chugg_lerp) * 4.0));
100 internal_state.last_chugg_steam = Instant::now();
101 }
102 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainSpeed) {
104 let volume = chugg_lerp * 8.0;
105
106 if internal_state.last_speed.0.elapsed().as_secs_f32() >= item.threshold
107 && chugg_lerp > 0.0
108 {
109 internal_state.last_speed = (
110 Instant::now(),
111 audio.emit_sfx(Some((event, item)), pos.0, None),
112 );
113 }
114
115 if let Some(chan) = internal_state
116 .last_speed
117 .1
118 .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
119 {
120 chan.set_volume(volume, Some(0.1));
121 chan.set_pos(pos.0);
122 }
123 }
124 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainAmbience) {
126 let volume = speed.clamp(20.0, 50.0) / 10.0;
127
128 if internal_state.last_ambience.0.elapsed().as_secs_f32() >= item.threshold {
129 internal_state.last_ambience = (
130 Instant::now(),
131 audio.emit_sfx(Some((event, item)), pos.0, None),
132 );
133 }
134
135 if let Some(chan) = internal_state
136 .last_ambience
137 .1
138 .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
139 {
140 chan.set_volume(volume, Some(0.1));
141 chan.set_pos(pos.0);
142 }
143 }
144 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainClack)
146 && internal_state.last_clack.elapsed().as_secs_f32() >= 48.0 / speed
147 && speed > 25.0
148 {
149 audio.emit_sfx(
150 Some((event, item)),
151 pos.0,
152 Some(speed.clamp(25.0, 50.0) / 18.0),
153 );
154 internal_state.last_clack = Instant::now();
155 }
156 }
157 }
158
159 self.cleanup(player_entity);
160 }
161}
162
163impl VehicleEventMapper {
164 pub fn new() -> Self {
165 Self {
166 event_history: HashMap::new(),
167 }
168 }
169
170 fn cleanup(&mut self, player: EcsEntity) {
176 const TRACKING_TIMEOUT: u64 = 10;
177
178 let now = Instant::now();
179 self.event_history.retain(|entity, event| {
180 now.duration_since(
181 event
182 .last_chugg
183 .max(event.last_ambience.0)
184 .max(event.last_clack)
185 .max(event.last_speed.0),
186 ) < Duration::from_secs(TRACKING_TIMEOUT)
187 || entity.id() == player.id()
188 });
189 }
190}