1mod event_mapper;
77
78use specs::WorldExt;
79
80use crate::{
81 audio::AudioFrontend,
82 scene::{Camera, Terrain},
83};
84
85use client::Client;
86use common::{
87 DamageSource,
88 assets::{self, AssetExt, AssetHandle},
89 comp::{
90 Body, CharacterAbilityType, Health, InventoryUpdateEvent, UtteranceKind, beam, biped_large,
91 biped_small, bird_large, bird_medium, crustacean, humanoid,
92 item::{AbilitySpec, ItemDefinitionId, ItemDesc, ItemKind, ToolKind, item_key::ItemKey},
93 object,
94 poise::PoiseState,
95 quadruped_low, quadruped_medium, quadruped_small,
96 },
97 outcome::Outcome,
98 terrain::{BlockKind, SpriteKind, TerrainChunk},
99 uid::Uid,
100 vol::ReadVol,
101};
102use common_state::State;
103use event_mapper::SfxEventMapper;
104use hashbrown::HashMap;
105use rand::prelude::*;
106use serde::Deserialize;
107use tracing::{debug, error, warn};
108use vek::*;
109
110const SFX_DIST_LIMIT_SQR: f32 = 20000.0;
116
117#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
118pub enum SfxEvent {
119 Campfire,
120 Embers,
121 Birdcall,
122 Owl,
123 Cricket1,
124 Cricket2,
125 Cricket3,
126 Frog,
127 Bees,
128 RunningWaterSlow,
129 RunningWaterFast,
130 Lavapool,
131 Idle,
132 Swim,
133 SplashSmall,
134 SplashMedium,
135 SplashBig,
136 Run(BlockKind),
137 QuadRun(BlockKind),
138 Roll,
139 RollCancel,
140 Sneak,
141 Climb,
142 GliderOpen,
143 Glide,
144 GliderClose,
145 CatchAir,
146 Jump,
147 Fall,
148 Attack(CharacterAbilityType, ToolKind),
149 Wield(ToolKind),
150 Unwield(ToolKind),
151 Inventory(SfxInventoryEvent),
152 Explosion,
153 Damage,
154 Death,
155 Parry,
156 Block,
157 BreakBlock,
158 PickaxeDamage,
159 PickaxeDamageStrong,
160 PickaxeBreakBlock,
161 SceptreBeam,
162 SkillPointGain,
163 ArrowHit,
164 ArrowMiss,
165 ArrowShot,
166 FireShot,
167 FlameThrower,
168 PoiseChange(PoiseState),
169 GroundSlam,
170 FlashFreeze,
171 GigaRoar,
172 IceSpikes,
173 IceCrack,
174 Utterance(UtteranceKind, VoiceKind),
175 Lightning,
176 CyclopsCharge,
177 TerracottaStatueCharge,
178 LaserBeam,
179 Steam,
180 FuseCharge,
181 Music(ToolKind, AbilitySpec),
182 Yeet,
183 Hiss,
184 LongHiss,
185 Klonk,
186 SmashKlonk,
187 FireShockwave,
188 DeepLaugh,
189 Whoosh,
190 Swoosh,
191 GroundDig,
192 PortalActivated,
193 TeleportedByPortal,
194 FromTheAshes,
195 SurpriseEgg,
196 Transformation,
197 Bleep,
198 Charge,
199 StrigoiHead,
200 BloodmoonHeiressSummon,
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
390#[derive(Deserialize, Default)]
391pub struct SfxTriggers(HashMap<SfxEvent, SfxTriggerItem>);
392
393impl SfxTriggers {
394 pub fn get_trigger(&self, trigger: &SfxEvent) -> Option<&SfxTriggerItem> { self.0.get(trigger) }
395
396 pub fn get_key_value(&self, trigger: &SfxEvent) -> Option<(&SfxEvent, &SfxTriggerItem)> {
397 self.0.get_key_value(trigger)
398 }
399}
400
401pub struct SfxMgr {
402 pub triggers: AssetHandle<SfxTriggers>,
405 event_mapper: SfxEventMapper,
406}
407
408impl Default for SfxMgr {
409 fn default() -> Self {
410 Self {
411 triggers: Self::load_sfx_items(),
412 event_mapper: SfxEventMapper::new(),
413 }
414 }
415}
416
417impl SfxMgr {
418 pub fn maintain(
419 &mut self,
420 audio: &mut AudioFrontend,
421 state: &State,
422 player_entity: specs::Entity,
423 camera: &Camera,
424 terrain: &Terrain<TerrainChunk>,
425 client: &Client,
426 ) {
427 if !audio.sfx_enabled() && !audio.subtitles_enabled {
430 return;
431 }
432
433 let cam_pos = camera.get_pos_with_focus();
434
435 audio.set_listener_pos(cam_pos, camera.dependents().cam_dir);
438
439 let triggers = self.triggers.read();
440
441 let underwater = state
442 .terrain()
443 .get(cam_pos.map(|e| e.floor() as i32))
444 .map(|b| b.is_liquid())
445 .unwrap_or(false);
446
447 if underwater {
448 audio.set_sfx_master_filter(888);
449 } else {
450 audio.set_sfx_master_filter(20000);
451 }
452
453 self.event_mapper.maintain(
454 audio,
455 state,
456 player_entity,
457 camera,
458 &triggers,
459 terrain,
460 client,
461 );
462 }
463
464 #[expect(clippy::single_match)]
465 pub fn handle_outcome(
466 &mut self,
467 outcome: &Outcome,
468 audio: &mut AudioFrontend,
469 client: &Client,
470 ) {
471 if !audio.sfx_enabled() && !audio.subtitles_enabled {
472 return;
473 }
474 let triggers = self.triggers.read();
475 let uids = client.state().ecs().read_storage::<Uid>();
476 if audio.get_listener().is_none() {
477 return;
478 }
479 match outcome {
480 Outcome::Explosion { pos, power, .. } => {
481 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Explosion);
482 audio.emit_sfx(sfx_trigger_item, *pos, Some((power.abs() / 2.5).min(1.5)));
483 },
484 Outcome::Lightning { pos } => {
485 let distance = pos.distance(audio.get_listener_pos());
486 let power = (1.0 - distance / 6_000.0).max(0.0).powi(7);
487 if power > 0.0 {
488 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Lightning);
489 let volume = (power * 3.0).min(2.9);
490 audio.play_ambience_oneshot(
492 super::channel::AmbienceChannelTag::Thunder,
493 sfx_trigger_item,
494 Some(volume),
495 Some(distance / 340.0),
496 );
497 }
498 },
499 Outcome::GroundSlam { pos, .. } | Outcome::ClayGolemDash { pos, .. } => {
500 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GroundSlam);
501 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
502 },
503 Outcome::SurpriseEgg { pos, .. } => {
504 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SurpriseEgg);
505 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
506 },
507 Outcome::Transformation { pos, .. } => {
508 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Transformation);
510 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
511 },
512 Outcome::LaserBeam { pos, .. } => {
513 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::LaserBeam);
514 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
515 },
516 Outcome::CyclopsCharge { pos, .. } => {
517 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
518 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
519 },
520 Outcome::FlamethrowerCharge { pos, .. }
521 | Outcome::TerracottaStatueCharge { pos, .. } => {
522 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
523 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
524 },
525 Outcome::FuseCharge { pos, .. } => {
526 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FuseCharge);
527 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
528 },
529 Outcome::Charge { pos, .. } => {
530 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
531 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
532 },
533 Outcome::FlashFreeze { pos, .. } => {
534 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlashFreeze);
535 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
536 },
537 Outcome::SummonedCreature { pos, body, .. } => {
538 match body {
539 Body::BipedSmall(body) => match body.species {
540 biped_small::Species::IronDwarf => {
541 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Bleep);
542 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
543 },
544 biped_small::Species::Boreal => {
545 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GigaRoar);
546 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
547 },
548 biped_small::Species::ShamanicSpirit | biped_small::Species::Jiangshi => {
549 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
550 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
551 },
552 _ => {},
553 },
554 Body::BipedLarge(body) => match body.species {
555 biped_large::Species::TerracottaBesieger
556 | biped_large::Species::TerracottaPursuer => {
557 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
558 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
559 },
560 _ => {},
561 },
562 Body::BirdMedium(body) => match body.species {
563 bird_medium::Species::Bat => {
564 let sfx_trigger_item =
565 triggers.get_key_value(&SfxEvent::BloodmoonHeiressSummon);
566 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
567 },
568 _ => {},
569 },
570 Body::Crustacean(body) => match body.species {
571 crustacean::Species::SoldierCrab => {
572 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Hiss);
573 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
574 },
575 _ => {},
576 },
577 Body::Object(object::Body::Lavathrower) => {
578 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::DeepLaugh);
579 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
580 },
581 Body::Object(object::Body::SeaLantern) => {
582 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::LongHiss);
583 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
584 },
585 Body::Object(object::Body::Tornado)
586 | Body::Object(object::Body::FieryTornado) => {
587 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Swoosh);
588 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
589 },
590 _ => { },
592 }
593 },
594 Outcome::GroundDig { pos, .. } => {
595 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GroundDig);
596 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
597 },
598 Outcome::PortalActivated { pos, .. } => {
599 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::PortalActivated);
600 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
601 },
602 Outcome::TeleportedByPortal { pos, .. } => {
603 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::TeleportedByPortal);
604 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
605 },
606 Outcome::IceSpikes { pos, .. } => {
607 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::IceSpikes);
608 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
609 },
610 Outcome::IceCrack { pos, .. } => {
611 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::IceCrack);
612 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
613 },
614 Outcome::Steam { pos, .. } => {
615 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Steam);
616 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
617 },
618 Outcome::FireShockwave { pos, .. } => {
619 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlameThrower);
620 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
621 },
622 Outcome::FromTheAshes { pos, .. } => {
623 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FromTheAshes);
624 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
625 },
626 Outcome::ProjectileShot { pos, body, .. } => {
627 match body {
628 Body::Object(
629 object::Body::Arrow
630 | object::Body::MultiArrow
631 | object::Body::ArrowSnake
632 | object::Body::ArrowTurret
633 | object::Body::ArrowClay
634 | object::Body::BoltBesieger
635 | object::Body::HarlequinDagger
636 | object::Body::SpectralSwordSmall
637 | object::Body::SpectralSwordLarge,
638 ) => {
639 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowShot);
640 audio.emit_sfx(sfx_trigger_item, *pos, None);
641 },
642 Body::Object(
643 object::Body::BoltFire
644 | object::Body::BoltFireBig
645 | object::Body::BoltNature
646 | object::Body::BoltIcicle
647 | object::Body::SpearIcicle
648 | object::Body::GrenadeClay
649 | object::Body::SpitPoison,
650 ) => {
651 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FireShot);
652 audio.emit_sfx(sfx_trigger_item, *pos, None);
653 },
654 Body::Object(
655 object::Body::IronPikeBomb
656 | object::Body::BubbleBomb
657 | object::Body::MinotaurAxe
658 | object::Body::Pebble,
659 ) => {
660 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Whoosh);
661 audio.emit_sfx(sfx_trigger_item, *pos, None);
662 },
663 Body::Object(
664 object::Body::LaserBeam
665 | object::Body::LaserBeamSmall
666 | object::Body::LightningBolt,
667 ) => {
668 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::LaserBeam);
669 audio.emit_sfx(sfx_trigger_item, *pos, None);
670 },
671 Body::Object(
672 object::Body::AdletTrap | object::Body::BorealTrap | object::Body::Mine,
673 ) => {
674 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Yeet);
675 audio.emit_sfx(sfx_trigger_item, *pos, None);
676 },
677 Body::Object(object::Body::StrigoiHead) => {
678 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::StrigoiHead);
679 audio.emit_sfx(sfx_trigger_item, *pos, None);
680 },
681 _ => {
682 },
684 }
685 },
686 Outcome::ProjectileHit {
687 pos,
688 body,
689 source,
690 target,
691 ..
692 } => match body {
693 Body::Object(
694 object::Body::Arrow
695 | object::Body::MultiArrow
696 | object::Body::ArrowSnake
697 | object::Body::ArrowTurret
698 | object::Body::ArrowClay
699 | object::Body::BoltBesieger
700 | object::Body::HarlequinDagger
701 | object::Body::SpectralSwordSmall
702 | object::Body::SpectralSwordLarge
703 | object::Body::Pebble,
704 ) => {
705 if target.is_none() {
706 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowMiss);
707 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
708 } else if *source == client.uid() {
709 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit);
710 audio.emit_sfx(
711 sfx_trigger_item,
712 client.position().unwrap_or(*pos),
713 Some(2.0),
714 );
715 } else {
716 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit);
717 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
718 }
719 },
720 Body::Object(
721 object::Body::AdletTrap
722 | object::Body::BorealTrap
723 | object::Body::Mine
724 | object::Body::StrigoiHead,
725 ) => {
726 if target.is_none() {
727 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
728 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
729 } else if *source == client.uid() {
730 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
731 audio.emit_sfx(
732 sfx_trigger_item,
733 client.position().unwrap_or(*pos),
734 Some(2.0),
735 );
736 } else {
737 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
738 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
739 }
740 },
741 _ => {},
742 },
743 Outcome::SkillPointGain { uid, .. } => {
744 if let Some(client_uid) = uids.get(client.entity()) {
745 if uid == client_uid {
746 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SkillPointGain);
747 audio.emit_ui_sfx(sfx_trigger_item, Some(0.4));
748 }
749 }
750 },
751 Outcome::Beam { pos, specifier } => match specifier {
752 beam::FrontendSpecifier::LifestealBeam
753 | beam::FrontendSpecifier::Steam
754 | beam::FrontendSpecifier::Poison
755 | beam::FrontendSpecifier::Ink
756 | beam::FrontendSpecifier::Lightning
757 | beam::FrontendSpecifier::Frost
758 | beam::FrontendSpecifier::Bubbles => {
759 if thread_rng().gen_bool(0.5) {
760 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SceptreBeam);
761 audio.emit_sfx(sfx_trigger_item, *pos, None);
762 };
763 },
764 beam::FrontendSpecifier::Flamethrower
765 | beam::FrontendSpecifier::Cultist
766 | beam::FrontendSpecifier::PhoenixLaser => {
767 if thread_rng().gen_bool(0.5) {
768 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlameThrower);
769 audio.emit_sfx(sfx_trigger_item, *pos, None);
770 }
771 },
772 beam::FrontendSpecifier::Gravewarden | beam::FrontendSpecifier::WebStrand => {},
773 },
774 Outcome::SpriteUnlocked { pos } => {
775 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderOpen);
777 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
778 },
779 Outcome::FailedSpriteUnlock { pos } => {
780 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::BreakBlock);
782 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
783 },
784 Outcome::BreakBlock { pos, tool, .. } => {
785 let sfx_trigger_item =
786 triggers.get_key_value(&if matches!(tool, Some(ToolKind::Pick)) {
787 SfxEvent::PickaxeBreakBlock
788 } else {
789 SfxEvent::BreakBlock
790 });
791 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(3.0));
792 },
793 Outcome::DamagedBlock {
794 pos,
795 stage_changed,
796 tool,
797 ..
798 } => {
799 let sfx_trigger_item = triggers.get_key_value(&match (stage_changed, tool) {
800 (false, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamage,
801 (true, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamageStrong,
802 (_, Some(ToolKind::Shovel)) => return,
804 (_, _) => SfxEvent::BreakBlock,
805 });
806
807 audio.emit_sfx(
808 sfx_trigger_item,
809 pos.map(|e| e as f32 + 0.5),
810 Some(if *stage_changed { 3.0 } else { 2.0 }),
811 );
812 },
813 Outcome::HealthChange { pos, info, .. } => {
814 if info.amount < Health::HEALTH_EPSILON
816 && !matches!(info.cause, Some(DamageSource::Buff(_)))
817 {
818 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Damage);
819 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
820 }
821 },
822 Outcome::Death { pos, .. } => {
823 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Death);
824 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
825 },
826 Outcome::Block { pos, parry, .. } => {
827 if *parry {
828 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Parry);
829 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
830 } else {
831 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Block);
832 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
833 }
834 },
835 Outcome::PoiseChange { pos, state, .. } => match state {
836 PoiseState::Normal => {},
837 PoiseState::Interrupted => {
838 let sfx_trigger_item =
839 triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Interrupted));
840 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
841 },
842 PoiseState::Stunned => {
843 let sfx_trigger_item =
844 triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Stunned));
845 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
846 },
847 PoiseState::Dazed => {
848 let sfx_trigger_item =
849 triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Dazed));
850 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
851 },
852 PoiseState::KnockedDown => {
853 let sfx_trigger_item =
854 triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::KnockedDown));
855 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
856 },
857 },
858 Outcome::Utterance { pos, kind, body } => {
859 if let Some(voice) = body_to_voice(body) {
860 let sfx_trigger_item =
861 triggers.get_key_value(&SfxEvent::Utterance(*kind, voice));
862 if let Some(sfx_trigger_item) = sfx_trigger_item {
863 audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5));
864 } else {
865 debug!(
866 "No utterance sound effect exists for ({:?}, {:?})",
867 kind, voice
868 );
869 }
870 }
871 },
872 Outcome::Glider { pos, wielded } => {
873 if *wielded {
874 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderOpen);
875 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
876 } else {
877 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderClose);
878 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
879 }
880 },
881 Outcome::SpriteDelete { pos, sprite } => {
882 match sprite {
883 SpriteKind::SeaUrchin => {
884 let pos = pos.map(|e| e as f32 + 0.5);
885 let power = (0.6 - pos.distance(audio.get_listener_pos()) / 5_000.0)
886 .max(0.0)
887 .powi(7);
888 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Explosion);
889 audio.emit_sfx(sfx_trigger_item, pos, Some((power.abs() / 2.5).min(0.3)));
890 },
891 _ => {},
892 };
893 },
894 Outcome::Whoosh { pos, .. } => {
895 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Whoosh);
896 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
897 },
898 Outcome::Swoosh { pos, .. } => {
899 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Swoosh);
900 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
901 },
902 Outcome::Slash { pos, .. } => {
903 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
904 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
905 },
906 Outcome::Bleep { pos, .. } => {
907 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Bleep);
908 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
909 },
910 Outcome::HeadLost { uid, .. } => {
911 let positions = client.state().ecs().read_storage::<common::comp::Pos>();
912 if let Some(pos) = client
913 .state()
914 .ecs()
915 .read_resource::<common::uid::IdMaps>()
916 .uid_entity(*uid)
917 .and_then(|entity| positions.get(entity))
918 {
919 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Death);
920 audio.emit_sfx(sfx_trigger_item, pos.0, Some(2.0));
921 } else {
922 error!("Couldn't get position of entity that lost head");
923 }
924 },
925 Outcome::Splash { vel, pos, mass, .. } => {
926 let magnitude = (-vel.z).max(0.0);
927 let energy = mass * magnitude;
928
929 if energy > 0.0 {
930 let (sfx, volume) = if energy < 10.0 {
931 (SfxEvent::SplashSmall, energy / 20.0)
932 } else if energy < 100.0 {
933 (SfxEvent::SplashMedium, (energy - 10.0) / 90.0 + 0.5)
934 } else {
935 (SfxEvent::SplashBig, (energy / 100.0).sqrt() + 0.5)
936 };
937 let sfx_trigger_item = triggers.get_key_value(&sfx);
938 audio.emit_sfx(sfx_trigger_item, *pos, Some(volume.min(2.0)));
939 }
940 },
941 Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => {},
942 }
943 }
944
945 fn load_sfx_items() -> AssetHandle<SfxTriggers> {
946 SfxTriggers::load_or_insert_with("voxygen.audio.sfx", |error| {
947 warn!(
948 "Error reading sfx config file, sfx will not be available: {:#?}",
949 error
950 );
951
952 SfxTriggers::default()
953 })
954 }
955}
956
957impl assets::Asset for SfxTriggers {
958 type Loader = assets::RonLoader;
959
960 const EXTENSION: &'static str = "ron";
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn test_load_sfx_triggers() { let _ = SfxTriggers::load_expect("voxygen.audio.sfx"); }
969}