Skip to main content

veloren_voxygen/audio/sfx/event_mapper/
campfire.rs

1/// EventMapper::Campfire maps sfx to campfires
2use crate::{
3    AudioFrontend,
4    audio::sfx::{SFX_DIST_LIMIT_SQR, SfxEvent, SfxTag, SfxTriggerItem, SfxTriggers},
5    scene::{Camera, FigureMgr, Terrain},
6};
7
8use super::EventMapper;
9
10use client::Client;
11use common::{
12    comp::{Body, Pos, object},
13    terrain::TerrainChunk,
14};
15use common_state::State;
16use hashbrown::HashMap;
17use specs::{Entity as EcsEntity, Join, WorldExt};
18use std::time::{Duration, Instant};
19
20#[derive(Clone)]
21struct PreviousEntityState {
22    event: SfxEvent,
23    time: Instant,
24}
25
26impl Default for PreviousEntityState {
27    fn default() -> Self {
28        Self {
29            event: SfxEvent::Idle,
30            time: Instant::now(),
31        }
32    }
33}
34
35pub struct CampfireEventMapper {
36    event_history: HashMap<EcsEntity, PreviousEntityState>,
37}
38
39impl EventMapper for CampfireEventMapper {
40    fn maintain(
41        &mut self,
42        audio: &mut AudioFrontend,
43        state: &State,
44        player_entity: specs::Entity,
45        camera: &Camera,
46        triggers: &SfxTriggers,
47        _terrain: &Terrain<TerrainChunk>,
48        _client: &Client,
49        _figure_mgr: &FigureMgr,
50    ) {
51        let ecs = state.ecs();
52
53        let cam_pos = camera.get_pos_with_focus();
54
55        for (entity, body, pos) in (
56            &ecs.entities(),
57            &ecs.read_storage::<Body>(),
58            &ecs.read_storage::<Pos>(),
59        )
60            .join()
61            .filter(|(_, _, e_pos)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
62        {
63            if let Body::Object(object::Body::CampfireLit) = body {
64                let internal_state = self.event_history.entry(entity).or_default();
65
66                let mapped_event = SfxEvent::Campfire;
67
68                // Check for SFX config entry for this movement
69                if Self::should_emit(internal_state, triggers.0.get_key_value(&mapped_event)) {
70                    let sfx_trigger_item = triggers.0.get_key_value(&mapped_event);
71                    const CAMPFIRE_VOLUME: f32 = 0.8;
72                    audio.emit_sfx(
73                        sfx_trigger_item,
74                        pos.0,
75                        Some(CAMPFIRE_VOLUME),
76                        SfxTag::Looping,
77                    );
78                    internal_state.time = Instant::now();
79                }
80
81                // update state to determine the next event. We only record the time (above) if
82                // it was dispatched
83                internal_state.event = mapped_event;
84            }
85        }
86        self.cleanup(player_entity);
87    }
88}
89
90impl CampfireEventMapper {
91    pub fn new() -> Self {
92        Self {
93            event_history: HashMap::new(),
94        }
95    }
96
97    /// As the player explores the world, we track the last event of the nearby
98    /// entities to determine the correct SFX item to play next based on
99    /// their activity. `cleanup` will remove entities from event tracking if
100    /// they have not triggered an event for > n seconds. This prevents
101    /// stale records from bloating the Map size.
102    fn cleanup(&mut self, player: EcsEntity) {
103        const TRACKING_TIMEOUT: u64 = 10;
104
105        let now = Instant::now();
106        self.event_history.retain(|entity, event| {
107            now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
108                || entity.id() == player.id()
109        });
110    }
111
112    /// Ensures that:
113    /// 1. An sfx.ron entry exists for an SFX event
114    /// 2. The sfx has not been played since it's timeout threshold has elapsed,
115    ///    which prevents firing every tick
116    fn should_emit(
117        previous_state: &PreviousEntityState,
118        sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
119    ) -> bool {
120        if let Some((event, item)) = sfx_trigger_item {
121            if &previous_state.event == event {
122                previous_state.time.elapsed().as_secs_f32() >= item.threshold
123            } else {
124                true
125            }
126        } else {
127            false
128        }
129    }
130}