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        for (entity, body, pos, vel) in (
64            &ecs.entities(),
65            &ecs.read_storage::<Body>(),
66            &ecs.read_storage::<Pos>(),
67            &ecs.read_storage::<Vel>(),
68        )
69            .join()
70            .filter(|(_, _, e_pos, _)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
71        {
72            if let Body::Ship(ship::Body::Train) = body {
73                let internal_state = self.event_history.entry(entity).or_default();
74
75                let speed = vel.0.magnitude();
76
77                // Determines whether we play low-speed chuggs or high-speed chugging
78                let chugg_lerp = ((speed - 20.0) / 25.0).clamp(0.0, 1.0);
79
80                // Low-speed chugging
81                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainChugg)
82                    && internal_state.last_chugg.elapsed().as_secs_f32() >= 7.5 / speed.min(50.0)
83                    && chugg_lerp < 1.0
84                {
85                    audio.emit_sfx(
86                        Some((event, item)),
87                        pos.0,
88                        Some(((1.0 - chugg_lerp) * 4.0).min(3.0)),
89                    );
90                    internal_state.last_chugg = Instant::now();
91                }
92                // Steam release
93                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainChuggSteam)
94                    && internal_state.last_chugg_steam.elapsed().as_secs_f32()
95                        >= 10.0 / speed.min(50.0)
96                    && chugg_lerp < 1.0
97                {
98                    audio.emit_sfx(Some((event, item)), pos.0, Some((1.0 - chugg_lerp) * 4.0));
99                    internal_state.last_chugg_steam = Instant::now();
100                }
101                // High-speed chugging
102                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainSpeed) {
103                    let volume = chugg_lerp * 8.0;
104
105                    if internal_state.last_speed.0.elapsed().as_secs_f32() >= item.threshold
106                        && chugg_lerp > 0.0
107                    {
108                        internal_state.last_speed = (
109                            Instant::now(),
110                            audio.emit_sfx(Some((event, item)), pos.0, None),
111                        );
112                    }
113
114                    if let Some(chan) = internal_state
115                        .last_speed
116                        .1
117                        .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
118                    {
119                        chan.set_volume(volume, Some(0.1));
120                        chan.set_pos(pos.0);
121                    }
122                }
123                // Train ambience
124                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainAmbience) {
125                    let volume = speed.clamp(20.0, 50.0) / 10.0;
126
127                    if internal_state.last_ambience.0.elapsed().as_secs_f32() >= item.threshold {
128                        internal_state.last_ambience = (
129                            Instant::now(),
130                            audio.emit_sfx(Some((event, item)), pos.0, None),
131                        );
132                    }
133
134                    if let Some(chan) = internal_state
135                        .last_ambience
136                        .1
137                        .and_then(|sfx| audio.channels_mut()?.get_sfx_channel(&sfx))
138                    {
139                        chan.set_volume(volume, Some(0.1));
140                        chan.set_pos(pos.0);
141                    }
142                }
143                // Train clack
144                if let Some((event, item)) = triggers.0.get_key_value(&SfxEvent::TrainClack)
145                    && internal_state.last_clack.elapsed().as_secs_f32() >= 48.0 / speed
146                    && speed > 25.0
147                {
148                    audio.emit_sfx(
149                        Some((event, item)),
150                        pos.0,
151                        Some(speed.clamp(25.0, 50.0) / 18.0),
152                    );
153                    internal_state.last_clack = Instant::now();
154                }
155            }
156        }
157
158        self.cleanup(player_entity);
159    }
160}
161
162impl VehicleEventMapper {
163    pub fn new() -> Self {
164        Self {
165            event_history: HashMap::new(),
166        }
167    }
168
169    /// As the player explores the world, we track the last event of the nearby
170    /// entities to determine the correct SFX item to play next based on
171    /// their activity. `cleanup` will remove entities from event tracking if
172    /// they have not triggered an event for > n seconds. This prevents
173    /// stale records from bloating the Map size.
174    fn cleanup(&mut self, player: EcsEntity) {
175        const TRACKING_TIMEOUT: u64 = 10;
176
177        let now = Instant::now();
178        self.event_history.retain(|entity, event| {
179            now.duration_since(
180                event
181                    .last_chugg
182                    .max(event.last_ambience.0)
183                    .max(event.last_clack)
184                    .max(event.last_speed.0),
185            ) < Duration::from_secs(TRACKING_TIMEOUT)
186                || entity.id() == player.id()
187        });
188    }
189}