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