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 => VoiceKind::Legoom,
309 _ => return None,
310 },
311 Body::BipedLarge(body) => match body.species {
312 biped_large::Species::Wendigo => VoiceKind::Wendigo,
313 biped_large::Species::Occultsaurok
314 | biped_large::Species::Mightysaurok
315 | biped_large::Species::Slysaurok => VoiceKind::Saurok,
316 biped_large::Species::Mindflayer => VoiceKind::Mindflayer,
317 _ => VoiceKind::BipedLarge,
318 },
319 Body::Theropod(_) | Body::Dragon(_) => VoiceKind::Reptile,
320 Body::FishSmall(_) | Body::FishMedium(_) => VoiceKind::Marlin,
321 _ => return None,
322 })
323}
324
325#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
326pub enum SfxInventoryEvent {
327 Collected,
328 CollectedTool(ToolKind),
329 CollectedItem(String),
330 CollectFailed,
331 Consumed(ItemKey),
332 Debug,
333 Dropped,
334 Given,
335 Swapped,
336 Craft,
337}
338
339impl From<&InventoryUpdateEvent> for SfxEvent {
341 fn from(value: &InventoryUpdateEvent) -> Self {
342 match value {
343 InventoryUpdateEvent::Collected(item) => {
344 match &*item.kind() {
347 ItemKind::Tool(tool) => {
348 SfxEvent::Inventory(SfxInventoryEvent::CollectedTool(tool.kind))
349 },
350 ItemKind::Ingredient { .. }
351 if matches!(
352 item.item_definition_id(),
353 ItemDefinitionId::Simple(id) if id.contains("mineral.gem.")
354 ) =>
355 {
356 SfxEvent::Inventory(SfxInventoryEvent::CollectedItem(String::from(
357 "Gemstone",
358 )))
359 },
360 _ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
361 }
362 },
363 InventoryUpdateEvent::BlockCollectFailed { .. }
364 | InventoryUpdateEvent::EntityCollectFailed { .. } => {
365 SfxEvent::Inventory(SfxInventoryEvent::CollectFailed)
366 },
367 InventoryUpdateEvent::Consumed(consumable) => {
368 SfxEvent::Inventory(SfxInventoryEvent::Consumed(consumable.clone()))
369 },
370 InventoryUpdateEvent::Debug => SfxEvent::Inventory(SfxInventoryEvent::Debug),
371 InventoryUpdateEvent::Dropped => SfxEvent::Inventory(SfxInventoryEvent::Dropped),
372 InventoryUpdateEvent::Given => SfxEvent::Inventory(SfxInventoryEvent::Given),
373 InventoryUpdateEvent::Swapped => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
374 InventoryUpdateEvent::Craft => SfxEvent::Inventory(SfxInventoryEvent::Craft),
375 _ => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
376 }
377 }
378}
379
380#[derive(Deserialize, Debug)]
381pub struct SfxTriggerItem {
382 pub files: Vec<String>,
384 pub threshold: f32,
386
387 #[serde(default)]
388 pub subtitle: Option<String>,
389}
390
391pub type SfxTriggers = Ron<HashMap<SfxEvent, SfxTriggerItem>>;
392
393pub struct SfxMgr {
394 pub triggers: AssetHandle<SfxTriggers>,
397 event_mapper: SfxEventMapper,
398}
399
400impl Default for SfxMgr {
401 fn default() -> Self {
402 Self {
403 triggers: Self::load_sfx_items(),
404 event_mapper: SfxEventMapper::new(),
405 }
406 }
407}
408
409impl SfxMgr {
410 pub fn maintain(
411 &mut self,
412 audio: &mut AudioFrontend,
413 state: &State,
414 player_entity: specs::Entity,
415 camera: &Camera,
416 terrain: &Terrain<TerrainChunk>,
417 client: &Client,
418 ) {
419 if !audio.sfx_enabled() && !audio.subtitles_enabled {
422 return;
423 }
424
425 let cam_pos = camera.get_pos_with_focus();
426
427 audio.set_listener_pos(cam_pos, camera.dependents().cam_dir);
430
431 let triggers = self.triggers.read();
432
433 let underwater = state
434 .terrain()
435 .get(cam_pos.map(|e| e.floor() as i32))
436 .map(|b| b.is_liquid())
437 .unwrap_or(false);
438
439 if underwater {
440 audio.set_sfx_master_filter(888);
441 } else {
442 audio.set_sfx_master_filter(20000);
443 }
444
445 let player_pos = client.position().unwrap_or_default();
446
447 if let Some(inner) = audio.inner.as_mut() {
449 inner.player_pos = player_pos;
450 inner.channels.sfx.iter_mut().for_each(|c| {
451 if !c.is_done() {
452 c.update(player_pos)
453 }
454 })
455 }
456
457 self.event_mapper.maintain(
458 audio,
459 state,
460 player_entity,
461 camera,
462 &triggers,
463 terrain,
464 client,
465 );
466 }
467
468 #[expect(clippy::single_match)]
469 pub fn handle_outcome(
470 &mut self,
471 outcome: &Outcome,
472 audio: &mut AudioFrontend,
473 client: &Client,
474 ) {
475 if !audio.sfx_enabled() && !audio.subtitles_enabled {
476 return;
477 }
478 let triggers = self.triggers.read();
479 let uids = client.state().ecs().read_storage::<Uid>();
480 if audio.get_listener().is_none() {
481 return;
482 }
483 match outcome {
484 Outcome::Explosion { pos, power, .. } => {
485 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Explosion);
486 audio.emit_sfx(sfx_trigger_item, *pos, Some((power.abs() / 2.5).min(1.5)));
487 },
488 Outcome::Lightning { pos } => {
489 let distance = pos.distance(audio.get_listener_pos());
490 let power = (1.0 - distance / 6_000.0).max(0.0).powi(7);
491 if power > 0.0 {
492 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Lightning);
493 let volume = (power * 3.0).min(2.9);
494 audio.play_ambience_oneshot(
496 super::channel::AmbienceChannelTag::Thunder,
497 sfx_trigger_item,
498 Some(volume),
499 Some(distance / 340.0),
500 );
501 }
502 },
503 Outcome::GroundSlam { pos, .. } | Outcome::ClayGolemDash { pos, .. } => {
504 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GroundSlam);
505 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
506 },
507 Outcome::SurpriseEgg { pos, .. } => {
508 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SurpriseEgg);
509 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
510 },
511 Outcome::Transformation { pos, .. } => {
512 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Transformation);
514 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
515 },
516 Outcome::LaserBeam { pos, .. } => {
517 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LaserBeam);
518 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
519 },
520 Outcome::CyclopsCharge { pos, .. } => {
521 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
522 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
523 },
524 Outcome::FlamethrowerCharge { pos, .. }
525 | Outcome::TerracottaStatueCharge { pos, .. } => {
526 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
527 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
528 },
529 Outcome::FuseCharge { pos, .. } => {
530 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FuseCharge);
531 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
532 },
533 Outcome::Charge { 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::FlashFreeze { pos, .. } => {
538 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlashFreeze);
539 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
540 },
541 Outcome::SummonedCreature { pos, body, .. } => {
542 match body {
543 Body::BipedSmall(body) => match body.species {
544 biped_small::Species::IronDwarf => {
545 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Bleep);
546 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
547 },
548 biped_small::Species::Boreal | biped_small::Species::Ashen => {
549 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GigaRoar);
550 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
551 },
552 biped_small::Species::ShamanicSpirit | biped_small::Species::Jiangshi => {
553 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
554 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
555 },
556 _ => {},
557 },
558 Body::BipedLarge(body) => match body.species {
559 biped_large::Species::TerracottaBesieger
560 | biped_large::Species::TerracottaPursuer => {
561 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
562 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
563 },
564 _ => {},
565 },
566 Body::BirdMedium(body) => match body.species {
567 bird_medium::Species::Bat => {
568 let sfx_trigger_item =
569 triggers.0.get_key_value(&SfxEvent::BloodmoonHeiressSummon);
570 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
571 },
572 _ => {},
573 },
574 Body::Crustacean(body) => match body.species {
575 crustacean::Species::SoldierCrab => {
576 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Hiss);
577 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
578 },
579 _ => {},
580 },
581 Body::Object(object::Body::Lavathrower) => {
582 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::DeepLaugh);
583 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
584 },
585 Body::Object(object::Body::SeaLantern) => {
586 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LongHiss);
587 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
588 },
589 Body::Object(object::Body::Tornado)
590 | Body::Object(object::Body::FieryTornado) => {
591 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Swoosh);
592 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
593 },
594 _ => { },
596 }
597 },
598 Outcome::GroundDig { pos, .. } => {
599 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GroundDig);
600 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
601 },
602 Outcome::PortalActivated { pos, .. } => {
603 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::PortalActivated);
604 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
605 },
606 Outcome::TeleportedByPortal { pos, .. } => {
607 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::TeleportedByPortal);
608 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
609 },
610 Outcome::IceSpikes { pos, .. } => {
611 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::IceSpikes);
612 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
613 },
614 Outcome::IceCrack { pos, .. } => {
615 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::IceCrack);
616 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
617 },
618 Outcome::Steam { pos, .. } => {
619 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Steam);
620 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
621 },
622 Outcome::FireShockwave { pos, .. } | Outcome::FireLowShockwave { pos, .. } => {
623 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
624 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
625 },
626 Outcome::FromTheAshes { pos, .. } => {
627 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FromTheAshes);
628 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
629 },
630 Outcome::ProjectileShot { pos, body, .. } => {
631 match body {
632 Body::Object(
633 object::Body::Arrow
634 | object::Body::MultiArrow
635 | object::Body::ArrowSnake
636 | object::Body::ArrowTurret
637 | object::Body::ArrowClay
638 | object::Body::ArrowHeavy
639 | object::Body::BoltBesieger
640 | object::Body::HarlequinDagger
641 | object::Body::SpectralSwordSmall
642 | object::Body::SpectralSwordLarge,
643 ) => {
644 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowShot);
645 audio.emit_sfx(sfx_trigger_item, *pos, None);
646 },
647 Body::Object(
648 object::Body::BoltFire
649 | object::Body::BoltFireBig
650 | object::Body::BoltNature
651 | object::Body::BoltIcicle
652 | object::Body::SpearIcicle
653 | object::Body::GrenadeClay
654 | object::Body::SpitPoison,
655 ) => {
656 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FireShot);
657 audio.emit_sfx(sfx_trigger_item, *pos, None);
658 },
659 Body::Object(
660 object::Body::IronPikeBomb
661 | object::Body::BubbleBomb
662 | object::Body::MinotaurAxe
663 | object::Body::Pebble,
664 ) => {
665 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Whoosh);
666 audio.emit_sfx(sfx_trigger_item, *pos, None);
667 },
668 Body::Object(
669 object::Body::LaserBeam
670 | object::Body::LaserBeamSmall
671 | object::Body::LightningBolt,
672 ) => {
673 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LaserBeam);
674 audio.emit_sfx(sfx_trigger_item, *pos, None);
675 },
676 Body::Object(
677 object::Body::AdletTrap | object::Body::BorealTrap | object::Body::Mine,
678 ) => {
679 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Yeet);
680 audio.emit_sfx(sfx_trigger_item, *pos, None);
681 },
682 Body::Object(object::Body::StrigoiHead) => {
683 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::StrigoiHead);
684 audio.emit_sfx(sfx_trigger_item, *pos, None);
685 },
686 _ => {
687 },
689 }
690 },
691 Outcome::ProjectileHit {
692 pos,
693 body,
694 source,
695 target,
696 ..
697 } => match body {
698 Body::Object(
699 object::Body::Arrow
700 | object::Body::MultiArrow
701 | object::Body::ArrowSnake
702 | object::Body::ArrowTurret
703 | object::Body::ArrowClay
704 | object::Body::ArrowHeavy
705 | object::Body::BoltBesieger
706 | object::Body::HarlequinDagger
707 | object::Body::SpectralSwordSmall
708 | object::Body::SpectralSwordLarge
709 | object::Body::Pebble,
710 ) => {
711 if target.is_none() {
712 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowMiss);
713 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
714 } else if *source == client.uid() {
715 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowHit);
716 audio.emit_sfx(
717 sfx_trigger_item,
718 client.position().unwrap_or(*pos),
719 Some(2.0),
720 );
721 } else {
722 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowHit);
723 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
724 }
725 },
726 Body::Object(
727 object::Body::AdletTrap
728 | object::Body::BorealTrap
729 | object::Body::Mine
730 | object::Body::StrigoiHead,
731 ) => {
732 if target.is_none() {
733 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
734 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
735 } else if *source == client.uid() {
736 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
737 audio.emit_sfx(
738 sfx_trigger_item,
739 client.position().unwrap_or(*pos),
740 Some(2.0),
741 );
742 } else {
743 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
744 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
745 }
746 },
747 _ => {},
748 },
749 Outcome::SkillPointGain { uid, .. } => {
750 if let Some(client_uid) = uids.get(client.entity())
751 && uid == client_uid
752 {
753 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SkillPointGain);
754 audio.emit_ui_sfx(sfx_trigger_item, Some(0.4), Some(UiChannelTag::LevelUp));
755 }
756 },
757 Outcome::Beam { pos, specifier } => match specifier {
758 beam::FrontendSpecifier::LifestealBeam
759 | beam::FrontendSpecifier::Steam
760 | beam::FrontendSpecifier::Poison
761 | beam::FrontendSpecifier::Ink
762 | beam::FrontendSpecifier::Lightning
763 | beam::FrontendSpecifier::Frost
764 | beam::FrontendSpecifier::Bubbles => {
765 if rand::rng().random_bool(0.5) {
766 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SceptreBeam);
767 audio.emit_sfx(sfx_trigger_item, *pos, None);
768 };
769 },
770 beam::FrontendSpecifier::Flamethrower
771 | beam::FrontendSpecifier::Cultist
772 | beam::FrontendSpecifier::PhoenixLaser
773 | beam::FrontendSpecifier::FireGigasOverheat
774 | beam::FrontendSpecifier::FirePillar => {
775 if rand::rng().random_bool(0.5) {
776 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
777 audio.emit_sfx(sfx_trigger_item, *pos, None);
778 }
779 },
780 beam::FrontendSpecifier::FlameWallPillar => {
781 if rand::rng().random_bool(0.02) {
782 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
783 audio.emit_sfx(sfx_trigger_item, *pos, None);
784 }
785 },
786 beam::FrontendSpecifier::Gravewarden | beam::FrontendSpecifier::WebStrand => {},
787 },
788 Outcome::SpriteUnlocked { pos } => {
789 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderOpen);
791 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
792 },
793 Outcome::FailedSpriteUnlock { pos } => {
794 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::BreakBlock);
796 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
797 },
798 Outcome::BreakBlock { pos, tool, .. } => {
799 let sfx_trigger_item =
800 triggers
801 .0
802 .get_key_value(&if matches!(tool, Some(ToolKind::Pick)) {
803 SfxEvent::PickaxeBreakBlock
804 } else {
805 SfxEvent::BreakBlock
806 });
807 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(3.0));
808 },
809 Outcome::DamagedBlock {
810 pos,
811 stage_changed,
812 tool,
813 ..
814 } => {
815 let sfx_trigger_item = triggers.0.get_key_value(&match (stage_changed, tool) {
816 (false, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamage,
817 (true, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamageStrong,
818 (_, Some(ToolKind::Shovel)) => return,
820 (_, _) => SfxEvent::BreakBlock,
821 });
822
823 audio.emit_sfx(
824 sfx_trigger_item,
825 pos.map(|e| e as f32 + 0.5),
826 Some(if *stage_changed { 3.0 } else { 2.0 }),
827 );
828 },
829 Outcome::HealthChange { pos, info, .. } => {
830 if info.amount < Health::HEALTH_EPSILON
832 && !matches!(info.cause, Some(DamageSource::Buff(_)))
833 {
834 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Damage);
835 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
836 }
837 },
838 Outcome::Death { pos, .. } => {
839 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Death);
840 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
841 },
842 Outcome::Block { pos, parry, .. } => {
843 if *parry {
844 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Parry);
845 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
846 } else {
847 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Block);
848 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
849 }
850 },
851 Outcome::PoiseChange {
852 pos,
853 state: poise_state,
854 ..
855 } => match poise_state {
856 PoiseState::Normal => {},
857 PoiseState::Interrupted => {
858 let sfx_trigger_item = triggers
859 .0
860 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Interrupted));
861 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
862 },
863 PoiseState::Stunned => {
864 let sfx_trigger_item = triggers
865 .0
866 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Stunned));
867 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
868 },
869 PoiseState::Dazed => {
870 let sfx_trigger_item = triggers
871 .0
872 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Dazed));
873 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
874 },
875 PoiseState::KnockedDown => {
876 let sfx_trigger_item = triggers
877 .0
878 .get_key_value(&SfxEvent::PoiseChange(PoiseState::KnockedDown));
879 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
880 },
881 },
882 Outcome::Utterance { pos, kind, body } => {
883 if let Some(voice) = body_to_voice(body) {
884 let sfx_trigger_item =
885 triggers.0.get_key_value(&SfxEvent::Utterance(*kind, voice));
886 if let Some(sfx_trigger_item) = sfx_trigger_item {
887 audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5));
888 } else {
889 debug!(
890 "No utterance sound effect exists for ({:?}, {:?})",
891 kind, voice
892 );
893 }
894 }
895 },
896 Outcome::Glider { pos, wielded } => {
897 if *wielded {
898 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderOpen);
899 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
900 } else {
901 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderClose);
902 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
903 }
904 },
905 Outcome::SpriteDelete {
906 pos,
907 sprite: SpriteKind::SeaUrchin,
908 } => {
909 let pos = pos.map(|e| e as f32 + 0.5);
910 let power = (0.6 - pos.distance(audio.get_listener_pos()) / 5_000.0)
911 .max(0.0)
912 .powi(7);
913 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Explosion);
914 audio.emit_sfx(sfx_trigger_item, pos, Some((power.abs() / 2.5).min(0.3)));
915 },
916 Outcome::Whoosh { pos, .. } => {
917 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Whoosh);
918 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
919 },
920 Outcome::Swoosh { pos, .. } => {
921 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Swoosh);
922 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
923 },
924 Outcome::Slash { pos, .. } => {
925 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
926 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
927 },
928 Outcome::Bleep { pos, .. } => {
929 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Bleep);
930 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
931 },
932 Outcome::HeadLost { uid, .. } => {
933 let positions = client.state().ecs().read_storage::<common::comp::Pos>();
934 if let Some(pos) = client
935 .state()
936 .ecs()
937 .read_resource::<common::uid::IdMaps>()
938 .uid_entity(*uid)
939 .and_then(|entity| positions.get(entity))
940 {
941 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Death);
942 audio.emit_sfx(sfx_trigger_item, pos.0, Some(2.0));
943 } else {
944 error!("Couldn't get position of entity that lost head");
945 }
946 },
947 Outcome::Splash { vel, pos, mass, .. } => {
948 let magnitude = (-vel.z).max(0.0);
949 let energy = mass * magnitude;
950
951 if energy > 0.0 {
952 let (sfx, volume) = if energy < 10.0 {
953 (SfxEvent::SplashSmall, energy / 20.0)
954 } else if energy < 100.0 {
955 (SfxEvent::SplashMedium, (energy - 10.0) / 90.0 + 0.5)
956 } else {
957 (SfxEvent::SplashBig, (energy / 100.0).sqrt() + 0.5)
958 };
959 let sfx_trigger_item = triggers.0.get_key_value(&sfx);
960 audio.emit_sfx(sfx_trigger_item, *pos, Some(volume.min(2.0)));
961 }
962 },
963 Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => {},
964 _ => {},
965 }
966 }
967
968 fn load_sfx_items() -> AssetHandle<SfxTriggers> {
969 SfxTriggers::load_or_insert_with("voxygen.audio.sfx", |error| {
970 warn!(
971 "Error reading sfx config file, sfx will not be available: {:#?}",
972 error
973 );
974
975 SfxTriggers::default()
976 })
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983
984 #[test]
985 fn test_load_sfx_triggers() { let _ = SfxTriggers::load_expect("voxygen.audio.sfx"); }
986}