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