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 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 FlameThrower,
163 PoiseChange(PoiseState),
164 GroundSlam,
165 FlashFreeze,
166 GigaRoar,
167 IceSpikes,
168 IceCrack,
169 Utterance(UtteranceKind, VoiceKind),
170 Lightning,
171 CyclopsCharge,
172 TerracottaStatueCharge,
173 LaserBeam,
174 Steam,
175 FuseCharge,
176 Music(ToolKind, AbilitySpec),
177 Yeet,
178 Hiss,
179 LongHiss,
180 Klonk,
181 SmashKlonk,
182 FireShockwave,
183 DeepLaugh,
184 Whoosh,
185 Swoosh,
186 GroundDig,
187 PortalActivated,
188 TeleportedByPortal,
189 FromTheAshes,
190 SurpriseEgg,
191 Transformation,
192 Bleep,
193 Charge,
194 StrigoiHead,
195 BloodmoonHeiressSummon,
196 TrainChugg,
197 TrainChuggSteam,
198 TrainAmbience,
199 TrainClack,
200 TrainSpeed,
201}
202
203#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
204pub enum VoiceKind {
205 HumanFemale,
206 HumanMale,
207 BipedLarge,
208 Wendigo,
209 Reptile,
210 Bird,
211 Critter,
212 Sheep,
213 Pig,
214 Cow,
215 Canine,
216 Dagon,
217 Lion,
218 Mindflayer,
219 Marlin,
220 Maneater,
221 Adlet,
222 Antelope,
223 Alligator,
224 SeaCrocodile,
225 Saurok,
226 Cat,
227 Goat,
228 Mandragora,
229 Asp,
230 Fungome,
231 Truffler,
232 Wolf,
233 Wyvern,
234 Phoenix,
235 VampireBat,
236 Legoom,
237}
238
239fn body_to_voice(body: &Body) -> Option<VoiceKind> {
240 Some(match body {
241 Body::Humanoid(body) => match &body.body_type {
242 humanoid::BodyType::Female => VoiceKind::HumanFemale,
243 humanoid::BodyType::Male => VoiceKind::HumanMale,
244 },
245 Body::QuadrupedLow(body) => match body.species {
246 quadruped_low::Species::Maneater => VoiceKind::Maneater,
247 quadruped_low::Species::Alligator | quadruped_low::Species::Snaretongue => {
248 VoiceKind::Alligator
249 },
250 quadruped_low::Species::SeaCrocodile => VoiceKind::SeaCrocodile,
251 quadruped_low::Species::Dagon => VoiceKind::Dagon,
252 quadruped_low::Species::Asp => VoiceKind::Asp,
253 _ => return None,
254 },
255 Body::QuadrupedSmall(body) => match body.species {
256 quadruped_small::Species::Truffler => VoiceKind::Truffler,
257 quadruped_small::Species::Fungome => VoiceKind::Fungome,
258 quadruped_small::Species::Sheep => VoiceKind::Sheep,
259 quadruped_small::Species::Pig | quadruped_small::Species::Boar => VoiceKind::Pig,
260 quadruped_small::Species::Cat => VoiceKind::Cat,
261 quadruped_small::Species::Goat => VoiceKind::Goat,
262 _ => VoiceKind::Critter,
263 },
264 Body::QuadrupedMedium(body) => match body.species {
265 quadruped_medium::Species::Saber
266 | quadruped_medium::Species::Tiger
267 | quadruped_medium::Species::Lion
268 | quadruped_medium::Species::Frostfang
269 | quadruped_medium::Species::Snowleopard => VoiceKind::Lion,
270 quadruped_medium::Species::Wolf => VoiceKind::Wolf,
271 quadruped_medium::Species::Roshwalr
272 | quadruped_medium::Species::Tarasque
273 | quadruped_medium::Species::Darkhound
274 | quadruped_medium::Species::Bonerattler
275 | quadruped_medium::Species::Grolgar => VoiceKind::Canine,
276 quadruped_medium::Species::Cattle
277 | quadruped_medium::Species::Catoblepas
278 | quadruped_medium::Species::Highland
279 | quadruped_medium::Species::Yak
280 | quadruped_medium::Species::Moose
281 | quadruped_medium::Species::Dreadhorn => VoiceKind::Cow,
282 quadruped_medium::Species::Antelope => VoiceKind::Antelope,
283 _ => return None,
284 },
285 Body::BirdMedium(body) => match body.species {
286 bird_medium::Species::BloodmoonBat | bird_medium::Species::VampireBat => {
287 VoiceKind::VampireBat
288 },
289 _ => VoiceKind::Bird,
290 },
291 Body::BirdLarge(body) => match body.species {
292 bird_large::Species::CloudWyvern
293 | bird_large::Species::FlameWyvern
294 | bird_large::Species::FrostWyvern
295 | bird_large::Species::SeaWyvern
296 | bird_large::Species::WealdWyvern => VoiceKind::Wyvern,
297 bird_large::Species::Phoenix => VoiceKind::Phoenix,
298 _ => VoiceKind::Bird,
299 },
300 Body::BipedSmall(body) => match body.species {
301 biped_small::Species::Adlet => VoiceKind::Adlet,
302 biped_small::Species::Mandragora => VoiceKind::Mandragora,
303 biped_small::Species::Flamekeeper => VoiceKind::BipedLarge,
304 biped_small::Species::GreenLegoom
305 | biped_small::Species::OchreLegoom
306 | biped_small::Species::PurpleLegoom
307 | biped_small::Species::RedLegoom => VoiceKind::Legoom,
308 _ => return None,
309 },
310 Body::BipedLarge(body) => match body.species {
311 biped_large::Species::Wendigo => VoiceKind::Wendigo,
312 biped_large::Species::Occultsaurok
313 | biped_large::Species::Mightysaurok
314 | biped_large::Species::Slysaurok => VoiceKind::Saurok,
315 biped_large::Species::Mindflayer => VoiceKind::Mindflayer,
316 _ => VoiceKind::BipedLarge,
317 },
318 Body::Theropod(_) | Body::Dragon(_) => VoiceKind::Reptile,
319 Body::FishSmall(_) | Body::FishMedium(_) => VoiceKind::Marlin,
320 _ => return None,
321 })
322}
323
324#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
325pub enum SfxInventoryEvent {
326 Collected,
327 CollectedTool(ToolKind),
328 CollectedItem(String),
329 CollectFailed,
330 Consumed(ItemKey),
331 Debug,
332 Dropped,
333 Given,
334 Swapped,
335 Craft,
336}
337
338impl From<&InventoryUpdateEvent> for SfxEvent {
340 fn from(value: &InventoryUpdateEvent) -> Self {
341 match value {
342 InventoryUpdateEvent::Collected(item) => {
343 match &*item.kind() {
346 ItemKind::Tool(tool) => {
347 SfxEvent::Inventory(SfxInventoryEvent::CollectedTool(tool.kind))
348 },
349 ItemKind::Ingredient { .. }
350 if matches!(
351 item.item_definition_id(),
352 ItemDefinitionId::Simple(id) if id.contains("mineral.gem.")
353 ) =>
354 {
355 SfxEvent::Inventory(SfxInventoryEvent::CollectedItem(String::from(
356 "Gemstone",
357 )))
358 },
359 _ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
360 }
361 },
362 InventoryUpdateEvent::BlockCollectFailed { .. }
363 | InventoryUpdateEvent::EntityCollectFailed { .. } => {
364 SfxEvent::Inventory(SfxInventoryEvent::CollectFailed)
365 },
366 InventoryUpdateEvent::Consumed(consumable) => {
367 SfxEvent::Inventory(SfxInventoryEvent::Consumed(consumable.clone()))
368 },
369 InventoryUpdateEvent::Debug => SfxEvent::Inventory(SfxInventoryEvent::Debug),
370 InventoryUpdateEvent::Dropped => SfxEvent::Inventory(SfxInventoryEvent::Dropped),
371 InventoryUpdateEvent::Given => SfxEvent::Inventory(SfxInventoryEvent::Given),
372 InventoryUpdateEvent::Swapped => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
373 InventoryUpdateEvent::Craft => SfxEvent::Inventory(SfxInventoryEvent::Craft),
374 _ => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
375 }
376 }
377}
378
379#[derive(Deserialize, Debug)]
380pub struct SfxTriggerItem {
381 pub files: Vec<String>,
383 pub threshold: f32,
385
386 #[serde(default)]
387 pub subtitle: Option<String>,
388}
389
390pub type SfxTriggers = Ron<HashMap<SfxEvent, SfxTriggerItem>>;
391
392pub struct SfxMgr {
393 pub triggers: AssetHandle<SfxTriggers>,
396 event_mapper: SfxEventMapper,
397}
398
399impl Default for SfxMgr {
400 fn default() -> Self {
401 Self {
402 triggers: Self::load_sfx_items(),
403 event_mapper: SfxEventMapper::new(),
404 }
405 }
406}
407
408impl SfxMgr {
409 pub fn maintain(
410 &mut self,
411 audio: &mut AudioFrontend,
412 state: &State,
413 player_entity: specs::Entity,
414 camera: &Camera,
415 terrain: &Terrain<TerrainChunk>,
416 client: &Client,
417 ) {
418 if !audio.sfx_enabled() && !audio.subtitles_enabled {
421 return;
422 }
423
424 let cam_pos = camera.get_pos_with_focus();
425
426 audio.set_listener_pos(cam_pos, camera.dependents().cam_dir);
429
430 let triggers = self.triggers.read();
431
432 let underwater = state
433 .terrain()
434 .get(cam_pos.map(|e| e.floor() as i32))
435 .map(|b| b.is_liquid())
436 .unwrap_or(false);
437
438 if underwater {
439 audio.set_sfx_master_filter(888);
440 } else {
441 audio.set_sfx_master_filter(20000);
442 }
443
444 let player_pos = client.position().unwrap_or_default();
445
446 if let Some(inner) = audio.inner.as_mut() {
448 inner.player_pos = player_pos;
449 inner.channels.sfx.iter_mut().for_each(|c| {
450 if !c.is_done() {
451 c.update(player_pos)
452 }
453 })
454 }
455
456 self.event_mapper.maintain(
457 audio,
458 state,
459 player_entity,
460 camera,
461 &triggers,
462 terrain,
463 client,
464 );
465 }
466
467 #[expect(clippy::single_match)]
468 pub fn handle_outcome(
469 &mut self,
470 outcome: &Outcome,
471 audio: &mut AudioFrontend,
472 client: &Client,
473 ) {
474 if !audio.sfx_enabled() && !audio.subtitles_enabled {
475 return;
476 }
477 let triggers = self.triggers.read();
478 let uids = client.state().ecs().read_storage::<Uid>();
479 if audio.get_listener().is_none() {
480 return;
481 }
482 match outcome {
483 Outcome::Explosion { pos, power, .. } => {
484 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Explosion);
485 audio.emit_sfx(sfx_trigger_item, *pos, Some((power.abs() / 2.5).min(1.5)));
486 },
487 Outcome::Lightning { pos } => {
488 let distance = pos.distance(audio.get_listener_pos());
489 let power = (1.0 - distance / 6_000.0).max(0.0).powi(7);
490 if power > 0.0 {
491 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Lightning);
492 let volume = (power * 3.0).min(2.9);
493 audio.play_ambience_oneshot(
495 super::channel::AmbienceChannelTag::Thunder,
496 sfx_trigger_item,
497 Some(volume),
498 Some(distance / 340.0),
499 );
500 }
501 },
502 Outcome::GroundSlam { pos, .. } | Outcome::ClayGolemDash { pos, .. } => {
503 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GroundSlam);
504 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
505 },
506 Outcome::SurpriseEgg { pos, .. } => {
507 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SurpriseEgg);
508 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
509 },
510 Outcome::Transformation { pos, .. } => {
511 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Transformation);
513 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
514 },
515 Outcome::LaserBeam { pos, .. } => {
516 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LaserBeam);
517 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
518 },
519 Outcome::CyclopsCharge { pos, .. } => {
520 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
521 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
522 },
523 Outcome::FlamethrowerCharge { pos, .. }
524 | Outcome::TerracottaStatueCharge { pos, .. } => {
525 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
526 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
527 },
528 Outcome::FuseCharge { pos, .. } => {
529 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FuseCharge);
530 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
531 },
532 Outcome::Charge { pos, .. } => {
533 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
534 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
535 },
536 Outcome::FlashFreeze { pos, .. } => {
537 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlashFreeze);
538 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
539 },
540 Outcome::SummonedCreature { pos, body, .. } => {
541 match body {
542 Body::BipedSmall(body) => match body.species {
543 biped_small::Species::IronDwarf => {
544 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Bleep);
545 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
546 },
547 biped_small::Species::Boreal | biped_small::Species::Ashen => {
548 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GigaRoar);
549 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
550 },
551 biped_small::Species::ShamanicSpirit | biped_small::Species::Jiangshi => {
552 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
553 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
554 },
555 _ => {},
556 },
557 Body::BipedLarge(body) => match body.species {
558 biped_large::Species::TerracottaBesieger
559 | biped_large::Species::TerracottaPursuer => {
560 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
561 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
562 },
563 _ => {},
564 },
565 Body::BirdMedium(body) => match body.species {
566 bird_medium::Species::Bat => {
567 let sfx_trigger_item =
568 triggers.0.get_key_value(&SfxEvent::BloodmoonHeiressSummon);
569 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
570 },
571 _ => {},
572 },
573 Body::Crustacean(body) => match body.species {
574 crustacean::Species::SoldierCrab => {
575 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Hiss);
576 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
577 },
578 _ => {},
579 },
580 Body::Object(object::Body::Lavathrower) => {
581 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::DeepLaugh);
582 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
583 },
584 Body::Object(object::Body::SeaLantern) => {
585 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LongHiss);
586 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
587 },
588 Body::Object(object::Body::Tornado)
589 | Body::Object(object::Body::FieryTornado) => {
590 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Swoosh);
591 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
592 },
593 _ => { },
595 }
596 },
597 Outcome::GroundDig { pos, .. } => {
598 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GroundDig);
599 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
600 },
601 Outcome::PortalActivated { pos, .. } => {
602 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::PortalActivated);
603 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
604 },
605 Outcome::TeleportedByPortal { pos, .. } => {
606 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::TeleportedByPortal);
607 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
608 },
609 Outcome::IceSpikes { pos, .. } => {
610 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::IceSpikes);
611 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
612 },
613 Outcome::IceCrack { pos, .. } => {
614 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::IceCrack);
615 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
616 },
617 Outcome::Steam { pos, .. } => {
618 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Steam);
619 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
620 },
621 Outcome::FireShockwave { pos, .. } | Outcome::FireLowShockwave { pos, .. } => {
622 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
623 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
624 },
625 Outcome::FromTheAshes { pos, .. } => {
626 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FromTheAshes);
627 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
628 },
629 Outcome::ProjectileShot { pos, body, .. } => {
630 match body {
631 Body::Object(
632 object::Body::Arrow
633 | object::Body::MultiArrow
634 | object::Body::ArrowSnake
635 | object::Body::ArrowTurret
636 | object::Body::ArrowClay
637 | object::Body::BoltBesieger
638 | object::Body::HarlequinDagger
639 | object::Body::SpectralSwordSmall
640 | object::Body::SpectralSwordLarge,
641 ) => {
642 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowShot);
643 audio.emit_sfx(sfx_trigger_item, *pos, None);
644 },
645 Body::Object(
646 object::Body::BoltFire
647 | object::Body::BoltFireBig
648 | object::Body::BoltNature
649 | object::Body::BoltIcicle
650 | object::Body::SpearIcicle
651 | object::Body::GrenadeClay
652 | object::Body::SpitPoison,
653 ) => {
654 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FireShot);
655 audio.emit_sfx(sfx_trigger_item, *pos, None);
656 },
657 Body::Object(
658 object::Body::IronPikeBomb
659 | object::Body::BubbleBomb
660 | object::Body::MinotaurAxe
661 | object::Body::Pebble,
662 ) => {
663 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Whoosh);
664 audio.emit_sfx(sfx_trigger_item, *pos, None);
665 },
666 Body::Object(
667 object::Body::LaserBeam
668 | object::Body::LaserBeamSmall
669 | object::Body::LightningBolt,
670 ) => {
671 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LaserBeam);
672 audio.emit_sfx(sfx_trigger_item, *pos, None);
673 },
674 Body::Object(
675 object::Body::AdletTrap | object::Body::BorealTrap | object::Body::Mine,
676 ) => {
677 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Yeet);
678 audio.emit_sfx(sfx_trigger_item, *pos, None);
679 },
680 Body::Object(object::Body::StrigoiHead) => {
681 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::StrigoiHead);
682 audio.emit_sfx(sfx_trigger_item, *pos, None);
683 },
684 _ => {
685 },
687 }
688 },
689 Outcome::ProjectileHit {
690 pos,
691 body,
692 source,
693 target,
694 ..
695 } => match body {
696 Body::Object(
697 object::Body::Arrow
698 | object::Body::MultiArrow
699 | object::Body::ArrowSnake
700 | object::Body::ArrowTurret
701 | object::Body::ArrowClay
702 | object::Body::BoltBesieger
703 | object::Body::HarlequinDagger
704 | object::Body::SpectralSwordSmall
705 | object::Body::SpectralSwordLarge
706 | object::Body::Pebble,
707 ) => {
708 if target.is_none() {
709 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowMiss);
710 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
711 } else if *source == client.uid() {
712 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowHit);
713 audio.emit_sfx(
714 sfx_trigger_item,
715 client.position().unwrap_or(*pos),
716 Some(2.0),
717 );
718 } else {
719 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowHit);
720 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
721 }
722 },
723 Body::Object(
724 object::Body::AdletTrap
725 | object::Body::BorealTrap
726 | object::Body::Mine
727 | object::Body::StrigoiHead,
728 ) => {
729 if target.is_none() {
730 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
731 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
732 } else if *source == client.uid() {
733 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
734 audio.emit_sfx(
735 sfx_trigger_item,
736 client.position().unwrap_or(*pos),
737 Some(2.0),
738 );
739 } else {
740 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
741 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
742 }
743 },
744 _ => {},
745 },
746 Outcome::SkillPointGain { uid, .. } => {
747 if let Some(client_uid) = uids.get(client.entity())
748 && uid == client_uid
749 {
750 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SkillPointGain);
751 audio.emit_ui_sfx(sfx_trigger_item, Some(0.4), Some(UiChannelTag::LevelUp));
752 }
753 },
754 Outcome::Beam { pos, specifier } => match specifier {
755 beam::FrontendSpecifier::LifestealBeam
756 | beam::FrontendSpecifier::Steam
757 | beam::FrontendSpecifier::Poison
758 | beam::FrontendSpecifier::Ink
759 | beam::FrontendSpecifier::Lightning
760 | beam::FrontendSpecifier::Frost
761 | beam::FrontendSpecifier::Bubbles => {
762 if rand::rng().random_bool(0.5) {
763 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SceptreBeam);
764 audio.emit_sfx(sfx_trigger_item, *pos, None);
765 };
766 },
767 beam::FrontendSpecifier::Flamethrower
768 | beam::FrontendSpecifier::Cultist
769 | beam::FrontendSpecifier::PhoenixLaser
770 | beam::FrontendSpecifier::FireGigasOverheat
771 | beam::FrontendSpecifier::FirePillar => {
772 if rand::rng().random_bool(0.5) {
773 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
774 audio.emit_sfx(sfx_trigger_item, *pos, None);
775 }
776 },
777 beam::FrontendSpecifier::FlameWallPillar => {
778 if rand::rng().random_bool(0.02) {
779 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
780 audio.emit_sfx(sfx_trigger_item, *pos, None);
781 }
782 },
783 beam::FrontendSpecifier::Gravewarden | beam::FrontendSpecifier::WebStrand => {},
784 },
785 Outcome::SpriteUnlocked { pos } => {
786 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderOpen);
788 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
789 },
790 Outcome::FailedSpriteUnlock { pos } => {
791 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::BreakBlock);
793 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
794 },
795 Outcome::BreakBlock { pos, tool, .. } => {
796 let sfx_trigger_item =
797 triggers
798 .0
799 .get_key_value(&if matches!(tool, Some(ToolKind::Pick)) {
800 SfxEvent::PickaxeBreakBlock
801 } else {
802 SfxEvent::BreakBlock
803 });
804 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(3.0));
805 },
806 Outcome::DamagedBlock {
807 pos,
808 stage_changed,
809 tool,
810 ..
811 } => {
812 let sfx_trigger_item = triggers.0.get_key_value(&match (stage_changed, tool) {
813 (false, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamage,
814 (true, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamageStrong,
815 (_, Some(ToolKind::Shovel)) => return,
817 (_, _) => SfxEvent::BreakBlock,
818 });
819
820 audio.emit_sfx(
821 sfx_trigger_item,
822 pos.map(|e| e as f32 + 0.5),
823 Some(if *stage_changed { 3.0 } else { 2.0 }),
824 );
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 super::*;
980
981 #[test]
982 fn test_load_sfx_triggers() { let _ = SfxTriggers::load_expect("voxygen.audio.sfx"); }
983}