Skip to main content

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, SfxTag, 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                // 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() >= 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                        SfxTag::Vehicle,
91                    );
92                    internal_state.last_chugg = Instant::now();
93                }
94                // Steam release
95                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainChuggSteam)
96                    && internal_state.last_chugg_steam.elapsed().as_secs_f32()
97                        >= 10.0 / speed.min(50.0)
98                    && chugg_lerp < 1.0
99                {
100                    audio.emit_sfx(
101                        Some((event, item)),
102                        pos.0,
103                        Some((1.0 - chugg_lerp) * 4.0),
104                        SfxTag::Vehicle,
105                    );
106                    internal_state.last_chugg_steam = Instant::now();
107                }
108                // High-speed chugging
109                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainSpeed) {
110                    let volume = chugg_lerp * 8.0;
111
112                    if internal_state.last_speed.0.elapsed().as_secs_f32() >= item.threshold
113                        && chugg_lerp > 0.0
114                    {
115                        internal_state.last_speed = (
116                            Instant::now(),
117                            audio.emit_sfx(Some((event, item)), pos.0, None, SfxTag::Vehicle),
118                        );
119                    }
120
121                    if let Some(chan) = internal_state
122                        .last_speed
123                        .1
124                        .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
125                    {
126                        chan.set_volume(volume, Some(0.1));
127                        chan.set_pos(pos.0);
128                    }
129                }
130                // Train ambience
131                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainAmbience) {
132                    let volume = speed.clamp(20.0, 50.0) / 10.0;
133
134                    if internal_state.last_ambience.0.elapsed().as_secs_f32() >= item.threshold {
135                        internal_state.last_ambience = (
136                            Instant::now(),
137                            audio.emit_sfx(Some((event, item)), pos.0, None, SfxTag::Vehicle),
138                        );
139                    }
140
141                    if let Some(chan) = internal_state
142                        .last_ambience
143                        .1
144                        .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
145                    {
146                        chan.set_volume(volume, Some(0.1));
147                        chan.set_pos(pos.0);
148                    }
149                }
150                // Train clack
151                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainClack)
152                    && internal_state.last_clack.elapsed().as_secs_f32() >= 48.0 / speed
153                    && speed > 25.0
154                {
155                    audio.emit_sfx(
156                        Some((event, item)),
157                        pos.0,
158                        Some(speed.clamp(25.0, 50.0) / 18.0),
159                        SfxTag::Vehicle,
160                    );
161                    internal_state.last_clack = Instant::now();
162                }
163            }
164        }
165
166        self.cleanup(player_entity);
167    }
168}
169
170impl VehicleEventMapper {
171    pub fn new() -> Self {
172        Self {
173            event_history: HashMap::new(),
174        }
175    }
176
177    /// As the player explores the world, we track the last event of the nearby
178    /// entities to determine the correct SFX item to play next based on
179    /// their activity. `cleanup` will remove entities from event tracking if
180    /// they have not triggered an event for > n seconds. This prevents
181    /// stale records from bloating the Map size.
182    fn cleanup(&mut self, player: EcsEntity) {
183        const TRACKING_TIMEOUT: u64 = 10;
184
185        let now = Instant::now();
186        self.event_history.retain(|entity, event| {
187            now.duration_since(
188                event
189                    .last_chugg
190                    .max(event.last_ambience.0)
191                    .max(event.last_clack)
192                    .max(event.last_speed.0),
193            ) < Duration::from_secs(TRACKING_TIMEOUT)
194                || entity.id() == player.id()
195        });
196    }
197}