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