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 if let Some(inner) = audio.inner.as_mut() {
446 let player_pos = client.position().unwrap_or_default();
447 inner.channels.sfx.iter_mut().for_each(|c| {
448 if !c.is_done() {
449 c.update(player_pos)
450 }
451 })
452 }
453
454 self.event_mapper.maintain(
455 audio,
456 state,
457 player_entity,
458 camera,
459 &triggers,
460 terrain,
461 client,
462 );
463 }
464
465 #[expect(clippy::single_match)]
466 pub fn handle_outcome(
467 &mut self,
468 outcome: &Outcome,
469 audio: &mut AudioFrontend,
470 client: &Client,
471 ) {
472 if !audio.sfx_enabled() && !audio.subtitles_enabled {
473 return;
474 }
475 let triggers = self.triggers.read();
476 let uids = client.state().ecs().read_storage::<Uid>();
477 let player_pos = client.position().unwrap_or_default();
478 if audio.get_listener().is_none() {
479 return;
480 }
481 match outcome {
482 Outcome::Explosion { pos, power, .. } => {
483 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Explosion);
484 audio.emit_sfx(
485 sfx_trigger_item,
486 *pos,
487 Some((power.abs() / 2.5).min(1.5)),
488 player_pos,
489 );
490 },
491 Outcome::Lightning { pos } => {
492 let distance = pos.distance(audio.get_listener_pos());
493 let power = (1.0 - distance / 6_000.0).max(0.0).powi(7);
494 if power > 0.0 {
495 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Lightning);
496 let volume = (power * 3.0).min(2.9);
497 audio.play_ambience_oneshot(
499 super::channel::AmbienceChannelTag::Thunder,
500 sfx_trigger_item,
501 Some(volume),
502 Some(distance / 340.0),
503 );
504 }
505 },
506 Outcome::GroundSlam { pos, .. } | Outcome::ClayGolemDash { pos, .. } => {
507 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GroundSlam);
508 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
509 },
510 Outcome::SurpriseEgg { pos, .. } => {
511 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SurpriseEgg);
512 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
513 },
514 Outcome::Transformation { pos, .. } => {
515 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Transformation);
517 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
518 },
519 Outcome::LaserBeam { pos, .. } => {
520 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LaserBeam);
521 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
522 },
523 Outcome::CyclopsCharge { pos, .. } => {
524 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
525 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
526 },
527 Outcome::FlamethrowerCharge { pos, .. }
528 | Outcome::TerracottaStatueCharge { pos, .. } => {
529 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
530 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
531 },
532 Outcome::FuseCharge { pos, .. } => {
533 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FuseCharge);
534 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
535 },
536 Outcome::Charge { pos, .. } => {
537 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::CyclopsCharge);
538 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
539 },
540 Outcome::FlashFreeze { pos, .. } => {
541 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlashFreeze);
542 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
543 },
544 Outcome::SummonedCreature { pos, body, .. } => {
545 match body {
546 Body::BipedSmall(body) => match body.species {
547 biped_small::Species::IronDwarf => {
548 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Bleep);
549 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
550 },
551 biped_small::Species::Boreal | biped_small::Species::Ashen => {
552 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GigaRoar);
553 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
554 },
555 biped_small::Species::ShamanicSpirit | biped_small::Species::Jiangshi => {
556 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
557 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
558 },
559 _ => {},
560 },
561 Body::BipedLarge(body) => match body.species {
562 biped_large::Species::TerracottaBesieger
563 | biped_large::Species::TerracottaPursuer => {
564 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
565 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
566 },
567 _ => {},
568 },
569 Body::BirdMedium(body) => match body.species {
570 bird_medium::Species::Bat => {
571 let sfx_trigger_item =
572 triggers.0.get_key_value(&SfxEvent::BloodmoonHeiressSummon);
573 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
574 },
575 _ => {},
576 },
577 Body::Crustacean(body) => match body.species {
578 crustacean::Species::SoldierCrab => {
579 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Hiss);
580 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
581 },
582 _ => {},
583 },
584 Body::Object(object::Body::Lavathrower) => {
585 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::DeepLaugh);
586 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
587 },
588 Body::Object(object::Body::SeaLantern) => {
589 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LongHiss);
590 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
591 },
592 Body::Object(object::Body::Tornado)
593 | Body::Object(object::Body::FieryTornado) => {
594 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Swoosh);
595 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
596 },
597 _ => { },
599 }
600 },
601 Outcome::GroundDig { pos, .. } => {
602 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GroundDig);
603 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
604 },
605 Outcome::PortalActivated { pos, .. } => {
606 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::PortalActivated);
607 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
608 },
609 Outcome::TeleportedByPortal { pos, .. } => {
610 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::TeleportedByPortal);
611 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
612 },
613 Outcome::IceSpikes { pos, .. } => {
614 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::IceSpikes);
615 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
616 },
617 Outcome::IceCrack { pos, .. } => {
618 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::IceCrack);
619 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
620 },
621 Outcome::Steam { pos, .. } => {
622 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Steam);
623 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
624 },
625 Outcome::FireShockwave { pos, .. } | Outcome::FireLowShockwave { pos, .. } => {
626 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
627 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
628 },
629 Outcome::FromTheAshes { pos, .. } => {
630 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FromTheAshes);
631 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
632 },
633 Outcome::ProjectileShot { pos, body, .. } => {
634 match body {
635 Body::Object(
636 object::Body::Arrow
637 | object::Body::MultiArrow
638 | object::Body::ArrowSnake
639 | object::Body::ArrowTurret
640 | object::Body::ArrowClay
641 | object::Body::BoltBesieger
642 | object::Body::HarlequinDagger
643 | object::Body::SpectralSwordSmall
644 | object::Body::SpectralSwordLarge,
645 ) => {
646 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowShot);
647 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
648 },
649 Body::Object(
650 object::Body::BoltFire
651 | object::Body::BoltFireBig
652 | object::Body::BoltNature
653 | object::Body::BoltIcicle
654 | object::Body::SpearIcicle
655 | object::Body::GrenadeClay
656 | object::Body::SpitPoison,
657 ) => {
658 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FireShot);
659 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
660 },
661 Body::Object(
662 object::Body::IronPikeBomb
663 | object::Body::BubbleBomb
664 | object::Body::MinotaurAxe
665 | object::Body::Pebble,
666 ) => {
667 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Whoosh);
668 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
669 },
670 Body::Object(
671 object::Body::LaserBeam
672 | object::Body::LaserBeamSmall
673 | object::Body::LightningBolt,
674 ) => {
675 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::LaserBeam);
676 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
677 },
678 Body::Object(
679 object::Body::AdletTrap | object::Body::BorealTrap | object::Body::Mine,
680 ) => {
681 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Yeet);
682 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
683 },
684 Body::Object(object::Body::StrigoiHead) => {
685 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::StrigoiHead);
686 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
687 },
688 _ => {
689 },
691 }
692 },
693 Outcome::ProjectileHit {
694 pos,
695 body,
696 source,
697 target,
698 ..
699 } => match body {
700 Body::Object(
701 object::Body::Arrow
702 | object::Body::MultiArrow
703 | object::Body::ArrowSnake
704 | object::Body::ArrowTurret
705 | object::Body::ArrowClay
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), player_pos);
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 player_pos,
722 );
723 } else {
724 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::ArrowHit);
725 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
726 }
727 },
728 Body::Object(
729 object::Body::AdletTrap
730 | object::Body::BorealTrap
731 | object::Body::Mine
732 | object::Body::StrigoiHead,
733 ) => {
734 if target.is_none() {
735 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Klonk);
736 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
737 } else if *source == client.uid() {
738 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
739 audio.emit_sfx(
740 sfx_trigger_item,
741 client.position().unwrap_or(*pos),
742 Some(2.0),
743 player_pos,
744 );
745 } else {
746 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
747 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), player_pos);
748 }
749 },
750 _ => {},
751 },
752 Outcome::SkillPointGain { uid, .. } => {
753 if let Some(client_uid) = uids.get(client.entity())
754 && uid == client_uid
755 {
756 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SkillPointGain);
757 audio.emit_ui_sfx(sfx_trigger_item, Some(0.4), Some(UiChannelTag::LevelUp));
758 }
759 },
760 Outcome::Beam { pos, specifier } => match specifier {
761 beam::FrontendSpecifier::LifestealBeam
762 | beam::FrontendSpecifier::Steam
763 | beam::FrontendSpecifier::Poison
764 | beam::FrontendSpecifier::Ink
765 | beam::FrontendSpecifier::Lightning
766 | beam::FrontendSpecifier::Frost
767 | beam::FrontendSpecifier::Bubbles => {
768 if rand::rng().random_bool(0.5) {
769 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SceptreBeam);
770 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
771 };
772 },
773 beam::FrontendSpecifier::Flamethrower
774 | beam::FrontendSpecifier::Cultist
775 | beam::FrontendSpecifier::PhoenixLaser
776 | beam::FrontendSpecifier::FireGigasOverheat
777 | beam::FrontendSpecifier::FirePillar => {
778 if rand::rng().random_bool(0.5) {
779 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
780 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
781 }
782 },
783 beam::FrontendSpecifier::FlameWallPillar => {
784 if rand::rng().random_bool(0.02) {
785 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::FlameThrower);
786 audio.emit_sfx(sfx_trigger_item, *pos, None, player_pos);
787 }
788 },
789 beam::FrontendSpecifier::Gravewarden | beam::FrontendSpecifier::WebStrand => {},
790 },
791 Outcome::SpriteUnlocked { pos } => {
792 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderOpen);
794 audio.emit_sfx(
795 sfx_trigger_item,
796 pos.map(|e| e as f32 + 0.5),
797 Some(2.0),
798 player_pos,
799 );
800 },
801 Outcome::FailedSpriteUnlock { pos } => {
802 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::BreakBlock);
804 audio.emit_sfx(
805 sfx_trigger_item,
806 pos.map(|e| e as f32 + 0.5),
807 Some(2.0),
808 player_pos,
809 );
810 },
811 Outcome::BreakBlock { pos, tool, .. } => {
812 let sfx_trigger_item =
813 triggers
814 .0
815 .get_key_value(&if matches!(tool, Some(ToolKind::Pick)) {
816 SfxEvent::PickaxeBreakBlock
817 } else {
818 SfxEvent::BreakBlock
819 });
820 audio.emit_sfx(
821 sfx_trigger_item,
822 pos.map(|e| e as f32 + 0.5),
823 Some(3.0),
824 player_pos,
825 );
826 },
827 Outcome::DamagedBlock {
828 pos,
829 stage_changed,
830 tool,
831 ..
832 } => {
833 let sfx_trigger_item = triggers.0.get_key_value(&match (stage_changed, tool) {
834 (false, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamage,
835 (true, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamageStrong,
836 (_, Some(ToolKind::Shovel)) => return,
838 (_, _) => SfxEvent::BreakBlock,
839 });
840
841 audio.emit_sfx(
842 sfx_trigger_item,
843 pos.map(|e| e as f32 + 0.5),
844 Some(if *stage_changed { 3.0 } else { 2.0 }),
845 player_pos,
846 );
847 },
848 Outcome::HealthChange { pos, info, .. } => {
849 if info.amount < Health::HEALTH_EPSILON
851 && !matches!(info.cause, Some(DamageSource::Buff(_)))
852 {
853 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Damage);
854 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), player_pos);
855 }
856 },
857 Outcome::Death { pos, .. } => {
858 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Death);
859 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), player_pos);
860 },
861 Outcome::Block { pos, parry, .. } => {
862 if *parry {
863 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Parry);
864 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), player_pos);
865 } else {
866 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Block);
867 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), player_pos);
868 }
869 },
870 Outcome::PoiseChange {
871 pos,
872 state: poise_state,
873 ..
874 } => match poise_state {
875 PoiseState::Normal => {},
876 PoiseState::Interrupted => {
877 let sfx_trigger_item = triggers
878 .0
879 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Interrupted));
880 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), player_pos);
881 },
882 PoiseState::Stunned => {
883 let sfx_trigger_item = triggers
884 .0
885 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Stunned));
886 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), player_pos);
887 },
888 PoiseState::Dazed => {
889 let sfx_trigger_item = triggers
890 .0
891 .get_key_value(&SfxEvent::PoiseChange(PoiseState::Dazed));
892 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), player_pos);
893 },
894 PoiseState::KnockedDown => {
895 let sfx_trigger_item = triggers
896 .0
897 .get_key_value(&SfxEvent::PoiseChange(PoiseState::KnockedDown));
898 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), player_pos);
899 },
900 },
901 Outcome::Utterance { pos, kind, body } => {
902 if let Some(voice) = body_to_voice(body) {
903 let sfx_trigger_item =
904 triggers.0.get_key_value(&SfxEvent::Utterance(*kind, voice));
905 if let Some(sfx_trigger_item) = sfx_trigger_item {
906 audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5), player_pos);
907 } else {
908 debug!(
909 "No utterance sound effect exists for ({:?}, {:?})",
910 kind, voice
911 );
912 }
913 }
914 },
915 Outcome::Glider { pos, wielded } => {
916 if *wielded {
917 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderOpen);
918 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), player_pos);
919 } else {
920 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::GliderClose);
921 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), player_pos);
922 }
923 },
924 Outcome::SpriteDelete {
925 pos,
926 sprite: SpriteKind::SeaUrchin,
927 } => {
928 let pos = pos.map(|e| e as f32 + 0.5);
929 let power = (0.6 - pos.distance(audio.get_listener_pos()) / 5_000.0)
930 .max(0.0)
931 .powi(7);
932 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Explosion);
933 audio.emit_sfx(
934 sfx_trigger_item,
935 pos,
936 Some((power.abs() / 2.5).min(0.3)),
937 player_pos,
938 );
939 },
940 Outcome::Whoosh { pos, .. } => {
941 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Whoosh);
942 audio.emit_sfx(
943 sfx_trigger_item,
944 pos.map(|e| e + 0.5),
945 Some(3.0),
946 player_pos,
947 );
948 },
949 Outcome::Swoosh { pos, .. } => {
950 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Swoosh);
951 audio.emit_sfx(
952 sfx_trigger_item,
953 pos.map(|e| e + 0.5),
954 Some(3.0),
955 player_pos,
956 );
957 },
958 Outcome::Slash { pos, .. } => {
959 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::SmashKlonk);
960 audio.emit_sfx(
961 sfx_trigger_item,
962 pos.map(|e| e + 0.5),
963 Some(3.0),
964 player_pos,
965 );
966 },
967 Outcome::Bleep { pos, .. } => {
968 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Bleep);
969 audio.emit_sfx(
970 sfx_trigger_item,
971 pos.map(|e| e + 0.5),
972 Some(3.0),
973 player_pos,
974 );
975 },
976 Outcome::HeadLost { uid, .. } => {
977 let positions = client.state().ecs().read_storage::<common::comp::Pos>();
978 if let Some(pos) = client
979 .state()
980 .ecs()
981 .read_resource::<common::uid::IdMaps>()
982 .uid_entity(*uid)
983 .and_then(|entity| positions.get(entity))
984 {
985 let sfx_trigger_item = triggers.0.get_key_value(&SfxEvent::Death);
986 audio.emit_sfx(sfx_trigger_item, pos.0, Some(2.0), player_pos);
987 } else {
988 error!("Couldn't get position of entity that lost head");
989 }
990 },
991 Outcome::Splash { vel, pos, mass, .. } => {
992 let magnitude = (-vel.z).max(0.0);
993 let energy = mass * magnitude;
994
995 if energy > 0.0 {
996 let (sfx, volume) = if energy < 10.0 {
997 (SfxEvent::SplashSmall, energy / 20.0)
998 } else if energy < 100.0 {
999 (SfxEvent::SplashMedium, (energy - 10.0) / 90.0 + 0.5)
1000 } else {
1001 (SfxEvent::SplashBig, (energy / 100.0).sqrt() + 0.5)
1002 };
1003 let sfx_trigger_item = triggers.0.get_key_value(&sfx);
1004 audio.emit_sfx(sfx_trigger_item, *pos, Some(volume.min(2.0)), player_pos);
1005 }
1006 },
1007 Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => {},
1008 _ => {},
1009 }
1010 }
1011
1012 fn load_sfx_items() -> AssetHandle<SfxTriggers> {
1013 SfxTriggers::load_or_insert_with("voxygen.audio.sfx", |error| {
1014 warn!(
1015 "Error reading sfx config file, sfx will not be available: {:#?}",
1016 error
1017 );
1018
1019 SfxTriggers::default()
1020 })
1021 }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026 use super::*;
1027
1028 #[test]
1029 fn test_load_sfx_triggers() { let _ = SfxTriggers::load_expect("voxygen.audio.sfx"); }
1030}