veloren_voxygen/audio/sfx/event_mapper/
vehicle.rs

1/// EventMapper::Campfire maps sfx to campfires
2use 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                    // Determines whether we play low-speed chuggs or high-speed chugging
79                    let chugg_lerp = ((speed - 20.0) / 25.0).clamp(0.0, 1.0);
80
81                    // Low-speed chugging
82                    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                    // Steam release
96                    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                    // High-speed chugging
111                    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                    // Train ambience
133                    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                    // Train clack
155                    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    /// As the player explores the world, we track the last event of the nearby
182    /// entities to determine the correct SFX item to play next based on
183    /// their activity. `cleanup` will remove entities from event tracking if
184    /// they have not triggered an event for > n seconds. This prevents
185    /// stale records from bloating the Map size.
186    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}