veloren_voxygen/audio/sfx/
mod.rs

1//! Manages individual sfx event system, listens for sfx events, and requests
2//! playback at the requested position and volume
3//!
4//! Veloren's sfx are managed through a configuration which lives in the
5//! codebase under `/assets/voxygen/audio/sfx.ron`.
6//!
7//! If there are errors while reading or deserialising the configuration file, a
8//! warning is logged and sfx will be disabled.
9//!
10//! Each entry in the configuration consists of an
11//! [SfxEvent](../../../veloren_common/event/enum.SfxEvent.html) item, with some
12//! additional information to allow playback:
13//! - `files` - the paths to the `.wav` files to be played for the sfx. minus
14//!   the file extension. This can be a single item if the same sound can be
15//!   played each time, or a list of files from which one is chosen at random to
16//!   be played.
17//! - `threshold` - the time that the system should wait between successive
18//!   plays. This avoids playing the sound with very fast successive repetition
19//!   when the character can maintain a state over a long period, such as
20//!   running or climbing.
21//!
22//! The following snippet details some entries in the configuration and how they
23//! map to the sound files:
24//! ```ignore
25//! Run(Grass): ( // depends on underfoot block
26//!    files: [
27//!        "voxygen.audio.sfx.footsteps.stepgrass_1",
28//!        "voxygen.audio.sfx.footsteps.stepgrass_2",
29//!        "voxygen.audio.sfx.footsteps.stepgrass_3",
30//!        "voxygen.audio.sfx.footsteps.stepgrass_4",
31//!        "voxygen.audio.sfx.footsteps.stepgrass_5",
32//!        "voxygen.audio.sfx.footsteps.stepgrass_6",
33//!    ],
34//!    threshold: 1.6, // travelled distance before next play
35//! ),
36//! Wield(Sword): ( // depends on the player's weapon
37//!    files: [
38//!        "voxygen.audio.sfx.weapon.sword_out",
39//!    ],
40//!    threshold: 0.5, // wait 0.5s between plays
41//! ),
42//! ...
43//! ```
44//!
45//! These items (for example, the `Wield(Sword)` occasionally depend on some
46//! property which varies in game. The
47//! [SfxEvent](../../../veloren_common/event/enum.SfxEvent.html) documentation
48//! provides links to those variables, some examples are provided her for longer
49//! items:
50//!
51//! ```ignore
52//! // An inventory action
53//! Inventory(Dropped): (
54//!     files: [
55//!        "voxygen.audio.sfx.footsteps.stepgrass_4",
56//!    ],
57//!    threshold: 0.5,
58//! ),
59//! // An inventory action which depends upon the item
60//! Inventory(Consumed(Apple)): (
61//!    files: [
62//!        "voxygen.audio.sfx.inventory.consumable.apple",
63//!    ],
64//!    threshold: 0.5
65//! ),
66//! // An attack ability which depends on the weapon
67//! Attack(DashMelee, Sword): (
68//!     files: [
69//!         "voxygen.audio.sfx.weapon.sword_dash_01",
70//!         "voxygen.audio.sfx.weapon.sword_dash_02",
71//!     ],
72//!     threshold: 1.2,
73//! ),
74//! ```
75
76mod 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
110/// We watch the states of nearby entities in order to emit SFX at their
111/// position based on their state. This constant limits the radius that we
112/// observe to prevent tracking distant entities. It approximates the distance
113/// at which the volume of the sfx emitted is too quiet to be meaningful for the
114/// player.
115const 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
336// TODO Move to a separate event mapper?
337impl From<&InventoryUpdateEvent> for SfxEvent {
338    fn from(value: &InventoryUpdateEvent) -> Self {
339        match value {
340            InventoryUpdateEvent::Collected(item) => {
341                // Handle sound effects for types of collected items, falling
342                // back to the default Collected event
343                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    /// A list of SFX filepaths for this event
380    pub files: Vec<String>,
381    /// The time to wait before repeating this SfxEvent
382    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    /// This is an `AssetHandle` so it is reloaded automatically
401    /// when the manifest is edited.
402    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        // Checks if the SFX volume is set to zero or audio is disabled
426        // This prevents us from running all the following code unnecessarily
427        if !audio.sfx_enabled() && !audio.subtitles_enabled {
428            return;
429        }
430
431        let cam_pos = camera.get_pos_with_focus();
432
433        // Sets the listener position to the camera position facing the
434        // same direction as the camera
435        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                    // Delayed based on distance / speed of sound (approxmately 340 m/s)
489                    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                    _ => { // not mapped to sfx file
580                    },
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                        // not mapped to sfx file
672                    },
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                // TODO: Dedicated sound effect!
765                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                // TODO: Dedicated sound effect!
770                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                    // SFX already emitted by ability
792                    (_, 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                // Ignore positive damage (healing) and buffs for now
804                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}