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, 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                    );
91                    internal_state.last_chugg = Instant::now();
92                }
93                // Steam release
94                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                // High-speed chugging
103                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                // Train ambience
125                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                // Train clack
145                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    /// As the player explores the world, we track the last event of the nearby
171    /// entities to determine the correct SFX item to play next based on
172    /// their activity. `cleanup` will remove entities from event tracking if
173    /// they have not triggered an event for > n seconds. This prevents
174    /// stale records from bloating the Map size.
175    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}