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;
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
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        // Update continuing sounds with player position
454        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                    // Delayed based on distance / speed of sound (approxmately 340 m/s)
507                    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                // TODO: Give this a sound
525                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                    _ => { // not mapped to sfx file
607                    },
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                        // not mapped to sfx file
699                    },
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                // TODO: Dedicated sound effect!
802                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                // TODO: Dedicated sound effect!
812                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                    // SFX already emitted by ability
844                    (_, 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                // Ignore positive damage (healing) and buffs for now
857                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}