Merge branch 'isse/accessability' into 'master'

Accessibility settings tab, and subtitles

See merge request veloren/veloren!3901
This commit is contained in:
Isse 2023-05-03 16:26:22 +00:00
commit 4e3eb3ef44
16 changed files with 1149 additions and 77 deletions

View File

@ -43,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- NPCs will migrate to new towns if they are dissatisfied with their current town - NPCs will migrate to new towns if they are dissatisfied with their current town
- Female humanoids now have a greeting sound effect - Female humanoids now have a greeting sound effect
- Loot that drops multiple items is now distributed fairly between damage contributors. - Loot that drops multiple items is now distributed fairly between damage contributors.
- Added accessibility settings tab.
- Setting to enable subtitles describing sfx.
### Changed ### Changed

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ common-video = Graphics
common-sound = Sound common-sound = Sound
common-chat = Chat common-chat = Chat
common-networking = Networking common-networking = Networking
common-accessibility = Accessibility
common-resume = Resume common-resume = Resume
common-characters = Characters common-characters = Characters
common-close = Close common-close = Close
@ -42,6 +43,7 @@ common-sound_settings = Sound Settings
common-language_settings = Language Settings common-language_settings = Language Settings
common-chat_settings = Chat Settings common-chat_settings = Chat Settings
common-networking_settings = Networking Settings common-networking_settings = Networking Settings
common-accessibility_settings = Accessibility Settings
common-connection_lost = common-connection_lost =
Connection lost! Connection lost!
Did the server restart? Did the server restart?

View File

@ -151,3 +151,4 @@ hud-settings-group_only = Group only
hud-settings-reset_chat = Reset to Defaults hud-settings-reset_chat = Reset to Defaults
hud-settings-third_party_integrations = Third-party Integrations hud-settings-third_party_integrations = Third-party Integrations
hud-settings-enable_discord_integration = Enable Discord Integration hud-settings-enable_discord_integration = Enable Discord Integration
hud-settings-subtitles = Subtitles

View File

@ -0,0 +1,148 @@
subtitle-campfire = Campfire crackling
subtitle-bird_call = Birds singing
subtitle-bees = Bees buzzing
subtitle-owl = Owl hooting
subtitle-running_water = Water bubbling
subtitle-lightning = Thunder
subtitle-footsteps_grass = Walking on grass
subtitle-footsteps_earth = Walking on dirt
subtitle-footsteps_rock = Walking on rock
subtitle-footsteps_snow = Walking on snow
subtitle-pickup_item = Item picked up
subtitle-pickup_failed = Pickup failed
subtitle-glider_open = Glider equipped
subtitle-glider_close = Glider unequipped
subtitle-glide = Gliding
subtitle-roll = Rolling
subtitle-swim = Swimming
subtitle-climb = Climbing
subtitle-damage = Damage
subtitle-death = Death
subtitle-wield_bow = Bow equipped
subtitle-unwield_bow = Bow unequipped
subtitle-pickup_bow = Bow picked up
subtitle-wield_sword = Sword equipped
subtitle-unwield_sword = Sword unequipped
subtitle-sword_attack = Sword swung
subtitle-pickup_sword = Sword picked up
subtitle-wield_axe = Axe equipped
subtitle-unwield_axe = Axe unequipped
subtitle-axe_attack = Axe swung
subtitle-pickup_axe = Axe picked up
subtitle-wield_hammer = Hammer equipped
subtitle-unwield_hammer = Hammer unequipped
subtitle-hammer_attack = Hammer swung
subtitle-pickup_hammer = Hammer picked up
subtitle-wield_staff = Staff equipped
subtitle-unwield_staff = Staff unequipped
subtitle-fire_shot = Staff fired
subtitle-staff_attack = Staff fired
subtitle-pickup_staff = Staff picked up
subtitle-wield_sceptre = Sceptre equipped
subtitle-unwield_sceptre = Sceptre unequipped
subtitle-sceptre_heal = Sceptre heal aura
subtitle-pickup_sceptre = Sceptre picked up
subtitle-wield_dagger = Dagger equipped
subtitle-uwield_dagger = Dagger unequipped
subtitle-dagger_attack = Dagger swung
subtitle-pickup_dagger = Dagger picked up
subtitle-wield_shield = Shield equipped
subtitle-unwield_shield = Shield unequipped
subtitle-shield_attack = Shield pushed
subtitle-pickup_shield = Shield picked up
subtitle-pickup_pick = Pickaxe picked up
subtitle-pickup_gemstone = Gemstone picked up
subtitle-instrument_organ = Organ playing
subtitle-wield_instrument = Instrument equipped
subtitle-unwield_instrument = Instrument unequipped
subtitle-instrument_double_bass = Double Bass playing
subtitle-instrument_flute = Flute playing
subtitle-instrument_glass_flute = Glass Flute playing
subtitle-instrument_lyre = Lyre playing
subtitle-instrument_icy_talharpa = Icy Talharpa playing
subtitle-instrument_kalimba = Kalimba playing
subtitle-instrument_melodica = Melodica playing
subtitle-instrument_lute = Lute playing
subtitle-instrument_sitar = Sitar playing
subtitle-instrument_guitar = Guitar playing
subtitle-instrument_dark_guitar = Dark Guitar playing
subtitle-instrument_washboard = Washboard playing
subtitle-pickup_instrument = Pickup instrument
subtitle-explosion = Explosion
subtitle-arrow_shot = Arrow released
subtitle-arrow_miss = Arrow miss
subtitle-arrow_hit = Arrow hit
subtitle-skill_point = Skill Point gained
subtitle-sceptre_beam = Sceptre beam
subtitle-flame_thrower = Flame thrower
subtitle-break_block = Block destroyed
subtitle-attack_blocked = Attack blocked
subtitle-parry = Parried
subtitle-interrupted = Interrupted
subtitle-stunned = Stunned
subtitle-dazed = Dazed
subtitle-knocked_down = Knocked down
subtitle-attack-ground_slam = Ground slam
subtitle-attack-laser_beam = Laser beam
subtitle-attack-cyclops_charge = Cyclops charge
subtitle-giga_roar = Frost gigas roar
subtitle-attack-flash_freeze = Flash freeze
subtitle-attack-icy_spikes = Icy spikes
subtitle-attack-ice_crack = Ice crack
subtitle-consume_potion = Drinking potion
subtitle-consume_apple = Eating apple
subtitle-consume_cheese = Eating cheese
subtitle-consume_food = Eating
subtitle-consume_liquid = Drinking
subtitle-utterance-alligator-angry = Alligator hissing
subtitle-utterance-antelope-angry = Antelope snorting
subtitle-utterance-biped_large-angry = Heavy grunting
subtitle-utterance-bird-angry = Bird screeching
subtitle-utterance-adlet-angry = Adlet barking
subtitle-utterance-pig-angry = Pig grunting
subtitle-utterance-reptile-angry = Reptile hissing
subtitle-utterance-sea_crocodile-angry = Sea Crocodile hissing
subtitle-utterance-saurok-angry = Saurok hissing
subtitle-utterance-cat-calm = Cat meowing
subtitle-utterance-cow-calm = Cow mooing
subtitle-utterance-fungome-calm = Fungome squeaking
subtitle-utterance-goat-calm = Goat bleating
subtitle-utterance-pig-calm = Pig oinking
subtitle-utterance-sheep-calm = Sheep bleating
subtitle-utterance-truffler-calm = Truffler oinking
subtitle-utterance-human-greeting = Greeting
subtitle-utterance-adlet-hurt = Adlet whining
subtitle-utterance-antelope-hurt = Antelope crying
subtitle-utterance-biped_large-hurt = Heavy hurting
subtitle-utterance-human-hurt = Human hurting
subtitle-utterance-lion-hurt = Lion growling
subtitle-utterance-mandroga-hurt = Mandroga screaming
subtitle-utterance-maneater-hurt = Maneater burping
subtitle-utterance-marlin-hurt = Marlin hurting
subtitle-utterance-mindflayer-hurt = Mindflayer hurting
subtitle-utterance-dagon-hurt = Dagon hurting
subtitle-utterance-asp-angry = Asp hissing
subtitle-utterance-asp-calm = Asp croaking
subtitle-utterance-asp-hurt = Asp hurting
subtitle-utterance-wendigo-angry = Wendigo screaming
subtitle-utterance-wendigo-calm = Wendigo mumbling
subtitle-utterance-wolf-angry = Wolf growling
subtitle-utterance-wolf-hurt = Wolf whining

View File

@ -0,0 +1,148 @@
subtitle-campfire = Lägereld knastrar
subtitle-bird_call = Fåglar sjunger
subtitle-bees = Bin surrar
subtitle-owl = Uggla tutar
subtitle-running_water = Vatten bubblar
subtitle-lightning = Åska
subtitle-footsteps_grass = Steg på gräs
subtitle-footsteps_earth = Steg på jord
subtitle-footsteps_rock = Steg på sten
subtitle-footsteps_snow = Steg på snö
subtitle-pickup_item = Föremål upplockat
subtitle-pickup_failed = Misslyckad upplockning
subtitle-glider_open = Glidare framtagen
subtitle-glider_close = Glidare bortlagd
subtitle-glide = Glidning
subtitle-roll = Rullning
subtitle-swim = Simmning
subtitle-climb = Klättring
subtitle-damage = Skada
subtitle-death = Död
subtitle-wield_bow = Båge framtagen
subtitle-unwield_bow = Båge bortlagd
subtitle-pickup_bow = Båge upplockad
subtitle-wield_sword = Svärd framtagen
subtitle-unwield_sword = Svärd bortlagd
subtitle-sword_attack = Svärd svigning
subtitle-pickup_sword = Svärd upplockad
subtitle-wield_axe = Yxa framtagen
subtitle-unwield_axe = Yxa bortlagd
subtitle-axe_attack = Yx svigning
subtitle-pickup_axe = Yxa upplockad
subtitle-wield_hammer = Hammare framtagen
subtitle-unwield_hammer = Hammare bortlagd
subtitle-hammer_attack = Hammar svigning
subtitle-pickup_hammer = Hammare upplockad
subtitle-wield_staff = Stav framtagen
subtitle-unwield_staff = Stav bortlagd
subtitle-fire_shot = Stav avfyrad
subtitle-staff_attack = Stav avfyrad
subtitle-pickup_staff = Stav upplockad
subtitle-wield_sceptre = Spira framtagen
subtitle-unwield_sceptre = Spira bortlagd
subtitle-sceptre_heal = Spira healande aura
subtitle-pickup_sceptre = Spira upplockad
subtitle-wield_dagger = Dolk framtagen
subtitle-uwield_dagger = Dolk bortlagd
subtitle-dagger_attack = Dolk svigning
subtitle-pickup_dagger = Dolk upplockad
subtitle-wield_shield = Shöld framtagen
subtitle-unwield_shield = Shöld bortlagd
subtitle-shield_attack = Shöld knuff
subtitle-pickup_shield = Shöld upplockad
subtitle-pickup_pick = Hacka upplockad
subtitle-pickup_gemstone = Ädelsten upplockad
subtitle-instrument_organ = Orgel spelning
subtitle-wield_instrument = Instrument framtagen
subtitle-unwield_instrument = Instrument bortlagd
subtitle-instrument_double_bass = Kontrabas spelning
subtitle-instrument_flute = Flöjt spelning
subtitle-instrument_glass_flute = Glas Flöjt spelning
subtitle-instrument_lyre = Lyra spelning
subtitle-instrument_icy_talharpa = Isig Talharpa spelning
subtitle-instrument_kalimba = Kalimba spelning
subtitle-instrument_melodica = Melodica spelning
subtitle-instrument_lute = Luta spelning
subtitle-instrument_sitar = Sitar spelning
subtitle-instrument_guitar = Gitarr spelning
subtitle-instrument_dark_guitar = Mörk Gitarr spelning
subtitle-instrument_washboard = Tvättbräda spelning
subtitle-pickup_instrument = Instrument upplockad
subtitle-explosion = Explosion
subtitle-arrow_shot = Pil skjuten
subtitle-arrow_miss = Pil miss
subtitle-arrow_hit = Pil träff
subtitle-skill_point = Nytt färdighetspoäng
subtitle-sceptre_beam = Spirstråle
subtitle-flame_thrower = Flamkastare
subtitle-break_block = Block förstört
subtitle-attack_blocked = Attack blockerad
subtitle-parry = Parerad
subtitle-interrupted = Avbruten
subtitle-stunned = Överväldigad
subtitle-dazed = Förvirrad
subtitle-knocked_down = Nedslagen
subtitle-attack-ground_slam = Marksmäll
subtitle-attack-laser_beam = Laserstråle
subtitle-attack-cyclops_charge = Cyclopsanfallning
subtitle-giga_roar = Frost gigas vrål
subtitle-attack-flash_freeze = Blixtfrysning
subtitle-attack-icy_spikes = Istappar
subtitle-attack-ice_crack = Isspricka
subtitle-consume_potion = Dricker förtrollningsdryck
subtitle-consume_apple = Äter äpple
subtitle-consume_cheese = Äter ost
subtitle-consume_food = Äter
subtitle-consume_liquid = Dricker
subtitle-utterance-alligator-angry = Alligator väser
subtitle-utterance-antelope-angry = Antilop fnyser
subtitle-utterance-biped_large-angry = Tung grymtning
subtitle-utterance-bird-angry = Fågel skriker gällt
subtitle-utterance-adlet-angry = Adlet skäller
subtitle-utterance-pig-angry = Gris grymtar
subtitle-utterance-reptile-angry = Reptil väser
subtitle-utterance-sea_crocodile-angry = Saltvattens krokodil väser
subtitle-utterance-saurok-angry = Saurok väser
subtitle-utterance-cat-calm = Katt jamar
subtitle-utterance-cow-calm = Ko råmar
subtitle-utterance-fungome-calm = Fungome piper
subtitle-utterance-goat-calm = Get bräker
subtitle-utterance-pig-calm = Gris nöffar
subtitle-utterance-sheep-calm = Sheep bräker
subtitle-utterance-truffler-calm = Truffler nöffar
subtitle-utterance-human-greeting = Hälsning
subtitle-utterance-adlet-hurt = Adlet gnäller
subtitle-utterance-antelope-hurt = Antilop skriker
subtitle-utterance-biped_large-hurt = Tung gnällning
subtitle-utterance-human-hurt = Människa har ont
subtitle-utterance-lion-hurt = Lejon morrar
subtitle-utterance-mandroga-hurt = Mandroga skriker
subtitle-utterance-maneater-hurt = Maneater rapar
subtitle-utterance-marlin-hurt = Marlin har ont
subtitle-utterance-mindflayer-hurt = Mindflayer har ont
subtitle-utterance-dagon-hurt = Dagon har ont
subtitle-utterance-asp-angry = Asp väser
subtitle-utterance-asp-calm = Asp kväker
subtitle-utterance-asp-hurt = Asp har ont
subtitle-utterance-wendigo-angry = Wendigo skriker
subtitle-utterance-wendigo-calm = Wendigo mumlar
subtitle-utterance-wolf-angry = Wolf morrar
subtitle-utterance-wolf-hurt = Wolf gnäller

View File

@ -14,17 +14,19 @@ use fader::Fader;
use music::MusicTransitionManifest; use music::MusicTransitionManifest;
use sfx::{SfxEvent, SfxTriggerItem}; use sfx::{SfxEvent, SfxTriggerItem};
use soundcache::load_ogg; use soundcache::load_ogg;
use std::time::Duration; use std::{collections::VecDeque, time::Duration};
use tracing::{debug, error}; use tracing::{debug, error};
use common::assets::{AssetExt, AssetHandle}; use common::assets::{AssetExt, AssetHandle};
use rodio::{source::Source, OutputStream, OutputStreamHandle, StreamError}; use rodio::{source::Source, OutputStream, OutputStreamHandle, StreamError};
use vek::*; use vek::*;
use crate::hud::Subtitle;
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct Listener { pub struct Listener {
pos: Vec3<f32>, pub pos: Vec3<f32>,
ori: Vec3<f32>, pub ori: Vec3<f32>,
ear_left_rpos: Vec3<f32>, ear_left_rpos: Vec3<f32>,
ear_right_rpos: Vec3<f32>, ear_right_rpos: Vec3<f32>,
@ -53,12 +55,20 @@ pub struct AudioFrontend {
music_spacing: f32, music_spacing: f32,
listener: Listener, listener: Listener,
pub subtitles_enabled: bool,
pub subtitles: VecDeque<Subtitle>,
mtm: AssetHandle<MusicTransitionManifest>, mtm: AssetHandle<MusicTransitionManifest>,
} }
impl AudioFrontend { impl AudioFrontend {
/// Construct with given device /// Construct with given device
pub fn new(/* dev: String, */ num_sfx_channels: usize, num_ui_channels: usize) -> Self { pub fn new(
/* dev: String, */
num_sfx_channels: usize,
num_ui_channels: usize,
subtitles: bool,
) -> Self {
// Commented out until audio device switcher works // Commented out until audio device switcher works
//let audio_device = get_device_raw(&dev); //let audio_device = get_device_raw(&dev);
@ -106,6 +116,8 @@ impl AudioFrontend {
music_spacing: 1.0, music_spacing: 1.0,
listener: Listener::default(), listener: Listener::default(),
mtm: AssetExt::load_expect("voxygen.audio.music_transition_manifest"), mtm: AssetExt::load_expect("voxygen.audio.music_transition_manifest"),
subtitles: VecDeque::new(),
subtitles_enabled: subtitles,
} }
} }
@ -129,6 +141,8 @@ impl AudioFrontend {
music_spacing: 1.0, music_spacing: 1.0,
listener: Listener::default(), listener: Listener::default(),
mtm: AssetExt::load_expect("voxygen.audio.music_transition_manifest"), mtm: AssetExt::load_expect("voxygen.audio.music_transition_manifest"),
subtitles: VecDeque::new(),
subtitles_enabled: false,
} }
} }
@ -223,9 +237,9 @@ impl AudioFrontend {
/// Errors if no sounds are found /// Errors if no sounds are found
fn get_sfx_file<'a>( fn get_sfx_file<'a>(
trigger_item: Option<(&'a SfxEvent, &'a SfxTriggerItem)>, trigger_item: Option<(&'a SfxEvent, &'a SfxTriggerItem)>,
) -> Option<&'a str> { ) -> Option<(&'a str, f32, Option<&'a str>)> {
trigger_item.map(|(event, item)| { trigger_item.map(|(event, item)| {
match item.files.len() { let file = match item.files.len() {
0 => { 0 => {
debug!("Sfx event {:?} is missing audio file.", event); debug!("Sfx event {:?} is missing audio file.", event);
"voxygen.audio.sfx.placeholder" "voxygen.audio.sfx.placeholder"
@ -239,7 +253,12 @@ impl AudioFrontend {
let rand_step = rand::random::<usize>() % item.files.len(); let rand_step = rand::random::<usize>() % item.files.len();
&item.files[rand_step] &item.files[rand_step]
}, },
} };
// NOTE: Threshold here is meant to give subtitles some idea of the duration of
// the audio, it doesn't have to be perfect but in the future, if possible we
// might want to switch it out for the actual duration.
(file, item.threshold, item.subtitle.as_deref())
}) })
} }
@ -252,11 +271,11 @@ impl AudioFrontend {
volume: Option<f32>, volume: Option<f32>,
underwater: bool, underwater: bool,
) { ) {
if let Some(sfx_file) = Self::get_sfx_file(trigger_item) { if let Some((sfx_file, dur, subtitle)) = Self::get_sfx_file(trigger_item) {
self.emit_subtitle(subtitle, Some(position), dur);
// Play sound in empty channel at given position // Play sound in empty channel at given position
if self.audio_stream.is_some() { if self.audio_stream.is_some() && self.sfx_enabled() {
let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0)); let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0));
let listener = self.listener.clone(); let listener = self.listener.clone();
if let Some(channel) = self.get_sfx_channel() { if let Some(channel) = self.get_sfx_channel() {
channel.set_pos(position); channel.set_pos(position);
@ -286,11 +305,11 @@ impl AudioFrontend {
freq: Option<u32>, freq: Option<u32>,
underwater: bool, underwater: bool,
) { ) {
if let Some(sfx_file) = Self::get_sfx_file(trigger_item) { if let Some((sfx_file, dur, subtitle)) = Self::get_sfx_file(trigger_item) {
self.emit_subtitle(subtitle, Some(position), dur);
// Play sound in empty channel at given position // Play sound in empty channel at given position
if self.audio_stream.is_some() { if self.audio_stream.is_some() && self.sfx_enabled() {
let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0)); let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0));
let listener = self.listener.clone(); let listener = self.listener.clone();
if let Some(channel) = self.get_sfx_channel() { if let Some(channel) = self.get_sfx_channel() {
channel.set_pos(position); channel.set_pos(position);
@ -321,11 +340,11 @@ impl AudioFrontend {
trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>, trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
volume: Option<f32>, volume: Option<f32>,
) { ) {
if let Some(sfx_file) = Self::get_sfx_file(trigger_item) { if let Some((sfx_file, dur, subtitle)) = Self::get_sfx_file(trigger_item) {
self.emit_subtitle(subtitle, None, dur);
// Play sound in empty channel // Play sound in empty channel
if self.audio_stream.is_some() { if self.audio_stream.is_some() && self.sfx_enabled() {
let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0)); let sound = load_ogg(sfx_file).amplify(volume.unwrap_or(1.0));
if let Some(channel) = self.get_ui_channel() { if let Some(channel) = self.get_ui_channel() {
channel.play(sound); channel.play(sound);
} }
@ -335,6 +354,26 @@ impl AudioFrontend {
} }
} }
pub fn emit_subtitle(
&mut self,
subtitle: Option<&str>,
position: Option<Vec3<f32>>,
duration: f32,
) {
if self.subtitles_enabled {
if let Some(subtitle) = subtitle {
self.subtitles.push_back(Subtitle {
localization: subtitle.to_string(),
position,
show_for: duration as f64,
});
if self.subtitles.len() > 10 {
self.subtitles.pop_front();
}
}
}
}
/// Plays a file at a given volume in the channel with a given tag /// Plays a file at a given volume in the channel with a given tag
fn play_ambient(&mut self, channel_tag: AmbientChannelTag, sound: &str, volume: Option<f32>) { fn play_ambient(&mut self, channel_tag: AmbientChannelTag, sound: &str, volume: Option<f32>) {
if self.audio_stream.is_some() { if self.audio_stream.is_some() {
@ -446,6 +485,8 @@ impl AudioFrontend {
} }
} }
pub fn get_listener(&self) -> &Listener { &self.listener }
/// Switches the playing music to the title music, which is pinned to a /// Switches the playing music to the title music, which is pinned to a
/// specific sound file (veloren_title_tune.ogg) /// specific sound file (veloren_title_tune.ogg)
pub fn play_title_music(&mut self) { pub fn play_title_music(&mut self) {
@ -504,6 +545,8 @@ impl AudioFrontend {
pub fn set_music_spacing(&mut self, multiplier: f32) { self.music_spacing = multiplier } pub fn set_music_spacing(&mut self, multiplier: f32) { self.music_spacing = multiplier }
pub fn set_subtitles(&mut self, enabled: bool) { self.subtitles_enabled = enabled }
/// Updates master volume in all channels /// Updates master volume in all channels
pub fn set_master_volume(&mut self, master_volume: f32) { pub fn set_master_volume(&mut self, master_volume: f32) {
self.master_volume = master_volume; self.master_volume = master_volume;

View File

@ -23,6 +23,7 @@ fn config_but_played_since_threshold_no_emit() {
let trigger_item = SfxTriggerItem { let trigger_item = SfxTriggerItem {
files: vec![String::from("some.path.to.sfx.file")], files: vec![String::from("some.path.to.sfx.file")],
threshold: 1.0, threshold: 1.0,
subtitle: None,
}; };
// Triggered a 'Run' 0 seconds ago // Triggered a 'Run' 0 seconds ago
@ -47,6 +48,7 @@ fn config_and_not_played_since_threshold_emits() {
let trigger_item = SfxTriggerItem { let trigger_item = SfxTriggerItem {
files: vec![String::from("some.path.to.sfx.file")], files: vec![String::from("some.path.to.sfx.file")],
threshold: 0.5, threshold: 0.5,
subtitle: None,
}; };
let previous_state = PreviousEntityState { let previous_state = PreviousEntityState {
@ -70,6 +72,7 @@ fn same_previous_event_elapsed_emits() {
let trigger_item = SfxTriggerItem { let trigger_item = SfxTriggerItem {
files: vec![String::from("some.path.to.sfx.file")], files: vec![String::from("some.path.to.sfx.file")],
threshold: 0.5, threshold: 0.5,
subtitle: None,
}; };
let previous_state = PreviousEntityState { let previous_state = PreviousEntityState {

View File

@ -328,6 +328,9 @@ pub struct SfxTriggerItem {
pub files: Vec<String>, pub files: Vec<String>,
/// The time to wait before repeating this SfxEvent /// The time to wait before repeating this SfxEvent
pub threshold: f32, pub threshold: f32,
#[serde(default)]
pub subtitle: Option<String>,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
@ -369,7 +372,7 @@ impl SfxMgr {
) { ) {
// Checks if the SFX volume is set to zero or audio is disabled // Checks if the SFX volume is set to zero or audio is disabled
// This prevents us from running all the following code unnecessarily // This prevents us from running all the following code unnecessarily
if !audio.sfx_enabled() { if !audio.sfx_enabled() && !audio.subtitles_enabled {
return; return;
} }
@ -401,7 +404,7 @@ impl SfxMgr {
client: &Client, client: &Client,
underwater: bool, underwater: bool,
) { ) {
if !audio.sfx_enabled() { if !audio.sfx_enabled() && !audio.subtitles_enabled {
return; return;
} }
let triggers = self.triggers.read(); let triggers = self.triggers.read();

View File

@ -9,8 +9,6 @@ mod diary;
mod esc_menu; mod esc_menu;
mod group; mod group;
mod hotbar; mod hotbar;
pub mod img_ids;
pub mod item_imgs;
mod loot_scroller; mod loot_scroller;
mod map; mod map;
mod minimap; mod minimap;
@ -23,7 +21,11 @@ mod settings_window;
mod skillbar; mod skillbar;
mod slots; mod slots;
mod social; mod social;
mod subtitles;
mod trade; mod trade;
pub mod img_ids;
pub mod item_imgs;
pub mod util; pub mod util;
pub use crafting::CraftingTab; pub use crafting::CraftingTab;
@ -31,6 +33,7 @@ pub use hotbar::{SlotContents as HotbarSlotContents, State as HotbarState};
pub use item_imgs::animate_by_pulse; pub use item_imgs::animate_by_pulse;
pub use loot_scroller::LootMessage; pub use loot_scroller::LootMessage;
pub use settings_window::ScaleChange; pub use settings_window::ScaleChange;
pub use subtitles::Subtitle;
use bag::Bag; use bag::Bag;
use buffs::BuffsBar; use buffs::BuffsBar;
@ -54,6 +57,7 @@ use serde::{Deserialize, Serialize};
use settings_window::{SettingsTab, SettingsWindow}; use settings_window::{SettingsTab, SettingsWindow};
use skillbar::Skillbar; use skillbar::Skillbar;
use social::Social; use social::Social;
use subtitles::Subtitles;
use trade::Trade; use trade::Trade;
use crate::{ use crate::{
@ -328,6 +332,7 @@ widget_ids! {
settings_window, settings_window,
group_window, group_window,
item_info, item_info,
subtitles,
// Free look indicator // Free look indicator
free_look_txt, free_look_txt,
@ -1451,7 +1456,7 @@ impl Hud {
fn update_layout( fn update_layout(
&mut self, &mut self,
client: &Client, client: &Client,
global_state: &GlobalState, global_state: &mut GlobalState,
debug_info: &Option<DebugInfo>, debug_info: &Option<DebugInfo>,
dt: Duration, dt: Duration,
info: HudInfo, info: HudInfo,
@ -3343,6 +3348,18 @@ impl Hud {
} }
} }
if global_state.settings.audio.subtitles {
Subtitles::new(
client,
&global_state.settings,
&global_state.audio.get_listener().clone(),
&mut global_state.audio.subtitles,
&self.fonts,
i18n,
)
.set(self.ids.subtitles, ui_widgets);
}
self.new_messages = VecDeque::new(); self.new_messages = VecDeque::new();
self.new_notifications = VecDeque::new(); self.new_notifications = VecDeque::new();

View File

@ -0,0 +1,156 @@
use crate::{
hud::{img_ids::Imgs, TEXT_COLOR},
render::RenderMode,
session::settings_change::{Accessibility as AccessibilityChange, Accessibility::*},
ui::{fonts::Fonts, ToggleButton},
GlobalState,
};
use conrod_core::{
color,
widget::{self, Rectangle, Text},
widget_ids, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};
use i18n::Localization;
widget_ids! {
struct Ids {
window,
window_r,
flashing_lights_button,
flashing_lights_label,
flashing_lights_info_label,
subtitles_button,
subtitles_label,
}
}
#[derive(WidgetCommon)]
pub struct Accessibility<'a> {
global_state: &'a GlobalState,
imgs: &'a Imgs,
fonts: &'a Fonts,
localized_strings: &'a Localization,
#[conrod(common_builder)]
common: widget::CommonBuilder,
}
impl<'a> Accessibility<'a> {
pub fn new(
global_state: &'a GlobalState,
imgs: &'a Imgs,
fonts: &'a Fonts,
localized_strings: &'a Localization,
) -> Self {
Self {
global_state,
imgs,
fonts,
localized_strings,
common: widget::CommonBuilder::default(),
}
}
}
pub struct State {
ids: Ids,
}
impl<'a> Widget for Accessibility<'a> {
type Event = Vec<AccessibilityChange>;
type State = State;
type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
State {
ids: Ids::new(id_gen),
}
}
fn style(&self) -> Self::Style {}
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
common_base::prof_span!("Accessibility::update");
let widget::UpdateArgs { state, ui, .. } = args;
let mut events = Vec::new();
Rectangle::fill_with(args.rect.dim(), color::TRANSPARENT)
.xy(args.rect.xy())
.graphics_for(args.id)
.scroll_kids()
.scroll_kids_vertically()
.set(state.ids.window, ui);
Rectangle::fill_with([args.rect.w() / 2.0, args.rect.h()], color::TRANSPARENT)
.top_right()
.parent(state.ids.window)
.set(state.ids.window_r, ui);
// Get render mode
let render_mode = &self.global_state.settings.graphics.render_mode;
// Flashing lights
Text::new(
&self
.localized_strings
.get_msg("hud-settings-flashing_lights"),
)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.top_left_with_margins_on(state.ids.window, 10.0, 10.0)
.color(TEXT_COLOR)
.set(state.ids.flashing_lights_label, ui);
let flashing_lights_enabled = ToggleButton::new(
render_mode.flashing_lights_enabled,
self.imgs.checkbox,
self.imgs.checkbox_checked,
)
.w_h(18.0, 18.0)
.right_from(state.ids.flashing_lights_label, 10.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
.set(state.ids.flashing_lights_button, ui);
Text::new(
&self
.localized_strings
.get_msg("hud-settings-flashing_lights_info"),
)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.right_from(state.ids.flashing_lights_label, 32.0)
.color(TEXT_COLOR)
.set(state.ids.flashing_lights_info_label, ui);
if render_mode.flashing_lights_enabled != flashing_lights_enabled {
events.push(ChangeRenderMode(Box::new(RenderMode {
flashing_lights_enabled,
..render_mode.clone()
})));
}
// Subtitles
Text::new(&self.localized_strings.get_msg("hud-settings-subtitles"))
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.down_from(state.ids.flashing_lights_label, 10.0)
.color(TEXT_COLOR)
.set(state.ids.subtitles_label, ui);
let subtitles_enabled = ToggleButton::new(
self.global_state.settings.audio.subtitles,
self.imgs.checkbox,
self.imgs.checkbox_checked,
)
.w_h(18.0, 18.0)
.right_from(state.ids.subtitles_label, 10.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
.set(state.ids.subtitles_button, ui);
if subtitles_enabled != self.global_state.settings.audio.subtitles {
events.push(SetSubtitles(subtitles_enabled));
}
events
}
}

View File

@ -1,3 +1,4 @@
mod accessibility;
mod chat; mod chat;
mod controls; mod controls;
mod gameplay; mod gameplay;
@ -41,6 +42,7 @@ widget_ids! {
language, language,
chat, chat,
networking, networking,
accessibility,
} }
} }
@ -57,6 +59,7 @@ pub enum SettingsTab {
Controls, Controls,
Lang, Lang,
Networking, Networking,
Accessibility,
} }
impl SettingsTab { impl SettingsTab {
fn name_key(&self) -> &str { fn name_key(&self) -> &str {
@ -69,6 +72,7 @@ impl SettingsTab {
SettingsTab::Sound => "common-sound", SettingsTab::Sound => "common-sound",
SettingsTab::Lang => "common-languages", SettingsTab::Lang => "common-languages",
SettingsTab::Networking => "common-networking", SettingsTab::Networking => "common-networking",
SettingsTab::Accessibility => "common-accessibility",
} }
} }
@ -82,6 +86,7 @@ impl SettingsTab {
SettingsTab::Sound => "common-sound_settings", SettingsTab::Sound => "common-sound_settings",
SettingsTab::Lang => "common-language_settings", SettingsTab::Lang => "common-language_settings",
SettingsTab::Networking => "common-networking_settings", SettingsTab::Networking => "common-networking_settings",
SettingsTab::Accessibility => "common-accessibility_settings",
} }
} }
} }
@ -350,6 +355,16 @@ impl<'a> Widget for SettingsWindow<'a> {
events.push(Event::SettingsChange(change.into())); events.push(Event::SettingsChange(change.into()));
} }
}, },
SettingsTab::Accessibility => {
for change in
accessibility::Accessibility::new(global_state, imgs, fonts, localized_strings)
.top_left_with_margins_on(state.ids.settings_content_align, 0.0, 0.0)
.wh_of(state.ids.settings_content_align)
.set(state.ids.accessibility, ui)
{
events.push(Event::SettingsChange(change.into()));
}
},
} }
events events

View File

@ -0,0 +1,332 @@
use std::{cmp::Ordering, collections::VecDeque};
use crate::{audio::Listener, settings::Settings, ui::fonts::Fonts};
use client::Client;
use conrod_core::{
widget::{self, Id, Rectangle, Text},
widget_ids, Colorable, Positionable, UiCell, Widget, WidgetCommon,
};
use i18n::Localization;
use vek::{Vec2, Vec3};
widget_ids! {
struct Ids {
subtitle_box_bg,
subtitle_message[],
subtitle_dir[],
}
}
#[derive(WidgetCommon)]
pub struct Subtitles<'a> {
client: &'a Client,
settings: &'a Settings,
listener: &'a Listener,
fonts: &'a Fonts,
new_subtitles: &'a mut VecDeque<Subtitle>,
#[conrod(common_builder)]
common: widget::CommonBuilder,
localized_strings: &'a Localization,
}
impl<'a> Subtitles<'a> {
pub fn new(
client: &'a Client,
settings: &'a Settings,
listener: &'a Listener,
new_subtitles: &'a mut VecDeque<Subtitle>,
fonts: &'a Fonts,
localized_strings: &'a Localization,
) -> Self {
Self {
client,
settings,
listener,
fonts,
new_subtitles,
common: widget::CommonBuilder::default(),
localized_strings,
}
}
}
const MIN_SUBTITLE_DURATION: f64 = 1.5;
const MAX_SUBTITLE_DIST: f32 = 80.0;
#[derive(Debug)]
pub struct Subtitle {
pub localization: String,
/// Position the sound is played at, if any.
pub position: Option<Vec3<f32>>,
/// Amount of seconds to show the subtitle for.
pub show_for: f64,
}
#[derive(Clone, PartialEq)]
struct SubtitleData {
position: Option<Vec3<f32>>,
/// `Time` to show until.
show_until: f64,
}
impl SubtitleData {
/// Prioritize showing nearby sounds, and secondarily prioritize longer
/// living sounds.
fn compare_priority(&self, other: &Self, listener_pos: Vec3<f32>) -> Ordering {
let life_cmp = self
.show_until
.partial_cmp(&other.show_until)
.unwrap_or(Ordering::Equal);
match (self.position, other.position) {
(Some(a), Some(b)) => match a
.distance_squared(listener_pos)
.partial_cmp(&b.distance_squared(listener_pos))
.unwrap_or(Ordering::Equal)
{
Ordering::Equal => life_cmp,
Ordering::Less => Ordering::Greater,
Ordering::Greater => Ordering::Less,
},
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => life_cmp,
}
}
}
#[derive(Clone)]
struct SubtitleList {
subtitles: Vec<(String, Vec<SubtitleData>)>,
}
impl SubtitleList {
fn new() -> Self {
Self {
subtitles: Vec::new(),
}
}
/// Updates the subtitle state, returns the amount of subtitles that should
/// be displayed.
fn update(
&mut self,
new_subtitles: impl Iterator<Item = Subtitle>,
time: f64,
listener_pos: Vec3<f32>,
) -> usize {
for subtitle in new_subtitles {
let show_until = time + subtitle.show_for.max(MIN_SUBTITLE_DURATION);
let data = SubtitleData {
position: subtitle.position,
show_until,
};
if let Some((_, datas)) = self
.subtitles
.iter_mut()
.find(|(key, _)| key == &subtitle.localization)
{
datas.push(data);
} else {
self.subtitles.push((subtitle.localization, vec![data]))
}
}
let mut to_display = 0;
self.subtitles.retain_mut(|(_, data)| {
data.retain(|subtitle| subtitle.show_until > time);
// Place the most prioritized subtitle in the back.
if let Some((i, s)) = data
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.compare_priority(b, listener_pos))
{
// We only display subtitles that are in range.
if s.position.map_or(true, |pos| {
pos.distance_squared(listener_pos) < MAX_SUBTITLE_DIST * MAX_SUBTITLE_DIST
}) {
to_display += 1;
}
let last = data.len() - 1;
data.swap(i, last);
true
} else {
// If data is empty we have no sounds with this key.
false
}
});
to_display
}
}
pub struct State {
subtitles: SubtitleList,
ids: Ids,
}
impl<'a> Widget for Subtitles<'a> {
type Event = ();
type State = State;
type Style = ();
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
State {
subtitles: SubtitleList::new(),
ids: Ids::new(id_gen),
}
}
fn style(&self) -> Self::Style {}
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
common_base::prof_span!("Chat::update");
let widget::UpdateArgs { state, ui, .. } = args;
let time = self.client.state().get_time();
let listener_pos = self.listener.pos;
let listener_forward = self.listener.ori;
// Update subtitles and look for changes
let mut subtitles = state.subtitles.clone();
let has_new = !self.new_subtitles.is_empty();
let show_count = subtitles.update(self.new_subtitles.drain(..), time, listener_pos);
let subtitles = if has_new || show_count != state.ids.subtitle_message.len() {
state.update(|s| {
s.subtitles = subtitles;
s.ids
.subtitle_message
.resize(show_count, &mut ui.widget_id_generator());
s.ids
.subtitle_dir
.resize(show_count, &mut ui.widget_id_generator());
});
&state.subtitles
} else {
&subtitles
};
let color = |t: &SubtitleData| -> conrod_core::Color {
conrod_core::Color::Rgba(
0.9,
1.0,
1.0,
((t.show_until - time) * 2.0).clamp(0.0, 1.0) as f32,
)
};
let listener_forward = listener_forward
.xy()
.try_normalized()
.unwrap_or(Vec2::unit_y());
let listener_right = Vec2::new(listener_forward.y, -listener_forward.x);
let dir = |subtitle: &SubtitleData, id: &Id, dir_id: &Id, ui: &mut UiCell| {
enum Side {
/// Also used for sounds without direction.
Forward,
Right,
Left,
}
let is_right = subtitle
.position
.map(|pos| {
let dist = pos.distance(listener_pos);
let dir = (pos - listener_pos) / dist;
let dot = dir.xy().dot(listener_forward);
if dist < 2.0 || dot > 0.85 {
Side::Forward
} else if dir.xy().dot(listener_right) >= 0.0 {
Side::Right
} else {
Side::Left
}
})
.unwrap_or(Side::Forward);
match is_right {
Side::Right => Text::new("> ")
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
.align_right_of(state.ids.subtitle_box_bg)
.align_middle_y_of(*id)
.color(color(subtitle))
.set(*dir_id, ui),
Side::Left => Text::new(" <")
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
.align_left_of(state.ids.subtitle_box_bg)
.align_middle_y_of(*id)
.color(color(subtitle))
.set(*dir_id, ui),
Side::Forward => Text::new("")
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
.color(color(subtitle))
.set(*dir_id, ui),
}
};
Rectangle::fill([200.0, 22.0 * show_count as f64])
.rgba(0.0, 0.0, 0.0, self.settings.chat.chat_opacity)
.bottom_right_with_margins_on(ui.window, 40.0, 30.0)
.set(state.ids.subtitle_box_bg, ui);
let mut subtitles = state
.ids
.subtitle_message
.iter()
.zip(state.ids.subtitle_dir.iter())
.zip(
subtitles
.subtitles
.iter()
.filter_map(|(localization, data)| {
let data = data.last()?;
data.position
.map_or(true, |pos| {
pos.distance_squared(listener_pos)
< MAX_SUBTITLE_DIST * MAX_SUBTITLE_DIST
})
.then(|| (self.localized_strings.get_msg(localization), data))
}),
);
if let Some(((id, dir_id), (message, data))) = subtitles.next() {
Text::new(&message)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
.center_justify()
.mid_bottom_with_margin_on(state.ids.subtitle_box_bg, 6.0)
.color(color(data))
.set(*id, ui);
dir(data, id, dir_id, ui);
let mut last_id = *id;
for ((id, dir_id), (message, data)) in subtitles {
Text::new(&message)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.parent(state.ids.subtitle_box_bg)
.up_from(last_id, 8.0)
.align_middle_x_of(last_id)
.color(color(data))
.set(*id, ui);
dir(data, id, dir_id, ui);
last_id = *id;
}
}
}
}

View File

@ -139,6 +139,7 @@ fn main() {
AudioOutput::Automatic => AudioFrontend::new( AudioOutput::Automatic => AudioFrontend::new(
settings.audio.num_sfx_channels, settings.audio.num_sfx_channels,
settings.audio.num_ui_channels, settings.audio.num_ui_channels,
settings.audio.subtitles,
), ),
// AudioOutput::Device(ref dev) => Some(dev.clone()), // AudioOutput::Device(ref dev) => Some(dev.clone()),
}; };

View File

@ -172,6 +172,12 @@ pub enum Networking {
// option) // option)
} }
#[derive(Clone)]
pub enum Accessibility {
ChangeRenderMode(Box<RenderMode>),
SetSubtitles(bool),
}
#[derive(Clone)] #[derive(Clone)]
pub enum SettingsChange { pub enum SettingsChange {
Audio(Audio), Audio(Audio),
@ -183,6 +189,7 @@ pub enum SettingsChange {
Interface(Interface), Interface(Interface),
Language(Language), Language(Language),
Networking(Networking), Networking(Networking),
Accessibility(Accessibility),
} }
macro_rules! settings_change_from { macro_rules! settings_change_from {
@ -201,6 +208,7 @@ settings_change_from!(Graphics);
settings_change_from!(Interface); settings_change_from!(Interface);
settings_change_from!(Language); settings_change_from!(Language);
settings_change_from!(Networking); settings_change_from!(Networking);
settings_change_from!(Accessibility);
impl SettingsChange { impl SettingsChange {
pub fn process(self, global_state: &mut GlobalState, session_state: &mut SessionState) { pub fn process(self, global_state: &mut GlobalState, session_state: &mut SessionState) {
@ -734,6 +742,15 @@ impl SettingsChange {
} }
}, },
}, },
SettingsChange::Accessibility(accessibility_change) => match accessibility_change {
Accessibility::ChangeRenderMode(new_render_mode) => {
change_render_mode(*new_render_mode, &mut global_state.window, settings);
},
Accessibility::SetSubtitles(enabled) => {
global_state.settings.audio.subtitles = enabled;
global_state.audio.set_subtitles(enabled);
},
},
} }
global_state global_state
.settings .settings

View File

@ -47,6 +47,7 @@ pub struct AudioSettings {
pub num_sfx_channels: usize, pub num_sfx_channels: usize,
pub num_ui_channels: usize, pub num_ui_channels: usize,
pub music_spacing: f32, pub music_spacing: f32,
pub subtitles: bool,
/// Audio Device that Voxygen will use to play audio. /// Audio Device that Voxygen will use to play audio.
pub output: AudioOutput, pub output: AudioOutput,
@ -63,6 +64,7 @@ impl Default for AudioSettings {
num_sfx_channels: 60, num_sfx_channels: 60,
num_ui_channels: 10, num_ui_channels: 10,
music_spacing: 1.0, music_spacing: 1.0,
subtitles: false,
output: AudioOutput::Automatic, output: AudioOutput::Automatic,
} }
} }