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.0.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)) =
97 triggers.0.get_key_value(&SfxEvent::TrainChuggSteam)
98 && internal_state.last_chugg_steam.elapsed().as_secs_f32()
99 >= 10.0 / speed.min(50.0)
100 && chugg_lerp < 1.0
101 {
102 audio.emit_sfx_ext(
103 Some((event, item)),
104 pos.0,
105 Some((1.0 - chugg_lerp) * 4.0),
106 player_pos.0,
107 );
108 internal_state.last_chugg_steam = Instant::now();
109 }
110 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainSpeed) {
112 let volume = chugg_lerp * 8.0;
113
114 if internal_state.last_speed.0.elapsed().as_secs_f32() >= item.threshold
115 && chugg_lerp > 0.0
116 {
117 internal_state.last_speed = (
118 Instant::now(),
119 audio.emit_sfx_ext(Some((event, item)), pos.0, None, player_pos.0),
120 );
121 }
122
123 if let Some(chan) = internal_state
124 .last_speed
125 .1
126 .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
127 {
128 chan.set_volume(volume);
129 chan.set_pos(pos.0);
130 }
131 }
132 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainAmbience)
134 {
135 let volume = speed.clamp(20.0, 50.0) / 10.0;
136
137 if internal_state.last_ambience.0.elapsed().as_secs_f32() >= item.threshold
138 {
139 internal_state.last_ambience = (
140 Instant::now(),
141 audio.emit_sfx_ext(Some((event, item)), pos.0, None, player_pos.0),
142 );
143 }
144
145 if let Some(chan) = internal_state
146 .last_ambience
147 .1
148 .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
149 {
150 chan.set_volume(volume);
151 chan.set_pos(pos.0);
152 }
153 }
154 if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainClack)
156 && internal_state.last_clack.elapsed().as_secs_f32() >= 48.0 / speed
157 && speed > 25.0
158 {
159 audio.emit_sfx_ext(
160 Some((event, item)),
161 pos.0,
162 Some(speed.clamp(25.0, 50.0) / 18.0),
163 player_pos.0,
164 );
165 internal_state.last_clack = Instant::now();
166 }
167 }
168 }
169 }
170 self.cleanup(player_entity);
171 }
172}
173
174impl VehicleEventMapper {
175 pub fn new() -> Self {
176 Self {
177 event_history: HashMap::new(),
178 }
179 }
180
181 fn cleanup(&mut self, player: EcsEntity) {
187 const TRACKING_TIMEOUT: u64 = 10;
188
189 let now = Instant::now();
190 self.event_history.retain(|entity, event| {
191 now.duration_since(
192 event
193 .last_chugg
194 .max(event.last_ambience.0)
195 .max(event.last_clack)
196 .max(event.last_speed.0),
197 ) < Duration::from_secs(TRACKING_TIMEOUT)
198 || entity.id() == player.id()
199 });
200 }
201}