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 Klonk,
185 SmashKlonk,
186 FireShockwave,
187 DeepLaugh,
188 Whoosh,
189 Swoosh,
190 GroundDig,
191 PortalActivated,
192 TeleportedByPortal,
193 FromTheAshes,
194 SurpriseEgg,
195 Bleep,
196 Charge,
197 StrigoiHead,
198 BloodmoonHeiressSummon,
199}
200
201#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
202pub enum VoiceKind {
203 HumanFemale,
204 HumanMale,
205 BipedLarge,
206 Wendigo,
207 Reptile,
208 Bird,
209 Critter,
210 Sheep,
211 Pig,
212 Cow,
213 Canine,
214 Dagon,
215 Lion,
216 Mindflayer,
217 Marlin,
218 Maneater,
219 Adlet,
220 Antelope,
221 Alligator,
222 SeaCrocodile,
223 Saurok,
224 Cat,
225 Goat,
226 Mandragora,
227 Asp,
228 Fungome,
229 Truffler,
230 Wolf,
231 Wyvern,
232 Phoenix,
233 VampireBat,
234 Legoom,
235}
236
237fn body_to_voice(body: &Body) -> Option<VoiceKind> {
238 Some(match body {
239 Body::Humanoid(body) => match &body.body_type {
240 humanoid::BodyType::Female => VoiceKind::HumanFemale,
241 humanoid::BodyType::Male => VoiceKind::HumanMale,
242 },
243 Body::QuadrupedLow(body) => match body.species {
244 quadruped_low::Species::Maneater => VoiceKind::Maneater,
245 quadruped_low::Species::Alligator | quadruped_low::Species::Snaretongue => {
246 VoiceKind::Alligator
247 },
248 quadruped_low::Species::SeaCrocodile => VoiceKind::SeaCrocodile,
249 quadruped_low::Species::Dagon => VoiceKind::Dagon,
250 quadruped_low::Species::Asp => VoiceKind::Asp,
251 _ => return None,
252 },
253 Body::QuadrupedSmall(body) => match body.species {
254 quadruped_small::Species::Truffler => VoiceKind::Truffler,
255 quadruped_small::Species::Fungome => VoiceKind::Fungome,
256 quadruped_small::Species::Sheep => VoiceKind::Sheep,
257 quadruped_small::Species::Pig | quadruped_small::Species::Boar => VoiceKind::Pig,
258 quadruped_small::Species::Cat => VoiceKind::Cat,
259 quadruped_small::Species::Goat => VoiceKind::Goat,
260 _ => VoiceKind::Critter,
261 },
262 Body::QuadrupedMedium(body) => match body.species {
263 quadruped_medium::Species::Saber
264 | quadruped_medium::Species::Tiger
265 | quadruped_medium::Species::Lion
266 | quadruped_medium::Species::Frostfang
267 | quadruped_medium::Species::Snowleopard => VoiceKind::Lion,
268 quadruped_medium::Species::Wolf => VoiceKind::Wolf,
269 quadruped_medium::Species::Roshwalr
270 | quadruped_medium::Species::Tarasque
271 | quadruped_medium::Species::Darkhound
272 | quadruped_medium::Species::Bonerattler
273 | quadruped_medium::Species::Grolgar => VoiceKind::Canine,
274 quadruped_medium::Species::Cattle
275 | quadruped_medium::Species::Catoblepas
276 | quadruped_medium::Species::Highland
277 | quadruped_medium::Species::Yak
278 | quadruped_medium::Species::Moose
279 | quadruped_medium::Species::Dreadhorn => VoiceKind::Cow,
280 quadruped_medium::Species::Antelope => VoiceKind::Antelope,
281 _ => return None,
282 },
283 Body::BirdMedium(body) => match body.species {
284 bird_medium::Species::BloodmoonBat | bird_medium::Species::VampireBat => {
285 VoiceKind::VampireBat
286 },
287 _ => VoiceKind::Bird,
288 },
289 Body::BirdLarge(body) => match body.species {
290 bird_large::Species::CloudWyvern
291 | bird_large::Species::FlameWyvern
292 | bird_large::Species::FrostWyvern
293 | bird_large::Species::SeaWyvern
294 | bird_large::Species::WealdWyvern => VoiceKind::Wyvern,
295 bird_large::Species::Phoenix => VoiceKind::Phoenix,
296 _ => VoiceKind::Bird,
297 },
298 Body::BipedSmall(body) => match body.species {
299 biped_small::Species::Adlet => VoiceKind::Adlet,
300 biped_small::Species::Mandragora => VoiceKind::Mandragora,
301 biped_small::Species::Flamekeeper => VoiceKind::BipedLarge,
302 biped_small::Species::GreenLegoom
303 | biped_small::Species::OchreLegoom
304 | biped_small::Species::PurpleLegoom
305 | biped_small::Species::RedLegoom => VoiceKind::Legoom,
306 _ => return None,
307 },
308 Body::BipedLarge(body) => match body.species {
309 biped_large::Species::Wendigo => VoiceKind::Wendigo,
310 biped_large::Species::Occultsaurok
311 | biped_large::Species::Mightysaurok
312 | biped_large::Species::Slysaurok => VoiceKind::Saurok,
313 biped_large::Species::Mindflayer => VoiceKind::Mindflayer,
314 _ => VoiceKind::BipedLarge,
315 },
316 Body::Theropod(_) | Body::Dragon(_) => VoiceKind::Reptile,
317 Body::FishSmall(_) | Body::FishMedium(_) => VoiceKind::Marlin,
318 _ => return None,
319 })
320}
321
322#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
323pub enum SfxInventoryEvent {
324 Collected,
325 CollectedTool(ToolKind),
326 CollectedItem(String),
327 CollectFailed,
328 Consumed(ItemKey),
329 Debug,
330 Dropped,
331 Given,
332 Swapped,
333 Craft,
334}
335
336impl From<&InventoryUpdateEvent> for SfxEvent {
338 fn from(value: &InventoryUpdateEvent) -> Self {
339 match value {
340 InventoryUpdateEvent::Collected(item) => {
341 match &*item.kind() {
344 ItemKind::Tool(tool) => {
345 SfxEvent::Inventory(SfxInventoryEvent::CollectedTool(tool.kind))
346 },
347 ItemKind::Ingredient { .. }
348 if matches!(
349 item.item_definition_id(),
350 ItemDefinitionId::Simple(id) if id.contains("mineral.gem.")
351 ) =>
352 {
353 SfxEvent::Inventory(SfxInventoryEvent::CollectedItem(String::from(
354 "Gemstone",
355 )))
356 },
357 _ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
358 }
359 },
360 InventoryUpdateEvent::BlockCollectFailed { .. }
361 | InventoryUpdateEvent::EntityCollectFailed { .. } => {
362 SfxEvent::Inventory(SfxInventoryEvent::CollectFailed)
363 },
364 InventoryUpdateEvent::Consumed(consumable) => {
365 SfxEvent::Inventory(SfxInventoryEvent::Consumed(consumable.clone()))
366 },
367 InventoryUpdateEvent::Debug => SfxEvent::Inventory(SfxInventoryEvent::Debug),
368 InventoryUpdateEvent::Dropped => SfxEvent::Inventory(SfxInventoryEvent::Dropped),
369 InventoryUpdateEvent::Given => SfxEvent::Inventory(SfxInventoryEvent::Given),
370 InventoryUpdateEvent::Swapped => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
371 InventoryUpdateEvent::Craft => SfxEvent::Inventory(SfxInventoryEvent::Craft),
372 _ => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
373 }
374 }
375}
376
377#[derive(Deserialize)]
378pub struct SfxTriggerItem {
379 pub files: Vec<String>,
381 pub threshold: f32,
383
384 #[serde(default)]
385 pub subtitle: Option<String>,
386}
387
388#[derive(Deserialize, Default)]
389pub struct SfxTriggers(HashMap<SfxEvent, SfxTriggerItem>);
390
391impl SfxTriggers {
392 pub fn get_trigger(&self, trigger: &SfxEvent) -> Option<&SfxTriggerItem> { self.0.get(trigger) }
393
394 pub fn get_key_value(&self, trigger: &SfxEvent) -> Option<(&SfxEvent, &SfxTriggerItem)> {
395 self.0.get_key_value(trigger)
396 }
397}
398
399pub struct SfxMgr {
400 pub triggers: AssetHandle<SfxTriggers>,
403 event_mapper: SfxEventMapper,
404}
405
406impl Default for SfxMgr {
407 fn default() -> Self {
408 Self {
409 triggers: Self::load_sfx_items(),
410 event_mapper: SfxEventMapper::new(),
411 }
412 }
413}
414
415impl SfxMgr {
416 pub fn maintain(
417 &mut self,
418 audio: &mut AudioFrontend,
419 state: &State,
420 player_entity: specs::Entity,
421 camera: &Camera,
422 terrain: &Terrain<TerrainChunk>,
423 client: &Client,
424 ) {
425 if !audio.sfx_enabled() && !audio.subtitles_enabled {
428 return;
429 }
430
431 let cam_pos = camera.get_pos_with_focus();
432
433 audio.set_listener_pos(cam_pos, camera.dependents().cam_dir);
436
437 let triggers = self.triggers.read();
438
439 let underwater = state
440 .terrain()
441 .get(cam_pos.map(|e| e.floor() as i32))
442 .map(|b| b.is_liquid())
443 .unwrap_or(false);
444
445 if underwater {
446 audio.set_sfx_master_filter(888);
447 } else {
448 audio.set_sfx_master_filter(20000);
449 }
450
451 self.event_mapper.maintain(
452 audio,
453 state,
454 player_entity,
455 camera,
456 &triggers,
457 terrain,
458 client,
459 );
460 }
461
462 #[expect(clippy::single_match)]
463 pub fn handle_outcome(
464 &mut self,
465 outcome: &Outcome,
466 audio: &mut AudioFrontend,
467 client: &Client,
468 ) {
469 if !audio.sfx_enabled() && !audio.subtitles_enabled {
470 return;
471 }
472 let triggers = self.triggers.read();
473 let uids = client.state().ecs().read_storage::<Uid>();
474 if audio.get_listener().is_none() {
475 return;
476 }
477 match outcome {
478 Outcome::Explosion { pos, power, .. } => {
479 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Explosion);
480 audio.emit_sfx(sfx_trigger_item, *pos, Some((power.abs() / 2.5).min(1.5)));
481 },
482 Outcome::Lightning { pos } => {
483 let distance = pos.distance(audio.get_listener_pos());
484 let power = (1.0 - distance / 6_000.0).max(0.0).powi(7);
485 if power > 0.0 {
486 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Lightning);
487 let volume = (power * 3.0).min(2.9);
488 audio.play_ambience_oneshot(
490 super::channel::AmbienceChannelTag::Thunder,
491 sfx_trigger_item,
492 Some(volume),
493 Some(distance / 340.0),
494 );
495 }
496 },
497 Outcome::GroundSlam { pos, .. } | Outcome::ClayGolemDash { pos, .. } => {
498 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GroundSlam);
499 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
500 },
501 Outcome::SurpriseEgg { pos, .. } => {
502 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SurpriseEgg);
503 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
504 },
505 Outcome::LaserBeam { pos, .. } => {
506 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::LaserBeam);
507 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
508 },
509 Outcome::CyclopsCharge { pos, .. } => {
510 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
511 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
512 },
513 Outcome::FlamethrowerCharge { pos, .. }
514 | Outcome::TerracottaStatueCharge { pos, .. } => {
515 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
516 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
517 },
518 Outcome::FuseCharge { pos, .. } => {
519 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FuseCharge);
520 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
521 },
522 Outcome::Charge { pos, .. } => {
523 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
524 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
525 },
526 Outcome::FlashFreeze { pos, .. } => {
527 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlashFreeze);
528 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
529 },
530 Outcome::SummonedCreature { pos, body, .. } => {
531 match body {
532 Body::BipedSmall(body) => match body.species {
533 biped_small::Species::IronDwarf => {
534 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Bleep);
535 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
536 },
537 biped_small::Species::Boreal => {
538 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GigaRoar);
539 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
540 },
541 biped_small::Species::ShamanicSpirit | biped_small::Species::Jiangshi => {
542 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
543 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
544 },
545 _ => {},
546 },
547 Body::BipedLarge(body) => match body.species {
548 biped_large::Species::TerracottaBesieger
549 | biped_large::Species::TerracottaPursuer => {
550 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
551 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
552 },
553 _ => {},
554 },
555 Body::BirdMedium(body) => match body.species {
556 bird_medium::Species::Bat => {
557 let sfx_trigger_item =
558 triggers.get_key_value(&SfxEvent::BloodmoonHeiressSummon);
559 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
560 },
561 _ => {},
562 },
563 Body::Crustacean(body) => match body.species {
564 crustacean::Species::SoldierCrab => {
565 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Hiss);
566 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
567 },
568 _ => {},
569 },
570 Body::Object(object::Body::Lavathrower) => {
571 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::DeepLaugh);
572 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
573 },
574 Body::Object(object::Body::Tornado)
575 | Body::Object(object::Body::FieryTornado) => {
576 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Swoosh);
577 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
578 },
579 _ => { },
581 }
582 },
583 Outcome::GroundDig { pos, .. } => {
584 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GroundDig);
585 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
586 },
587 Outcome::PortalActivated { pos, .. } => {
588 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::PortalActivated);
589 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
590 },
591 Outcome::TeleportedByPortal { pos, .. } => {
592 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::TeleportedByPortal);
593 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
594 },
595 Outcome::IceSpikes { pos, .. } => {
596 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::IceSpikes);
597 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
598 },
599 Outcome::IceCrack { pos, .. } => {
600 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::IceCrack);
601 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
602 },
603 Outcome::Steam { pos, .. } => {
604 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Steam);
605 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
606 },
607 Outcome::FireShockwave { pos, .. } => {
608 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlameThrower);
609 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
610 },
611 Outcome::FromTheAshes { pos, .. } => {
612 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FromTheAshes);
613 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
614 },
615 Outcome::ProjectileShot { pos, body, .. } => {
616 match body {
617 Body::Object(
618 object::Body::Arrow
619 | object::Body::MultiArrow
620 | object::Body::ArrowSnake
621 | object::Body::ArrowTurret
622 | object::Body::ArrowClay
623 | object::Body::BoltBesieger
624 | object::Body::HarlequinDagger
625 | object::Body::SpectralSwordSmall
626 | object::Body::SpectralSwordLarge,
627 ) => {
628 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowShot);
629 audio.emit_sfx(sfx_trigger_item, *pos, None);
630 },
631 Body::Object(
632 object::Body::BoltFire
633 | object::Body::BoltFireBig
634 | object::Body::BoltNature
635 | object::Body::BoltIcicle
636 | object::Body::SpearIcicle
637 | object::Body::GrenadeClay
638 | object::Body::SpitPoison,
639 ) => {
640 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FireShot);
641 audio.emit_sfx(sfx_trigger_item, *pos, None);
642 },
643 Body::Object(
644 object::Body::IronPikeBomb
645 | object::Body::BubbleBomb
646 | object::Body::MinotaurAxe
647 | object::Body::Pebble,
648 ) => {
649 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Whoosh);
650 audio.emit_sfx(sfx_trigger_item, *pos, None);
651 },
652 Body::Object(
653 object::Body::LaserBeam
654 | object::Body::LaserBeamSmall
655 | object::Body::LightningBolt,
656 ) => {
657 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::LaserBeam);
658 audio.emit_sfx(sfx_trigger_item, *pos, None);
659 },
660 Body::Object(
661 object::Body::AdletTrap | object::Body::BorealTrap | object::Body::Mine,
662 ) => {
663 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Yeet);
664 audio.emit_sfx(sfx_trigger_item, *pos, None);
665 },
666 Body::Object(object::Body::StrigoiHead) => {
667 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::StrigoiHead);
668 audio.emit_sfx(sfx_trigger_item, *pos, None);
669 },
670 _ => {
671 },
673 }
674 },
675 Outcome::ProjectileHit {
676 pos,
677 body,
678 source,
679 target,
680 ..
681 } => match body {
682 Body::Object(
683 object::Body::Arrow
684 | object::Body::MultiArrow
685 | object::Body::ArrowSnake
686 | object::Body::ArrowTurret
687 | object::Body::ArrowClay
688 | object::Body::BoltBesieger
689 | object::Body::HarlequinDagger
690 | object::Body::SpectralSwordSmall
691 | object::Body::SpectralSwordLarge
692 | object::Body::Pebble,
693 ) => {
694 if target.is_none() {
695 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowMiss);
696 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
697 } else if *source == client.uid() {
698 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit);
699 audio.emit_sfx(
700 sfx_trigger_item,
701 client.position().unwrap_or(*pos),
702 Some(2.0),
703 );
704 } else {
705 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit);
706 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
707 }
708 },
709 Body::Object(
710 object::Body::AdletTrap
711 | object::Body::BorealTrap
712 | object::Body::Mine
713 | object::Body::StrigoiHead,
714 ) => {
715 if target.is_none() {
716 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
717 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
718 } else if *source == client.uid() {
719 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
720 audio.emit_sfx(
721 sfx_trigger_item,
722 client.position().unwrap_or(*pos),
723 Some(2.0),
724 );
725 } else {
726 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
727 audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
728 }
729 },
730 _ => {},
731 },
732 Outcome::SkillPointGain { uid, .. } => {
733 if let Some(client_uid) = uids.get(client.entity()) {
734 if uid == client_uid {
735 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SkillPointGain);
736 audio.emit_ui_sfx(sfx_trigger_item, Some(0.4));
737 }
738 }
739 },
740 Outcome::Beam { pos, specifier } => match specifier {
741 beam::FrontendSpecifier::LifestealBeam
742 | beam::FrontendSpecifier::Steam
743 | beam::FrontendSpecifier::Poison
744 | beam::FrontendSpecifier::Ink
745 | beam::FrontendSpecifier::Lightning
746 | beam::FrontendSpecifier::Frost
747 | beam::FrontendSpecifier::Bubbles => {
748 if thread_rng().gen_bool(0.5) {
749 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SceptreBeam);
750 audio.emit_sfx(sfx_trigger_item, *pos, None);
751 };
752 },
753 beam::FrontendSpecifier::Flamethrower
754 | beam::FrontendSpecifier::Cultist
755 | beam::FrontendSpecifier::PhoenixLaser => {
756 if thread_rng().gen_bool(0.5) {
757 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlameThrower);
758 audio.emit_sfx(sfx_trigger_item, *pos, None);
759 }
760 },
761 beam::FrontendSpecifier::Gravewarden | beam::FrontendSpecifier::WebStrand => {},
762 },
763 Outcome::SpriteUnlocked { pos } => {
764 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderOpen);
766 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
767 },
768 Outcome::FailedSpriteUnlock { pos } => {
769 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::BreakBlock);
771 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
772 },
773 Outcome::BreakBlock { pos, tool, .. } => {
774 let sfx_trigger_item =
775 triggers.get_key_value(&if matches!(tool, Some(ToolKind::Pick)) {
776 SfxEvent::PickaxeBreakBlock
777 } else {
778 SfxEvent::BreakBlock
779 });
780 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(3.0));
781 },
782 Outcome::DamagedBlock {
783 pos,
784 stage_changed,
785 tool,
786 ..
787 } => {
788 let sfx_trigger_item = triggers.get_key_value(&match (stage_changed, tool) {
789 (false, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamage,
790 (true, Some(ToolKind::Pick)) => SfxEvent::PickaxeDamageStrong,
791 (_, Some(ToolKind::Shovel)) => return,
793 (_, _) => SfxEvent::BreakBlock,
794 });
795
796 audio.emit_sfx(
797 sfx_trigger_item,
798 pos.map(|e| e as f32 + 0.5),
799 Some(if *stage_changed { 3.0 } else { 2.0 }),
800 );
801 },
802 Outcome::HealthChange { pos, info, .. } => {
803 if info.amount < Health::HEALTH_EPSILON
805 && !matches!(info.cause, Some(DamageSource::Buff(_)))
806 {
807 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Damage);
808 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
809 }
810 },
811 Outcome::Death { pos, .. } => {
812 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Death);
813 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
814 },
815 Outcome::Block { pos, parry, .. } => {
816 if *parry {
817 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Parry);
818 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
819 } else {
820 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Block);
821 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
822 }
823 },
824 Outcome::PoiseChange { pos, state, .. } => match state {
825 PoiseState::Normal => {},
826 PoiseState::Interrupted => {
827 let sfx_trigger_item =
828 triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Interrupted));
829 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
830 },
831 PoiseState::Stunned => {
832 let sfx_trigger_item =
833 triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Stunned));
834 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
835 },
836 PoiseState::Dazed => {
837 let sfx_trigger_item =
838 triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Dazed));
839 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
840 },
841 PoiseState::KnockedDown => {
842 let sfx_trigger_item =
843 triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::KnockedDown));
844 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
845 },
846 },
847 Outcome::Utterance { pos, kind, body } => {
848 if let Some(voice) = body_to_voice(body) {
849 let sfx_trigger_item =
850 triggers.get_key_value(&SfxEvent::Utterance(*kind, voice));
851 if let Some(sfx_trigger_item) = sfx_trigger_item {
852 audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5));
853 } else {
854 debug!(
855 "No utterance sound effect exists for ({:?}, {:?})",
856 kind, voice
857 );
858 }
859 }
860 },
861 Outcome::Glider { pos, wielded } => {
862 if *wielded {
863 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderOpen);
864 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
865 } else {
866 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderClose);
867 audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
868 }
869 },
870 Outcome::SpriteDelete { pos, sprite } => {
871 match sprite {
872 SpriteKind::SeaUrchin => {
873 let pos = pos.map(|e| e as f32 + 0.5);
874 let power = (0.6 - pos.distance(audio.get_listener_pos()) / 5_000.0)
875 .max(0.0)
876 .powi(7);
877 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Explosion);
878 audio.emit_sfx(sfx_trigger_item, pos, Some((power.abs() / 2.5).min(0.3)));
879 },
880 _ => {},
881 };
882 },
883 Outcome::Whoosh { pos, .. } => {
884 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Whoosh);
885 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
886 },
887 Outcome::Swoosh { pos, .. } => {
888 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Swoosh);
889 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
890 },
891 Outcome::Slash { pos, .. } => {
892 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
893 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
894 },
895 Outcome::Bleep { pos, .. } => {
896 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Bleep);
897 audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
898 },
899 Outcome::HeadLost { uid, .. } => {
900 let positions = client.state().ecs().read_storage::<common::comp::Pos>();
901 if let Some(pos) = client
902 .state()
903 .ecs()
904 .read_resource::<common::uid::IdMaps>()
905 .uid_entity(*uid)
906 .and_then(|entity| positions.get(entity))
907 {
908 let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Death);
909 audio.emit_sfx(sfx_trigger_item, pos.0, Some(2.0));
910 } else {
911 error!("Couldn't get position of entity that lost head");
912 }
913 },
914 Outcome::Splash { vel, pos, mass, .. } => {
915 let magnitude = (-vel.z).max(0.0);
916 let energy = mass * magnitude;
917
918 if energy > 0.0 {
919 let (sfx, volume) = if energy < 10.0 {
920 (SfxEvent::SplashSmall, energy / 20.0)
921 } else if energy < 100.0 {
922 (SfxEvent::SplashMedium, (energy - 10.0) / 90.0 + 0.5)
923 } else {
924 (SfxEvent::SplashBig, (energy / 100.0).sqrt() + 0.5)
925 };
926 let sfx_trigger_item = triggers.get_key_value(&sfx);
927 audio.emit_sfx(sfx_trigger_item, *pos, Some(volume.min(2.0)));
928 }
929 },
930 Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => {},
931 }
932 }
933
934 fn load_sfx_items() -> AssetHandle<SfxTriggers> {
935 SfxTriggers::load_or_insert_with("voxygen.audio.sfx", |error| {
936 warn!(
937 "Error reading sfx config file, sfx will not be available: {:#?}",
938 error
939 );
940
941 SfxTriggers::default()
942 })
943 }
944}
945
946impl assets::Asset for SfxTriggers {
947 type Loader = assets::RonLoader;
948
949 const EXTENSION: &'static str = "ron";
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955
956 #[test]
957 fn test_load_sfx_triggers() { let _ = SfxTriggers::load_expect("voxygen.audio.sfx"); }
958}