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