1mod event_mapper;
77use specs::WorldExt;
78
79use crate::{
80 audio::{
81 AudioFrontend,
82 channel::{SFX_DIST_LIMIT_SQR, UiChannelTag},
83 },
84 scene::{Camera, Terrain},
85};
86
87use client::Client;
88use common::{
89 DamageSource,
90 assets::{AssetExt, AssetHandle, Ron},
91 comp::{
92 Body, CharacterAbilityType, Health, InventoryUpdateEvent, UtteranceKind, beam, biped_large,
93 biped_small, bird_large, bird_medium, crustacean, humanoid,
94 item::{AbilitySpec, ItemDefinitionId, ItemDesc, ItemKind, ToolKind, item_key::ItemKey},
95 object,
96 poise::PoiseState,
97 quadruped_low, quadruped_medium, quadruped_small,
98 },
99 outcome::Outcome,
100 terrain::{BlockKind, SpriteKind, TerrainChunk},
101 uid::Uid,
102 vol::ReadVol,
103};
104use common_state::State;
105use event_mapper::SfxEventMapper;
106use hashbrown::HashMap;
107use rand::prelude::*;
108use serde::Deserialize;
109use tracing::{debug, error, warn};
110use vek::*;
111
112#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
113pub enum SfxEvent {
114 Campfire,
115 Embers,
116 Birdcall,
117 Owl,
118 Cricket1,
119 Cricket2,
120 Cricket3,
121 Frog,
122 Bees,
123 RunningWaterSlow,
124 RunningWaterFast,
125 Lavapool,
126 Idle,
127 Swim,
128 SplashSmall,
129 SplashMedium,
130 SplashBig,
131 Run(BlockKind),
132 QuadRun(BlockKind),
133 OctoRun(BlockKind),
134 Roll,
135 RollCancel,
136 Sneak,
137 Climb,
138 GliderOpen,
139 Glide,
140 GliderClose,
141 CatchAir,
142 Jump,
143 Fall,
144 Attack(CharacterAbilityType, ToolKind),
145 Wield(ToolKind),
146 Unwield(ToolKind),
147 Inventory(SfxInventoryEvent),
148 Explosion,
149 Damage,
150 Death,
151 Parry,
152 Block,
153 BreakBlock,
154 PickaxeDamage,
155 PickaxeDamageStrong,
156 PickaxeBreakBlock,
157 SceptreBeam,
158 SkillPointGain,
159 ArrowHit,
160 ArrowMiss,
161 ArrowShot,
162 FireShot,
163 FlameThrower,
164 PoiseChange(PoiseState),
165 GroundSlam,
166 FlashFreeze,
167 GigaRoar,
168 IceSpikes,
169 IceCrack,
170 Utterance(UtteranceKind, VoiceKind),
171 Lightning,
172 CyclopsCharge,
173 TerracottaStatueCharge,
174 LaserBeam,
175 Steam,
176 FuseCharge,
177 Music(ToolKind, AbilitySpec),
178 Yeet,
179 Hiss,
180 LongHiss,
181 Klonk,
182 SmashKlonk,
183 FireShockwave,
184 DeepLaugh,
185 Whoosh,
186 Swoosh,
187 GroundDig,
188 PortalActivated,
189 TeleportedByPortal,
190 FromTheAshes,
191 SurpriseEgg,
192 Transformation,
193 Bleep,
194 Charge,
195 StrigoiHead,
196 BloodmoonHeiressSummon,
197 TrainChugg,
198 TrainChuggSteam,
199 TrainAmbience,
200 TrainClack,
201 TrainSpeed,
202}
203
204#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
205pub enum VoiceKind {
206 HumanFemale,
207 HumanMale,
208 BipedLarge,
209 Wendigo,
210 Reptile,
211 Bird,
212 Critter,
213 Sheep,
214 Pig,
215 Cow,
216 Canine,
217 Dagon,
218 Lion,
219 Mindflayer,
220 Marlin,
221 Maneater,
222 Adlet,
223 Antelope,
224 Alligator,
225 SeaCrocodile,
226 Saurok,
227 Cat,
228 Goat,
229 Mandragora,
230 Asp,
231 Fungome,
232 Truffler,
233 Wolf,
234 Wyvern,
235 Phoenix,
236 VampireBat,
237 Legoom,
238}
239
240fn body_to_voice(body: &Body) -> Option<VoiceKind> {
241 Some(match body {
242 Body::Humanoid(body) => match &body.body_type {
243 humanoid::BodyType::Female => VoiceKind::HumanFemale,
244 humanoid::BodyType::Male => VoiceKind::HumanMale,
245 },
246 Body::QuadrupedLow(body) => match body.species {
247 quadruped_low::Species::Maneater => VoiceKind::Maneater,
248 quadruped_low::Species::Alligator | quadruped_low::Species::Snaretongue => {
249 VoiceKind::Alligator
250 },
251 quadruped_low::Species::SeaCrocodile => VoiceKind::SeaCrocodile,
252 quadruped_low::Species::Dagon => VoiceKind::Dagon,
253 quadruped_low::Species::Asp => VoiceKind::Asp,
254 _ => return None,
255 },
256 Body::QuadrupedSmall(body) => match body.species {
257 quadruped_small::Species::Truffler => VoiceKind::Truffler,
258 quadruped_small::Species::Fungome => VoiceKind::Fungome,
259 quadruped_small::Species::Sheep => VoiceKind::Sheep,
260 quadruped_small::Species::Pig | quadruped_small::Species::Boar => VoiceKind::Pig,
261 quadruped_small::Species::Cat => VoiceKind::Cat,
262 quadruped_small::Species::Goat => VoiceKind::Goat,
263 _ => VoiceKind::Critter,
264 },
265 Body::QuadrupedMedium(body) => match body.species {
266 quadruped_medium::Species::Saber
267 | quadruped_medium::Species::Tiger
268 | quadruped_medium::Species::Lion
269 | quadruped_medium::Species::Frostfang
270 | quadruped_medium::Species::Snowleopard => VoiceKind::Lion,
271 quadruped_medium::Species::Wolf => VoiceKind::Wolf,
272 quadruped_medium::Species::Roshwalr
273 | quadruped_medium::Species::Tarasque
274 | quadruped_medium::Species::Darkhound
275 | quadruped_medium::Species::Bonerattler
276 | quadruped_medium::Species::Grolgar => VoiceKind::Canine,
277 quadruped_medium::Species::Cattle
278 | quadruped_medium::Species::Catoblepas
279 | quadruped_medium::Species::Highland
280 | quadruped_medium::Species::Yak
281 | quadruped_medium::Species::Moose
282 | quadruped_medium::Species::Dreadhorn => VoiceKind::Cow,
283 quadruped_medium::Species::Antelope => VoiceKind::Antelope,
284 _ => return None,
285 },
286 Body::BirdMedium(body) => match body.species {
287 bird_medium::Species::BloodmoonBat | bird_medium::Species::VampireBat => {
288 VoiceKind::VampireBat
289 },
290 _ => VoiceKind::Bird,
291 },
292 Body::BirdLarge(body) => match body.species {
293 bird_large::Species::CloudWyvern
294 | bird_large::Species::FlameWyvern
295 | bird_large::Species::FrostWyvern
296 | bird_large::Species::SeaWyvern
297 | bird_large::Species::WealdWyvern => VoiceKind::Wyvern,
298 bird_large::Species::Phoenix => VoiceKind::Phoenix,
299 _ => VoiceKind::Bird,
300 },
301 Body::BipedSmall(body) => match body.species {
302 biped_small::Species::Adlet => VoiceKind::Adlet,
303 biped_small::Species::Mandragora => VoiceKind::Mandragora,
304 biped_small::Species::Flamekeeper => VoiceKind::BipedLarge,
305 biped_small::Species::GreenLegoom
306 | biped_small::Species::OchreLegoom
307 | biped_small::Species::PurpleLegoom
308 | biped_small::Species::RedLegoom
309 | biped_small::Species::UmberLegoom => VoiceKind::Legoom,
310 _ => return None,
311 },
312 Body::BipedLarge(body) => match body.species {
313 biped_large::Species::Wendigo => VoiceKind::Wendigo,
314 biped_large::Species::Occultsaurok
315 | biped_large::Species::Mightysaurok
316 | biped_large::Species::Slysaurok => VoiceKind::Saurok,
317 biped_large::Species::Mindflayer => VoiceKind::Mindflayer,
318 _ => VoiceKind::BipedLarge,
319 },
320 Body::Theropod(_) | Body::Dragon(_) => VoiceKind::Reptile,
321 Body::FishSmall(_) | Body::FishMedium(_) => VoiceKind::Marlin,
322 _ => return None,
323 })
324}
325
326#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
327pub enum SfxInventoryEvent {
328 Collected,
329 CollectedTool(ToolKind),
330 CollectedItem(String),
331 CollectFailed,
332 Consumed(ItemKey),
333 Debug,
334 Dropped,
335 Given,
336 Swapped,
337 Craft,
338}
339
340impl From<&InventoryUpdateEvent> for SfxEvent {
342 fn from(value: &InventoryUpdateEvent) -> Self {
343 match value {
344 InventoryUpdateEvent::Collected(item) => {
345 match &*item.kind() {
348 ItemKind::Tool(tool) => {
349 SfxEvent::Inventory(SfxInventoryEvent::CollectedTool(tool.kind))
350 },
351 ItemKind::Ingredient { .. }
352 if matches!(
353 item.item_definition_id(),
354 ItemDefinitionId::Simple(id) if id.contains("mineral.gem.")
355 ) =>
356 {
357 SfxEvent::Inventory(SfxInventoryEvent::CollectedItem(String::from(
358 "Gemstone",
359 )))
360 },
361 _ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
362 }
363 },
364 InventoryUpdateEvent::BlockCollectFailed { .. }
365 | InventoryUpdateEvent::EntityCollectFailed { .. } => {
366 SfxEvent::Inventory(SfxInventoryEvent::CollectFailed)
367 },
368 InventoryUpdateEvent::Consumed(consumable) => {
369 SfxEvent::Inventory(SfxInventoryEvent::Consumed(consumable.clone()))
370 },
371 InventoryUpdateEvent::Debug => SfxEvent::Inventory(SfxInventoryEvent::Debug),
372 InventoryUpdateEvent::Dropped => SfxEvent::Inventory(SfxInventoryEvent::Dropped),
373 InventoryUpdateEvent::Given => SfxEvent::Inventory(SfxInventoryEvent::Given),
374 InventoryUpdateEvent::Swapped => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
375 InventoryUpdateEvent::Craft => SfxEvent::Inventory(SfxInventoryEvent::Craft),
376 _ => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
377 }
378 }
379}
380
381#[derive(Deserialize, Debug)]
382pub struct SfxTriggerItem {
383 pub files: Vec<String>,
385 pub threshold: f32,
387
388 #[serde(default)]
389 pub subtitle: Option<String>,
390}
391
392pub type SfxTriggers = Ron<HashMap<SfxEvent, SfxTriggerItem>>;
393
394pub struct SfxMgr {
395 pub triggers: AssetHandle<SfxTriggers>,
398 event_mapper: SfxEventMapper,
399}
400
401impl Default for SfxMgr {
402 fn default() -> Self {
403 Self {
404 triggers: Self::load_sfx_items(),
405 event_mapper: SfxEventMapper::new(),
406 }
407 }
408}
409
410impl SfxMgr {
411 pub fn maintain(
412 &mut self,
413 audio: &mut AudioFrontend,
414 state: &State,
415 player_entity: specs::Entity,
416 camera: &Camera,
417 terrain: &Terrain<TerrainChunk>,
418 client: &Client,
419 ) {
420 if !audio.sfx_enabled() && !audio.subtitles_enabled {
423 return;
424 }
425
426 let cam_pos = camera.get_pos_with_focus();
427
428 audio.set_listener_pos(cam_pos, camera.dependents().cam_dir);
431
432 let triggers = self.triggers.read();
433
434 let underwater = state
435 .terrain()
436 .get(cam_pos.map(|e| e.floor() as i32))
437 .map(|b| b.is_liquid())
438 .unwrap_or(false);
439
440 if underwater {
441 audio.set_sfx_master_filter(888);
442 } else {
443 audio.set_sfx_master_filter(20000);
444 }
445
446 let player_pos = client.position().unwrap_or_default();
447
448 if let Some(inner) = audio.inner.as_mut() {
450 inner.player_pos = player_pos;
451 inner.channels.sfx.iter_mut().for_each(|c| {
452 if !c.is_done() {
453 c.update(player_pos)
454 }
455 })
456 }
457
458 self.event_mapper.maintain(
459 audio,
460 state,
461 player_entity,
462 camera,
463 &triggers,
464 terrain,
465 client,
466 );
467 }
468
469 #[expect(clippy::single_match)]
470 pub fn handle_outcome(
471 &mut self,
472 outcome: &Outcome,
473 audio: &mut AudioFrontend,
474 client: &Client,
475 ) {
476 if !audio.sfx_enabled() && !audio.subtitles_enabled {
477 return;
478 }
479 let triggers = self.triggers.read();
480 let uids = client.state().ecs().read_storage::<Uid>();
481 if audio.get_listener().is_none() {
482 return;
483 }
484 match outcome {
485 Outcome::Explosion { pos, power, .. } => {
486 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Explosion);
487 audio.emit_sfx(sfx_trigger_item, *pos, Some((power.abs() / 2.5).min(1.5)));
488 },
489 Outcome::Lightning { pos } => {
490 let distance = pos.distance(audio.get_listener_pos());
491 let power = (1.0 - distance / 6_000.0).max(0.0).powi(7);
492 if power > 0.0 {
493 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Lightning);
494 let volume = (power * 3.0).min(2.9);
495 audio.play_ambience_oneshot(
497 super::channel::AmbienceChannelTag::Thunder,
498 sfx_trigger_item,
499 Some(volume),
500 Some(distance / 340.0),
501 );
502 }
503 },
504 Outcome::GroundSlam { pos, .. } | Outcome::ClayGolemDash { pos, .. } => {
505 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GroundSlam);
506 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
507 },
508 Outcome::SurpriseEgg { pos, .. } => {
509 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SurpriseEgg);
510 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
511 },
512 Outcome::Transformation { pos, .. } => {
513 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Transformation);
515 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
516 },
517 Outcome::LaserBeam { pos, .. } => {
518 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LaserBeam);
519 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
520 },
521 Outcome::CyclopsCharge { pos, .. } => {
522 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
523 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
524 },
525 Outcome::FlamethrowerCharge { pos, .. }
526 | Outcome::TerracottaStatueCharge { pos, .. } => {
527 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
528 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
529 },
530 Outcome::FuseCharge { pos, .. } => {
531 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FuseCharge);
532 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
533 },
534 Outcome::Charge { pos, .. } => {
535 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
536 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
537 },
538 Outcome::FlashFreeze { pos, .. } => {
539 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlashFreeze);
540 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
541 },
542 Outcome::SummonedCreature { pos, body, .. } => {
543 match body {
544 Body::BipedSmall(body) => match body.species {
545 biped_small::Species::IronDwarf => {
546 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Bleep);
547 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
548 },
549 biped_small::Species::Boreal | biped_small::Species::Ashen => {
550 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GigaRoar);
551 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
552 },
553 biped_small::Species::ShamanicSpirit | biped_small::Species::Jiangshi => {
554 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
555 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
556 },
557 _ => {},
558 },
559 Body::BipedLarge(body) => match body.species {
560 biped_large::Species::TerracottaBesieger
561 | biped_large::Species::TerracottaPursuer => {
562 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
563 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
564 },
565 _ => {},
566 },
567 Body::BirdMedium(body) => match body.species {
568 bird_medium::Species::Bat => {
569 let sfx_trigger_item =
570 triggers.0.get_key_value(&SfxEvent::BloodmoonHeiressSummon);
571 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
572 },
573 _ => {},
574 },
575 Body::Crustacean(body) => match body.species {
576 crustacean::Species::SoldierCrab => {
577 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Hiss);
578 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
579 },
580 _ => {},
581 },
582 Body::Object(object::Body::Lavathrower) => {
583 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::DeepLaugh);
584 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
585 },
586 Body::Object(object::Body::SeaLantern) => {
587 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LongHiss);
588 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
589 },
590 Body::Object(object::Body::Tornado)
591 | Body::Object(object::Body::FieryTornado) => {
592 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Swoosh);
593 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
594 },
595 _ => { },
597 }
598 },
599 Outcome::GroundDig { pos, .. } => {
600 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GroundDig);
601 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
602 },
603 Outcome::PortalActivated { pos, .. } => {
604 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::PortalActivated);
605 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
606 },
607 Outcome::TeleportedByPortal { pos, .. } => {
608 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::TeleportedByPortal);
609 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
610 },
611 Outcome::IceSpikes { pos, .. } => {
612 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::IceSpikes);
613 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
614 },
615 Outcome::IceCrack { pos, .. } => {
616 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::IceCrack);
617 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
618 },
619 Outcome::Steam { pos, .. } => {
620 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Steam);
621 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
622 },
623 Outcome::FireShockwave { pos, .. } | Outcome::FireLowShockwave { pos, .. } => {
624 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
625 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
626 },
627 Outcome::FromTheAshes { pos, .. } => {
628 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FromTheAshes);
629 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
630 },
631 Outcome::ProjectileShot { pos, body, .. } => {
632 match body {
633 Body::Object(
634 object::Body::Arrow
635 | object::Body::MultiArrow
636 | object::Body::ArrowSnake
637 | object::Body::ArrowTurret
638 | object::Body::ArrowClay
639 | object::Body::ArrowHeavy
640 | object::Body::BoltBesieger
641 | object::Body::HarlequinDagger
642 | object::Body::SpectralSwordSmall
643 | object::Body::SpectralSwordLarge,
644 ) => {
645 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowShot);
646 audio.emit_sfx(sfx_trigger_item, *pos, None);
647 },
648 Body::Object(
649 object::Body::BoltFire
650 | object::Body::BoltFireBig
651 | object::Body::BoltNature
652 | object::Body::BoltIcicle
653 | object::Body::SpearIcicle
654 | object::Body::GrenadeClay
655 | object::Body::SpitPoison,
656 ) => {
657 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FireShot);
658 audio.emit_sfx(sfx_trigger_item, *pos, None);
659 },
660 Body::Object(
661 object::Body::IronPikeBomb
662 | object::Body::BubbleBomb
663 | object::Body::MinotaurAxe
664 | object::Body::Pebble,
665 ) => {
666 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Whoosh);
667 audio.emit_sfx(sfx_trigger_item, *pos, None);
668 },
669 Body::Object(
670 object::Body::LaserBeam
671 | object::Body::LaserBeamSmall
672 | object::Body::LightningBolt,
673 ) => {
674 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LaserBeam);
675 audio.emit_sfx(sfx_trigger_item, *pos, None);
676 },
677 Body::Object(
678 object::Body::AdletTrap | object::Body::BorealTrap | object::Body::Mine,
679 ) => {
680 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Yeet);
681 audio.emit_sfx(sfx_trigger_item, *pos, None);
682 },
683 Body::Object(object::Body::StrigoiHead) => {
684 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::StrigoiHead);
685 audio.emit_sfx(sfx_trigger_item, *pos, None);
686 },
687 _ => {
688 },
690 }
691 },
692 Outcome::ProjectileHit {
693 pos,
694 body,
695 source,
696 target,
697 ..
698 } => match body {
699 Body::Object(
700 object::Body::Arrow
701 | object::Body::MultiArrow
702 | object::Body::ArrowSnake
703 | object::Body::ArrowTurret
704 | object::Body::ArrowClay
705 | object::Body::ArrowHeavy
706 | object::Body::BoltBesieger
707 | object::Body::HarlequinDagger
708 | object::Body::SpectralSwordSmall
709 | object::Body::SpectralSwordLarge
710 | object::Body::Pebble,
711 ) => {
712 if target.is_none() {
713 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowMiss);
714 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
715 } else if *source == client.uid() {
716 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowHit);
717 audio.emit_sfx(
718 sfx_trigger_item,
719 client.position().unwrap_or(*pos),
720 Some(2.0),
721 );
722 } else {
723 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowHit);
724 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
725 }
726 },
727 Body::Object(
728 object::Body::AdletTrap
729 | object::Body::BorealTrap
730 | object::Body::Mine
731 | object::Body::StrigoiHead,
732 ) => {
733 if target.is_none() {
734 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
735 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
736 } else if *source == client.uid() {
737 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
738 audio.emit_sfx(
739 sfx_trigger_item,
740 client.position().unwrap_or(*pos),
741 Some(2.0),
742 );
743 } else {
744 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
745 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
746 }
747 },
748 _ => {},
749 },
750 Outcome::SkillPointGain { uid, .. } => {
751 if let Some(client_uid) = uids.get(client.entity())
752 && uid == client_uid
753 {
754 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SkillPointGain);
755 audio.emit_ui_sfx(sfx_trigger_item, Some(0.4), Some(UiChannelTag::LevelUp));
756 }
757 },
758 Outcome::Beam { pos, specifier } => match specifier {
759 beam::FrontendSpecifier::LifestealBeam
760 | beam::FrontendSpecifier::Steam
761 | beam::FrontendSpecifier::Poison
762 | beam::FrontendSpecifier::Ink
763 | beam::FrontendSpecifier::Lightning
764 | beam::FrontendSpecifier::Frost
765 | beam::FrontendSpecifier::Bubbles => {
766 if rand::rng().random_bool(0.5) {
767 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SceptreBeam);
768 audio.emit_sfx(sfx_trigger_item, *pos, None);
769 };
770 },
771 beam::FrontendSpecifier::Flamethrower
772 | beam::FrontendSpecifier::Cultist
773 | beam::FrontendSpecifier::PhoenixLaser
774 | beam::FrontendSpecifier::FireGigasOverheat
775 | beam::FrontendSpecifier::FirePillar => {
776 if rand::rng().random_bool(0.5) {
777 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
778 audio.emit_sfx(sfx_trigger_item, *pos, None);
779 }
780 },
781 beam::FrontendSpecifier::FlameWallPillar => {
782 if rand::rng().random_bool(0.02) {
783 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
784 audio.emit_sfx(sfx_trigger_item, *pos, None);
785 }
786 },
787 beam::FrontendSpecifier::Gravewarden | beam::FrontendSpecifier::WebStrand => {},
788 },
789 Outcome::SpriteUnlocked { pos } => {
790 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderOpen);
792 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
793 },
794 Outcome::FailedSpriteUnlock { pos } => {
795 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::BreakBlock);
797 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
798 },
799 Outcome::BreakBlock { pos, tool, .. } => {
800 let sfx_trigger_item =
801 triggers
802 .0
803 .get_key_value(&if matches!(tool, Some(ToolKind::Pick)) {
804 SfxEvent::PickaxeBreakBlock
805 } else {
806 SfxEvent::BreakBlock
807 });
808 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(1.2));
809 },
810 Outcome::DamagedBlock {
811 pos,
812 stage_changed,
813 tool,
814 ..
815 } => {
816 let sfx_trigger_item = triggers.0.get_key_value(&match (stage_changed, tool) {
817 (false, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamage,
818 (true, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamageStrong,
819 (_, Some(ToolKind::Shovel)) => return,
821 (_, _) => SfxEvent::BreakBlock,
822 });
823
824 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(1.0));
825 },
826 Outcome::HealthChange { pos, info, .. } => {
827 if info.amount < Health::HEALTH_EPSILON
829 && !matches!(info.cause, Some(DamageSource::Buff(_)))
830 {
831 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Damage);
832 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
833 }
834 },
835 Outcome::Death { pos, .. } => {
836 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Death);
837 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
838 },
839 Outcome::Block { pos, parry, .. } => {
840 if *parry {
841 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Parry);
842 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
843 } else {
844 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Block);
845 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
846 }
847 },
848 Outcome::PoiseChange {
849 pos,
850 state: poise_state,
851 ..
852 } => match poise_state {
853 PoiseState::Normal => {},
854 PoiseState::Interrupted => {
855 let sfx_trigger_item = triggers
856 .0
857 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Interrupted));
858 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
859 },
860 PoiseState::Stunned => {
861 let sfx_trigger_item = triggers
862 .0
863 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Stunned));
864 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
865 },
866 PoiseState::Dazed => {
867 let sfx_trigger_item = triggers
868 .0
869 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Dazed));
870 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
871 },
872 PoiseState::KnockedDown => {
873 let sfx_trigger_item = triggers
874 .0
875 .get_key_value(&SfxEvent::PoiseChange(PoiseState::KnockedDown));
876 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
877 },
878 },
879 Outcome::Utterance { pos, kind, body } => {
880 if let Some(voice) = body_to_voice(body) {
881 let sfx_trigger_item =
882 triggers.0.get_key_value(&SfxEvent::Utterance(*kind, voice));
883 if let Some(sfx_trigger_item) = sfx_trigger_item {
884 audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5));
885 } else {
886 debug!(
887 "No utterance sound effect exists for ({:?}, {:?})",
888 kind, voice
889 );
890 }
891 }
892 },
893 Outcome::Glider { pos, wielded } => {
894 if *wielded {
895 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderOpen);
896 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
897 } else {
898 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderClose);
899 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
900 }
901 },
902 Outcome::SpriteDelete {
903 pos,
904 sprite: SpriteKind::SeaUrchin,
905 } => {
906 let pos = pos.map(|e| e as f32 + 0.5);
907 let power = (0.6 - pos.distance(audio.get_listener_pos()) / 5_000.0)
908 .max(0.0)
909 .powi(7);
910 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Explosion);
911 audio.emit_sfx(sfx_trigger_item, pos, Some((power.abs() / 2.5).min(0.3)));
912 },
913 Outcome::Whoosh { pos, .. } => {
914 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Whoosh);
915 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
916 },
917 Outcome::Swoosh { pos, .. } => {
918 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Swoosh);
919 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
920 },
921 Outcome::Slash { pos, .. } => {
922 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
923 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
924 },
925 Outcome::Bleep { pos, .. } => {
926 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Bleep);
927 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
928 },
929 Outcome::HeadLost { uid, .. } => {
930 let positions = client.state().ecs().read_storage::<common::comp::Pos>();
931 if let Some(pos) = client
932 .state()
933 .ecs()
934 .read_resource::<common::uid::IdMaps>()
935 .uid_entity(*uid)
936 .and_then(|entity| positions.get(entity))
937 {
938 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Death);
939 audio.emit_sfx(sfx_trigger_item, pos.0, Some(2.0));
940 } else {
941 error!("Couldn't get position of entity that lost head");
942 }
943 },
944 Outcome::Splash { vel, pos, mass, .. } => {
945 let magnitude = (-vel.z).max(0.0);
946 let energy = mass * magnitude;
947
948 if energy > 0.0 {
949 let (sfx, volume) = if energy < 10.0 {
950 (SfxEvent::SplashSmall, energy / 20.0)
951 } else if energy < 100.0 {
952 (SfxEvent::SplashMedium, (energy - 10.0) / 90.0 + 0.5)
953 } else {
954 (SfxEvent::SplashBig, (energy / 100.0).sqrt() + 0.5)
955 };
956 let sfx_trigger_item = triggers.0.get_key_value(&sfx);
957 audio.emit_sfx(sfx_trigger_item, *pos, Some(volume.min(2.0)));
958 }
959 },
960 Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => {},
961 _ => {},
962 }
963 }
964
965 fn load_sfx_items() -> AssetHandle<SfxTriggers> {
966 SfxTriggers::load_or_insert_with("voxygen.audio.sfx", |error| {
967 warn!(
968 "Error reading sfx config file, sfx will not be available: {:#?}",
969 error
970 );
971
972 SfxTriggers::default()
973 })
974 }
975}
976
977#[cfg(test)]
978mod tests {
979 use crate::credits::Credits;
980
981 use super::*;
982 use chumsky::container::Seq;
983 use common::assets::{self, AssetExt, Ron};
984 use std::{fs, path::PathBuf};
985
986 #[test]
987 fn test_load_sfx_triggers() { let _ = SfxTriggers::load_expect("voxygen.audio.sfx"); }
988
989 #[test]
990 fn new_sfx_credited() {
991 let sfx_path = assets::ASSETS_PATH.join(std::path::PathBuf::from("voxygen/audio/sfx/"));
992 sfx_path.try_exists().unwrap_or_else(|_| {
993 panic!(
994 "{}/voxygen/audio/sfx does not exist",
995 assets::ASSETS_PATH.display()
996 )
997 });
998 let mut files = Vec::new();
999 list_files(sfx_path.clone(), &mut files);
1000
1001 let credits = Ron::<Credits>::load_expect_cloned("credits").into_inner();
1002 let mut sounds = Vec::new();
1003 for credit in &credits.sounds {
1004 sounds.append(
1005 &mut credit
1006 .files
1007 .iter()
1008 .map(|f| sfx_path.clone().join(f))
1009 .collect::<Vec<PathBuf>>(),
1010 )
1011 }
1012 for file in files.iter() {
1013 if !sounds.contains(file) {
1014 panic!(
1015 "{} was not found in credits. Credit the authors of the sound in \
1016 assets/credits.ron!",
1017 file.display(),
1018 );
1019 }
1020 }
1021 }
1022
1023 fn list_files(path: PathBuf, buffer: &mut Vec<PathBuf>) {
1024 for dir in fs::read_dir(path).expect("Could not read directory") {
1025 if dir
1026 .as_ref()
1027 .expect("Could not read file entry")
1028 .file_type()
1029 .expect("Could not read filetype")
1030 .is_dir()
1031 {
1032 list_files(dir.unwrap().path(), buffer);
1033 } else {
1034 buffer.push(dir.as_ref().unwrap().path());
1035 }
1036 }
1037 }
1038}