veloren_voxygen/audio/sfx/event_mapper/block.rs
1/// EventMapper::Block watches the sound emitting blocks within
2/// chunk range of the player and emits ambient sfx
3use crate::{
4 AudioFrontend,
5 audio::sfx::{SFX_DIST_LIMIT_SQR, SfxEvent, SfxTriggerItem, SfxTriggers},
6 scene::{Camera, FigureMgr, Terrain, terrain::BlocksOfInterest},
7};
8
9use super::EventMapper;
10use client::Client;
11use common::{comp::Pos, spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol};
12use common_state::State;
13use hashbrown::HashMap;
14use rand::{RngExt, prelude::*, rng};
15use rand_chacha::ChaCha8Rng;
16use std::time::{Duration, Instant};
17use vek::*;
18
19#[derive(Clone, PartialEq)]
20struct PreviousBlockState {
21 event: SfxEvent,
22 time: Instant,
23}
24
25impl Default for PreviousBlockState {
26 fn default() -> Self {
27 Self {
28 event: SfxEvent::Idle,
29 time: Instant::now()
30 .checked_add(Duration::from_millis(rng().random_range(0..500)))
31 .unwrap_or_else(Instant::now),
32 }
33 }
34}
35
36pub struct BlockEventMapper {
37 history: HashMap<Vec3<i32>, PreviousBlockState>,
38}
39
40impl EventMapper for BlockEventMapper {
41 fn maintain(
42 &mut self,
43 audio: &mut AudioFrontend,
44 state: &State,
45 player_entity: specs::Entity,
46 camera: &Camera,
47 triggers: &SfxTriggers,
48 terrain: &Terrain<TerrainChunk>,
49 client: &Client,
50 _figure_mgr: &FigureMgr,
51 ) {
52 let mut rng = ChaCha8Rng::from_seed(rand::rng().random());
53
54 // Get the player position and chunk
55 if let Some(player_pos) = state.read_component_copied::<Pos>(player_entity) {
56 let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
57 (e.floor() as i32).div_euclid(sz as i32)
58 });
59
60 // For determining if crickets should chirp
61 let (terrain_alt, temp) = match client.current_chunk() {
62 Some(chunk) => (chunk.meta().alt(), chunk.meta().temp()),
63 None => (0.0, 0.0),
64 };
65
66 struct BlockSounds<'a> {
67 // The function to select the blocks of interest that we should emit from
68 blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3<i32>],
69 // The range, in chunks, that the particles should be generated in from the player
70 range: usize,
71 // The sound of the generated particle
72 sfx: SfxEvent,
73 // The volume of the sfx
74 volume: f32,
75 // Condition that must be true to play
76 cond: fn(&State) -> bool,
77 }
78
79 let sounds: &[BlockSounds] = &[
80 BlockSounds {
81 blocks: |boi| &boi.leaves,
82 range: 1,
83 sfx: SfxEvent::Birdcall,
84 volume: 1.5,
85 cond: |st| st.get_day_period().is_light(),
86 },
87 BlockSounds {
88 blocks: |boi| &boi.leaves,
89 range: 1,
90 sfx: SfxEvent::Owl,
91 volume: 1.5,
92 cond: |st| st.get_day_period().is_dark(),
93 },
94 BlockSounds {
95 blocks: |boi| &boi.slow_river,
96 range: 1,
97 sfx: SfxEvent::RunningWaterSlow,
98 volume: 1.0,
99 cond: |_| true,
100 },
101 BlockSounds {
102 blocks: |boi| &boi.lavapool,
103 range: 1,
104 sfx: SfxEvent::Lavapool,
105 volume: 1.8,
106 cond: |_| true,
107 },
108 //BlockSounds {
109 // blocks: |boi| &boi.embers,
110 // range: 1,
111 // sfx: SfxEvent::Embers,
112 // volume: 0.15,
113 // //volume: 0.05,
114 // cond: |_| true,
115 // //cond: |st| st.get_day_period().is_dark(),
116 //},
117 BlockSounds {
118 blocks: |boi| &boi.frogs,
119 range: 1,
120 sfx: SfxEvent::Frog,
121 volume: 1.0,
122 cond: |st| st.get_day_period().is_dark(),
123 },
124 //BlockSounds {
125 // blocks: |boi| &boi.flowers,
126 // range: 4,
127 // sfx: SfxEvent::LevelUp,
128 // volume: 1.0,
129 // cond: |st| st.get_day_period().is_dark(),
130 //},
131 BlockSounds {
132 blocks: |boi| &boi.cricket1,
133 range: 1,
134 sfx: SfxEvent::Cricket1,
135 volume: 0.5,
136 cond: |st| st.get_day_period().is_dark(),
137 },
138 BlockSounds {
139 blocks: |boi| &boi.cricket2,
140 range: 1,
141 sfx: SfxEvent::Cricket2,
142 volume: 0.5,
143 cond: |st| st.get_day_period().is_dark(),
144 },
145 BlockSounds {
146 blocks: |boi| &boi.cricket3,
147 range: 1,
148 sfx: SfxEvent::Cricket3,
149 volume: 0.5,
150 cond: |st| st.get_day_period().is_dark(),
151 },
152 BlockSounds {
153 blocks: |boi| &boi.beehives,
154 range: 1,
155 sfx: SfxEvent::Bees,
156 volume: 0.5,
157 cond: |st| st.get_day_period().is_light(),
158 },
159 ];
160 // Iterate through each kind of block of interest
161 for sounds in sounds.iter() {
162 // If the timing condition is false, continue
163 // TODO Address bird hack properly. See TODO below
164 if !(sounds.cond)(state)
165 || (!(sounds.sfx == SfxEvent::Lavapool) && player_pos.0.z < (terrain_alt - 30.0))
166 || (sounds.sfx == SfxEvent::Birdcall && (rng.random_bool(0.9925) || client.weather_at_player().rain >= 0.07))
167 || (sounds.sfx == SfxEvent::Owl && (rng.random_bool(0.997) || client.weather_at_player().rain >= 0.14))
168 || (sounds.sfx == SfxEvent::Frog && rng.random_bool(0.95))
169 //Crickets will not chirp below 5 Celsius
170 || (sounds.sfx == SfxEvent::Cricket1 && ((temp < -0.33) || client.weather_at_player().rain >= 0.07))
171 || (sounds.sfx == SfxEvent::Cricket2 && ((temp < -0.33) || client.weather_at_player().rain >= 0.07))
172 || (sounds.sfx == SfxEvent::Cricket3 && ((temp < -0.33) || client.weather_at_player().rain >= 0.07))
173 {
174 continue;
175 }
176
177 // For chunks surrounding the player position
178 for offset in Spiral2d::new().take((sounds.range * 2 + 1).pow(2)) {
179 let chunk_pos = player_chunk + offset;
180
181 // Get all the blocks of interest in this chunk
182 terrain.get(chunk_pos).map(|chunk_data| {
183 // Get the positions of the blocks of type sounds
184 let blocks = (sounds.blocks)(&chunk_data.blocks_of_interest);
185
186 let absolute_pos: Vec3<i32> =
187 Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
188
189 // Replace all RunningWater blocks with just one random one per tick
190 let blocks = if sounds.sfx == SfxEvent::RunningWaterSlow
191 || sounds.sfx == SfxEvent::Lavapool
192 {
193 blocks
194 .choose(&mut rng)
195 .map(std::slice::from_ref)
196 .unwrap_or(&[])
197 } else {
198 blocks
199 };
200
201 // Iterate through each individual block
202 for block in blocks {
203 // TODO Address this hack properly, potentially by making a new
204 // block of interest type which picks fewer leaf blocks
205 // Hack to reduce the number of bird, frog, and water sounds
206 if ((sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl)
207 && rng.random_bool(0.9995))
208 || (sounds.sfx == SfxEvent::Frog && rng.random_bool(0.75))
209 || (sounds.sfx == SfxEvent::RunningWaterSlow
210 && rng.random_bool(0.85))
211 || (sounds.sfx == SfxEvent::Lavapool && rng.random_bool(0.99))
212 {
213 continue;
214 }
215 let block_pos: Vec3<i32> = absolute_pos + block;
216 let internal_state = self.history.entry(block_pos).or_default();
217
218 let cam_pos = camera.get_pos_with_focus();
219
220 let block_pos = block_pos.map(|x| x as f32);
221
222 if Self::should_emit(
223 internal_state,
224 triggers.0.get_key_value(&sounds.sfx),
225 temp,
226 ) {
227 // If the camera is within SFX distance
228 if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR {
229 let sfx_trigger_item = triggers.0.get_key_value(&sounds.sfx);
230 audio.emit_sfx(
231 sfx_trigger_item,
232 block_pos,
233 Some(sounds.volume),
234 );
235 }
236 internal_state.time = Instant::now();
237 internal_state.event = sounds.sfx.clone();
238 }
239 }
240 });
241 }
242 }
243 }
244 }
245}
246
247impl BlockEventMapper {
248 pub fn new() -> Self {
249 Self {
250 history: HashMap::new(),
251 }
252 }
253
254 /// Ensures that:
255 /// 1. An sfx.ron entry exists for an SFX event
256 /// 2. The sfx has not been played since it's timeout threshold has elapsed,
257 /// which prevents firing every tick. Note that with so many blocks to
258 /// choose from and different blocks being selected each time, this is
259 /// not perfect, but does reduce the number of plays from blocks that
260 /// have already emitted sfx and are stored in the BlockEventMapper
261 /// history.
262 fn should_emit(
263 previous_state: &PreviousBlockState,
264 sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
265 temp: f32,
266 ) -> bool {
267 let mut rng = ChaCha8Rng::from_seed(rand::rng().random());
268
269 if let Some((event, item)) = sfx_trigger_item {
270 //The interval between cricket chirps calculated by converting chunk
271 // temperature to centigrade (we should create a function for this) and applying
272 // the "cricket formula" to it
273 let cricket_interval = (25.0 / (3.0 * ((temp * 30.0) + 15.0))).max(0.5);
274 if &previous_state.event == event {
275 //In case certain sounds need modification to their threshold,
276 //use match event
277 match event {
278 SfxEvent::Cricket1 => {
279 previous_state.time.elapsed().as_secs_f32()
280 >= cricket_interval + rng.random_range(-0.1..0.1)
281 },
282 SfxEvent::Cricket2 => {
283 //the length and manner of this sound is quite different
284 if cricket_interval < 0.75 {
285 previous_state.time.elapsed().as_secs_f32() >= 0.75
286 } else {
287 previous_state.time.elapsed().as_secs_f32()
288 >= cricket_interval + rng.random_range(-0.1..0.1)
289 }
290 },
291 SfxEvent::Cricket3 => {
292 previous_state.time.elapsed().as_secs_f32()
293 >= cricket_interval + rng.random_range(-0.1..0.1)
294 },
295 //Adds random factor to frogs (probably doesn't do anything most of the time)
296 SfxEvent::Frog => {
297 previous_state.time.elapsed().as_secs_f32() >= rng.random_range(-2.0..2.0)
298 },
299 _ => previous_state.time.elapsed().as_secs_f32() >= item.threshold,
300 }
301 } else {
302 true
303 }
304 } else {
305 false
306 }
307 }
308}