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