Skip to main content

veloren_voxygen/hud/
mod.rs

1#![expect(non_local_definitions)] // because of WidgetCommon derive
2mod animation;
3mod bag;
4mod buffs;
5mod change_notification;
6mod chat;
7mod crafting;
8mod diary;
9mod esc_menu;
10mod group;
11mod hotbar;
12mod loot_scroller;
13mod map;
14mod minimap;
15mod overhead;
16mod overitem;
17mod popup;
18mod prompt_dialog;
19mod quest;
20mod settings_window;
21mod skillbar;
22mod slot_grid;
23mod slots;
24mod social;
25mod subtitles;
26mod trade;
27
28pub mod controller_icons;
29pub mod img_ids;
30pub mod item_imgs;
31pub mod tutorial;
32pub mod util;
33
34pub use chat::MessageBacklog;
35pub use crafting::CraftingTab;
36pub use hotbar::{SlotContents as HotbarSlotContents, State as HotbarState};
37pub use item_imgs::animate_by_pulse;
38pub use loot_scroller::LootMessage;
39pub use settings_window::ScaleChange;
40pub use subtitles::Subtitle;
41
42use bag::BagManager;
43use buffs::BuffsBar;
44use change_notification::{ChangeNotification, NotificationReason};
45use chat::Chat;
46use chrono::NaiveTime;
47use crafting::Crafting;
48use diary::{Diary, SelectedSkillTree};
49use esc_menu::EscMenu;
50use group::Group;
51use img_ids::Imgs;
52use item_imgs::ItemImgs;
53use loot_scroller::LootScroller;
54use map::Map;
55use minimap::{MiniMap, VoxelMinimap};
56use popup::Popup;
57use prompt_dialog::PromptDialog;
58use quest::Quest;
59use serde::{Deserialize, Serialize};
60use settings_window::{SettingsTab, SettingsWindow};
61use skillbar::Skillbar;
62use slot_grid::SlotGrid;
63use social::Social;
64use subtitles::Subtitles;
65use trade::Trade;
66use tutorial::{DynamicTutorial, Tutorial};
67
68use crate::{
69    GlobalState,
70    audio::ActiveChannels,
71    ecs::comp::{self as vcomp, HpFloater, HpFloaterList},
72    game_input::GameInput,
73    hud::{img_ids::ImgsRot, prompt_dialog::DialogOutcomeEvent},
74    key_state::KeyState,
75    render::UiDrawer,
76    scene::{
77        SceneData,
78        camera::{self, Camera},
79    },
80    session::{
81        interactable::{self, BlockInteraction, EntityInteraction},
82        settings_change::{
83            Audio, Chat as ChatChange, Interface as InterfaceChange, Inventory, SettingsChange,
84        },
85    },
86    settings::chat::ChatFilter,
87    ui::{
88        Graphic, Ingameable, ScaleMode, Ui,
89        fonts::Fonts,
90        img_ids::Rotations,
91        slot::{self, SlotKey},
92    },
93    window::{Event as WinEvent, MenuInput},
94};
95use client::{Client, UserNotification};
96use common::{
97    combat,
98    comp::{
99        self, BuffData, BuffKind, Content, Health, Item, MapMarkerChange, PickupItem, PresenceKind,
100        ability::{AuxiliaryAbility, Stance},
101        fluid_dynamics,
102        inventory::{
103            CollectFailedReason, InventorySortOrder,
104            slot::{InvSlotId, Slot},
105            trade_pricing::TradePricing,
106        },
107        item::{
108            ItemDefinitionIdOwned, ItemDesc, ItemI18n, MaterialStatManifest, Quality,
109            tool::ToolKind,
110        },
111        loot_owner::LootOwnerKind,
112        skillset::{SkillGroupKind, SkillsPersistenceError, skills::Skill},
113    },
114    consts::{MAX_NPCINTERACT_RANGE, MAX_PICKUP_RANGE},
115    link::Is,
116    mounting::{Mount, Rider, VolumePos},
117    outcome::Outcome,
118    recipe::RecipeBookManifest,
119    resources::{BattleMode, Secs, Time},
120    rtsim,
121    slowjob::SlowJobPool,
122    terrain::{Block, SpriteKind, TerrainChunk, UnlockKind},
123    trade::{ReducedInventory, TradeAction},
124    uid::Uid,
125    util::{Dir, srgba_to_linear},
126    vol::RectRasterableVol,
127};
128use common_base::{prof_span, span};
129use common_net::{msg::world_msg::SiteId, sync::WorldSyncExt};
130use conrod_core::{
131    Color, Colorable, Labelable, Positionable, Sizeable, Widget,
132    text::cursor::Index,
133    widget::{self, Button, Image, Rectangle, Text},
134    widget_ids,
135};
136use hashbrown::{HashMap, HashSet};
137use i18n::Localization;
138use rand::RngExt;
139use specs::{Entity as EcsEntity, Join, LendJoin, WorldExt};
140use std::{
141    borrow::Cow,
142    cell::RefCell,
143    cmp::Ordering,
144    collections::VecDeque,
145    rc::Rc,
146    sync::Arc,
147    time::{Duration, Instant},
148};
149use tracing::{instrument, trace, warn};
150use vek::*;
151
152const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
153const TEXT_VELORITE: Color = Color::Rgba(0.0, 0.66, 0.66, 1.0);
154const TEXT_BLUE_COLOR: Color = Color::Rgba(0.8, 0.9, 1.0, 1.0);
155const TEXT_GRAY_COLOR: Color = Color::Rgba(0.5, 0.5, 0.5, 1.0);
156const TEXT_DULL_RED_COLOR: Color = Color::Rgba(0.56, 0.2, 0.2, 1.0);
157const TEXT_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
158const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5);
159//const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
160const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1);
161const TEXT_BIND_CONFLICT_COLOR: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
162const BLACK: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0);
163//const BG_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 0.8);
164const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0);
165const LOW_HP_COLOR: Color = Color::Rgba(0.93, 0.59, 0.03, 1.0);
166const CRITICAL_HP_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0);
167const STAMINA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9);
168const ENEMY_HP_COLOR: Color = Color::Rgba(0.93, 0.1, 0.29, 1.0);
169const XP_COLOR: Color = Color::Rgba(0.59, 0.41, 0.67, 1.0);
170const POISE_COLOR: Color = Color::Rgba(0.70, 0.0, 0.60, 1.0);
171const POISEBAR_TICK_COLOR: Color = Color::Rgba(0.70, 0.90, 0.0, 1.0);
172//const TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0);
173//const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0);
174//const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0);
175const BUFF_COLOR: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0);
176const DEBUFF_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0);
177
178// Item Quality Colors
179const QUALITY_LOW: Color = Color::Rgba(0.60, 0.60, 0.60, 1.0); // Grey - Trash, can be sold to vendors
180const QUALITY_COMMON: Color = Color::Rgba(0.79, 1.00, 1.00, 1.0); // Light blue - Crafting mats, food, starting equipment, quest items (like
181// keys), rewards for easy quests
182const QUALITY_MODERATE: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0); // Green - Quest Rewards, commonly looted items from NPCs
183const QUALITY_HIGH: Color = Color::Rgba(0.18, 0.32, 0.9, 1.0); // Blue - Dungeon rewards, boss loot, rewards for hard quests
184const QUALITY_EPIC: Color = Color::Rgba(0.58, 0.29, 0.93, 1.0); // Purple - Rewards for epic quests and very hard bosses
185const QUALITY_LEGENDARY: Color = Color::Rgba(0.92, 0.76, 0.0, 1.0); // Gold - Legendary items that require a big effort to acquire
186const QUALITY_ARTIFACT: Color = Color::Rgba(0.74, 0.24, 0.11, 1.0); // Orange - Not obtainable by normal means, "artifacts"
187const QUALITY_DEBUG: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0); // Red - Admin and debug items
188
189// Chat Colors
190/// Color for chat command errors (yellow !)
191const ERROR_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0);
192/// Color for chat command info (blue i)
193const INFO_COLOR: Color = Color::Rgba(0.28, 0.83, 0.71, 1.0);
194/// Online color
195const ONLINE_COLOR: Color = Color::Rgba(0.3, 1.0, 0.3, 1.0);
196/// Offline color
197const OFFLINE_COLOR: Color = Color::Rgba(1.0, 0.3, 0.3, 1.0);
198/// Color for a private message from another player
199const TELL_COLOR: Color = Color::Rgba(0.98, 0.71, 1.0, 1.0);
200/// Color for local chat
201const SAY_COLOR: Color = Color::Rgba(1.0, 0.8, 0.8, 1.0);
202/// Color for group chat
203const GROUP_COLOR: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0);
204/// Color for factional chat
205const FACTION_COLOR: Color = Color::Rgba(0.24, 1.0, 0.48, 1.0);
206/// Color for regional chat
207const REGION_COLOR: Color = Color::Rgba(0.8, 1.0, 0.8, 1.0);
208/// Color for death messagesw
209const KILL_COLOR: Color = Color::Rgba(1.0, 0.17, 0.17, 1.0);
210/// Color for global messages
211const WORLD_COLOR: Color = Color::Rgba(0.95, 1.0, 0.95, 1.0);
212
213//Nametags
214const GROUP_MEMBER: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0);
215const DEFAULT_NPC: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
216const MARKED_NPC: Color = Color::Rgba(1.0, 0.8, 0.0, 1.0);
217
218// UI Color-Theme
219const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue
220const UI_SUBTLE: Color = Color::Rgba(0.2, 0.24, 0.24, 1.0); // Dark Greenish Blue
221//const UI_MAIN: Color = Color::Rgba(0.1, 0.1, 0.1, 0.97); // Dark
222const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0);
223// Pull-Down menu BG color
224const MENU_BG: Color = Color::Rgba(0.1, 0.12, 0.12, 1.0);
225//const UI_DARK_0: Color = Color::Rgba(0.25, 0.37, 0.37, 1.0);
226
227/// Distance at which nametags are visible for group members
228const NAMETAG_GROUP_RANGE: f32 = 1000.0;
229/// Distance at which nametags are visible
230const NAMETAG_RANGE: f32 = 40.0;
231/// Time nametags stay visible after doing damage even if they are out of range
232/// in seconds
233const NAMETAG_DMG_TIME: f32 = 60.0;
234/// Range damaged triggered nametags can be seen
235const NAMETAG_DMG_RANGE: f32 = 120.0;
236/// Range to display speech-bubbles at
237const SPEECH_BUBBLE_RANGE: f32 = NAMETAG_RANGE;
238const EXP_FLOATER_LIFETIME: f32 = 2.0;
239const EXP_ACCUMULATION_DURATION: f32 = 0.5;
240
241// TODO: Don't hard code this
242pub fn default_water_color() -> Rgba<f32> { srgba_to_linear(Rgba::new(0.0, 0.18, 0.37, 1.0)) }
243
244widget_ids! {
245    struct Ids {
246        // Crosshair
247        crosshair_inner,
248        crosshair_outer,
249        crosshair_charge,
250
251        // SCT
252        player_scts[],
253        player_sct_bgs[],
254        player_rank_up,
255        player_rank_up_txt_number,
256        player_rank_up_txt_0,
257        player_rank_up_txt_0_bg,
258        player_rank_up_txt_1,
259        player_rank_up_txt_1_bg,
260        player_rank_up_icon,
261        hurt_bg,
262        death_bg,
263        sct_bgs[],
264        scts[],
265
266        overheads[],
267        overitems[],
268
269        // Game Version
270        version,
271
272        // Debug
273        debug_bg,
274        fps_counter,
275        ping,
276        coordinates,
277        velocity,
278        glide_ratio,
279        glide_aoe,
280        air_vel,
281        orientation,
282        look_direction,
283        loaded_distance,
284        time,
285        entity_count,
286        num_chunks,
287        num_lights,
288        num_figures,
289        num_particles,
290        current_biome,
291        current_site,
292        graphics_backend,
293        gpu_timings[],
294        weather,
295        song_info,
296        active_channels,
297
298        // Help
299        help,
300        debug_info,
301
302        // External
303        chat,
304        loot_scroller,
305        map,
306        world_map,
307        popup,
308        minimap,
309        prompt_dialog,
310        bag,
311        trade,
312        social,
313        quest,
314        diary,
315        skillbar,
316        buttons,
317        buffs,
318        esc_menu,
319        social_window,
320        quest_window,
321        tutorial_window,
322        crafting_window,
323        settings_window,
324        group_window,
325        subtitles,
326
327        // Free look indicator
328        free_look_txt,
329        free_look_bg,
330
331        // Auto walk indicator
332        auto_walk_txt,
333        auto_walk_bg,
334
335        // Temporal (fading) camera zoom lock indicator
336        zoom_lock_txt,
337        zoom_lock_bg,
338
339        // Camera clamp indicator
340        camera_clamp_txt,
341        camera_clamp_bg,
342
343        // Tutorial
344        quest_bg,
345        q_headline_bg,
346        q_headline,
347        q_text_bg,
348        q_text,
349        accept_button,
350        intro_button,
351        tut_arrow,
352        tut_arrow_txt_bg,
353        tut_arrow_txt,
354    }
355}
356
357/// Specifier to use with `Position::position`
358/// Read its documentation for more
359// TODO: extend as you need it
360#[derive(Clone, Copy)]
361pub enum PositionSpecifier {
362    // Place the widget near other widget with the given margins
363    TopLeftWithMarginsOn(widget::Id, f64, f64),
364    TopRightWithMarginsOn(widget::Id, f64, f64),
365    MidBottomWithMarginOn(widget::Id, f64),
366    BottomLeftWithMarginsOn(widget::Id, f64, f64),
367    BottomRightWithMarginsOn(widget::Id, f64, f64),
368    // Place the widget near other widget with given margin
369    MidTopWithMarginOn(widget::Id, f64),
370    // Place the widget near other widget at given distance
371    MiddleOf(widget::Id),
372    UpFrom(widget::Id, f64),
373    DownFrom(widget::Id, f64),
374    LeftFrom(widget::Id, f64),
375    RightFrom(widget::Id, f64),
376}
377
378/// Trait which enables you to declare widget position
379/// to use later on widget creation.
380/// It is implemented for all widgets which are implement Positionable,
381/// so you can easily change your code to use this method.
382///
383/// Consider this example:
384/// ```text
385///     let slot1 = slot_maker
386///         .fabricate(hotbar::Slot::One, [40.0; 2])
387///         .filled_slot(self.imgs.skillbar_slot)
388///         .bottom_left_with_margins_on(state.ids.frame, 0.0, 0.0);
389///     if condition {
390///         call_slot1(slot1);
391///     } else {
392///         call_slot2(slot1);
393///     }
394///     let slot2 = slot_maker
395///         .fabricate(hotbar::Slot::Two, [40.0; 2])
396///         .filled_slot(self.imgs.skillbar_slot)
397///         .right_from(state.ids.slot1, slot_offset);
398///     if condition {
399///         call_slot1(slot2);
400///     } else {
401///         call_slot2(slot2);
402///     }
403/// ```
404/// Despite being identical, you can't easily deduplicate code
405/// which uses slot1 and slot2 as they are calling methods to position itself.
406/// This can be solved if you declare position and use it later like so
407/// ```text
408/// let slots = [
409///     (hotbar::Slot::One, BottomLeftWithMarginsOn(state.ids.frame, 0.0, 0.0)),
410///     (hotbar::Slot::Two, RightFrom(state.ids.slot1, slot_offset)),
411/// ];
412/// for (slot, pos) in slots {
413///     let slot = slot_maker
414///         .fabricate(slot, [40.0; 2])
415///         .filled_slot(self.imgs.skillbar_slot)
416///         .position(pos);
417///     if condition {
418///         call_slot1(slot);
419///     } else {
420///         call_slot2(slot);
421///     }
422/// }
423/// ```
424pub trait Position {
425    #[must_use]
426    fn position(self, request: PositionSpecifier) -> Self;
427}
428
429impl<W: Positionable> Position for W {
430    fn position(self, request: PositionSpecifier) -> Self {
431        match request {
432            // Place the widget near other widget with the given margins
433            PositionSpecifier::TopLeftWithMarginsOn(other, top, left) => {
434                self.top_left_with_margins_on(other, top, left)
435            },
436            PositionSpecifier::TopRightWithMarginsOn(other, top, right) => {
437                self.top_right_with_margins_on(other, top, right)
438            },
439            PositionSpecifier::MidBottomWithMarginOn(other, margin) => {
440                self.mid_bottom_with_margin_on(other, margin)
441            },
442            PositionSpecifier::BottomRightWithMarginsOn(other, bottom, right) => {
443                self.bottom_right_with_margins_on(other, bottom, right)
444            },
445            PositionSpecifier::BottomLeftWithMarginsOn(other, bottom, left) => {
446                self.bottom_left_with_margins_on(other, bottom, left)
447            },
448            // Place the widget near other widget with given margin
449            PositionSpecifier::MidTopWithMarginOn(other, margin) => {
450                self.mid_top_with_margin_on(other, margin)
451            },
452            // Place the widget near other widget at given distance
453            PositionSpecifier::MiddleOf(other) => self.middle_of(other),
454            PositionSpecifier::UpFrom(other, offset) => self.up_from(other, offset),
455            PositionSpecifier::DownFrom(other, offset) => self.down_from(other, offset),
456            PositionSpecifier::LeftFrom(other, offset) => self.left_from(other, offset),
457            PositionSpecifier::RightFrom(other, offset) => self.right_from(other, offset),
458        }
459    }
460}
461
462#[derive(Clone, Copy, Debug)]
463pub enum BuffIconKind {
464    Buff {
465        kind: BuffKind,
466        data: BuffData,
467        multiplicity: usize,
468    },
469    Stance(Stance),
470}
471
472impl BuffIconKind {
473    pub fn image(&self, imgs: &Imgs) -> conrod_core::image::Id {
474        match self {
475            Self::Buff { kind, .. } => get_buff_image(*kind, imgs),
476            Self::Stance(stance) => util::ability_image(imgs, stance.pseudo_ability_id()),
477        }
478    }
479
480    pub fn max_duration(&self) -> Option<Secs> {
481        match self {
482            Self::Buff { data, .. } => data.duration,
483            Self::Stance(_) => None,
484        }
485    }
486
487    pub fn title_description<'b>(
488        &self,
489        localized_strings: &'b Localization,
490    ) -> (Cow<'b, str>, Cow<'b, str>) {
491        match self {
492            Self::Buff {
493                kind,
494                data,
495                multiplicity: _,
496            } => (
497                util::get_buff_title(*kind, localized_strings),
498                util::get_buff_desc(*kind, *data, localized_strings),
499            ),
500            Self::Stance(stance) => {
501                util::ability_description(stance.pseudo_ability_id(), localized_strings)
502            },
503        }
504    }
505}
506
507impl PartialOrd for BuffIconKind {
508    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
509}
510
511impl Ord for BuffIconKind {
512    fn cmp(&self, other: &Self) -> Ordering {
513        match (self, other) {
514            (
515                BuffIconKind::Buff { kind, .. },
516                BuffIconKind::Buff {
517                    kind: other_kind, ..
518                },
519            ) => kind.cmp(other_kind),
520            (BuffIconKind::Buff { .. }, BuffIconKind::Stance(_)) => Ordering::Greater,
521            (BuffIconKind::Stance(_), BuffIconKind::Buff { .. }) => Ordering::Less,
522            (BuffIconKind::Stance(stance), BuffIconKind::Stance(stance_other)) => {
523                stance.cmp(stance_other)
524            },
525        }
526    }
527}
528
529impl PartialEq for BuffIconKind {
530    fn eq(&self, other: &Self) -> bool {
531        match (self, other) {
532            (
533                BuffIconKind::Buff { kind, .. },
534                BuffIconKind::Buff {
535                    kind: other_kind, ..
536                },
537            ) => kind == other_kind,
538            (BuffIconKind::Stance(stance), BuffIconKind::Stance(stance_other)) => {
539                stance == stance_other
540            },
541            _ => false,
542        }
543    }
544}
545
546impl Eq for BuffIconKind {}
547
548#[derive(Clone, Copy, Debug)]
549pub struct BuffIcon {
550    kind: BuffIconKind,
551    is_buff: bool,
552    end_time: Option<f64>,
553}
554
555impl BuffIcon {
556    pub fn multiplicity(&self) -> usize {
557        match self.kind {
558            BuffIconKind::Buff { multiplicity, .. } => multiplicity,
559            BuffIconKind::Stance(_) => 1,
560        }
561    }
562
563    pub fn get_buff_time(&self, time: Time) -> String {
564        if let Some(end) = self.end_time {
565            format!("{:.0}s", end - time.0)
566        } else {
567            "".to_string()
568        }
569    }
570
571    pub fn icons_vec(buffs: &comp::Buffs, stance: Option<&comp::Stance>) -> Vec<Self> {
572        buffs
573            .iter_active()
574            .filter_map(BuffIcon::from_buffs)
575            .chain(stance.and_then(BuffIcon::from_stance))
576            .collect::<Vec<_>>()
577    }
578
579    fn from_stance(stance: &comp::Stance) -> Option<Self> {
580        let stance = if let Stance::None = stance {
581            return None;
582        } else {
583            stance
584        };
585        Some(BuffIcon {
586            kind: BuffIconKind::Stance(*stance),
587            is_buff: true,
588            end_time: None,
589        })
590    }
591
592    fn from_buffs<'b, I: Iterator<Item = &'b comp::Buff>>(buffs: I) -> Option<Self> {
593        let (buff, count) = buffs.fold((None, 0), |(strongest, count), buff| {
594            (strongest.or(Some(buff)), count + 1)
595        });
596        let buff = buff?;
597        Some(Self {
598            kind: BuffIconKind::Buff {
599                kind: buff.kind,
600                data: buff.data,
601                multiplicity: count,
602            },
603            is_buff: buff.kind.is_buff(),
604            end_time: buff.end_time.map(|end| end.0),
605        })
606    }
607}
608
609pub struct ExpFloater {
610    pub owner: Uid,
611    pub exp_change: u32,
612    pub timer: f32,
613    pub jump_timer: f32,
614    pub rand_offset: (f32, f32),
615    pub xp_pools: HashSet<SkillGroupKind>,
616}
617
618pub struct SkillPointGain {
619    pub skill_tree: SkillGroupKind,
620    pub total_points: u16,
621    pub timer: f32,
622}
623
624#[derive(Debug, Clone, Copy)]
625pub struct ComboFloater {
626    pub combo: u32,
627    pub timer: f64,
628}
629
630pub struct BlockFloater {
631    pub timer: f32,
632}
633
634pub struct DebugInfo {
635    pub tps: f64,
636    pub frame_time: Duration,
637    pub frame_variance: Duration,
638    pub ping_ms: f64,
639    pub coordinates: Option<comp::Pos>,
640    pub velocity: Option<comp::Vel>,
641    pub ori: Option<comp::Ori>,
642    pub character_state: Option<comp::CharacterState>,
643    pub look_dir: Dir,
644    pub in_fluid: Option<comp::Fluid>,
645    pub num_chunks: u32,
646    pub num_lights: u32,
647    pub num_visible_chunks: u32,
648    pub num_shadow_chunks: u32,
649    pub num_figures: u32,
650    pub num_figures_visible: u32,
651    pub num_particles: u32,
652    pub num_particles_visible: u32,
653    pub current_track: String,
654    pub current_artist: String,
655    pub active_channels: ActiveChannels,
656    pub audio_cpu_usage: f32,
657}
658
659pub struct HudInfo<'a> {
660    pub is_aiming: bool,
661    pub active_mine_tool: Option<ToolKind>,
662    pub is_first_person: bool,
663    pub viewpoint_entity: specs::Entity,
664    pub mutable_viewpoint: bool,
665    pub target_entity: Option<specs::Entity>,
666    pub selected_entity: Option<(specs::Entity, Instant)>,
667    pub persistence_load_error: Option<SkillsPersistenceError>,
668    pub key_state: &'a KeyState,
669}
670
671#[derive(Clone)]
672pub enum Event {
673    SendMessage(String),
674    SendCommand(String, Vec<String>),
675
676    CharacterSelection,
677    UseSlot {
678        slot: comp::slot::Slot,
679        bypass_dialog: bool,
680    },
681    SwapEquippedWeapons,
682    SwapSlots {
683        slot_a: comp::slot::Slot,
684        slot_b: comp::slot::Slot,
685        bypass_dialog: bool,
686    },
687    SplitSwapSlots {
688        slot_a: comp::slot::Slot,
689        slot_b: comp::slot::Slot,
690        bypass_dialog: bool,
691    },
692    DropSlot(comp::slot::Slot),
693    SplitDropSlot(comp::slot::Slot),
694    SortInventory(InventorySortOrder),
695    ChangeHotbarState(Box<HotbarState>),
696    TradeAction(TradeAction),
697    Ability {
698        idx: usize,
699        state: bool,
700    },
701    Logout,
702    Quit,
703
704    CraftRecipe {
705        recipe_name: String,
706        craft_sprite: Option<(VolumePos, SpriteKind)>,
707        amount: u32,
708    },
709    SalvageItem {
710        slot: InvSlotId,
711        salvage_pos: VolumePos,
712    },
713    CraftModularWeapon {
714        primary_slot: InvSlotId,
715        secondary_slot: InvSlotId,
716        craft_sprite: Option<VolumePos>,
717    },
718    CraftModularWeaponComponent {
719        toolkind: ToolKind,
720        material: InvSlotId,
721        modifier: Option<InvSlotId>,
722        craft_sprite: Option<VolumePos>,
723    },
724    RepairItem {
725        item: Slot,
726        sprite_pos: VolumePos,
727    },
728    InviteMember(Uid),
729    AcceptInvite,
730    DeclineInvite,
731    KickMember(Uid),
732    LeaveGroup,
733    AssignLeader(Uid),
734    RemoveBuff(BuffKind),
735    LeaveStance,
736    UnlockSkill(Skill),
737    SelectExpBar(Option<SkillGroupKind>),
738
739    RequestSiteInfo(SiteId),
740    ChangeAbility(usize, AuxiliaryAbility),
741
742    SettingsChange(SettingsChange),
743    AcknowledgePersistenceLoadError,
744    MapMarkerEvent(MapMarkerChange),
745    Dialogue(EcsEntity, rtsim::Dialogue),
746    SetBattleMode(BattleMode),
747}
748
749// TODO: Are these the possible layouts we want?
750// TODO: Maybe replace this with bitflags.
751// `map` is not here because it currently is displayed over the top of other
752// open windows.
753#[derive(PartialEq, Eq)]
754pub enum Windows {
755    Settings, // Display settings window.
756    None,
757}
758
759#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
760pub enum CrosshairType {
761    RoundEdges,
762    Edges,
763    #[serde(other)]
764    Round,
765}
766#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
767pub enum Intro {
768    Never,
769    #[serde(other)]
770    Show,
771}
772#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
773pub enum XpBar {
774    OnGain,
775    #[serde(other)]
776    Always,
777}
778
779#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
780pub enum BarNumbers {
781    Percent,
782    Off,
783    #[serde(other)]
784    Values,
785}
786#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
787pub enum ShortcutNumbers {
788    Off,
789    #[serde(other)]
790    On,
791}
792
793#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
794pub enum BuffPosition {
795    Map,
796    #[serde(other)]
797    Bar,
798}
799
800#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
801pub enum PressBehavior {
802    Hold = 1,
803    #[serde(other)]
804    Toggle = 0,
805}
806/// Similar to [PressBehavior], with different semantics for settings that
807/// change state automatically. There is no [PressBehavior::update]
808/// implementation because it doesn't apply to the use case; this is just a
809/// sentinel.
810#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
811pub enum AutoPressBehavior {
812    Auto = 1,
813    #[serde(other)]
814    Toggle = 0,
815}
816#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
817pub struct ChatTab {
818    pub label: String,
819    pub filter: ChatFilter,
820}
821impl Default for ChatTab {
822    fn default() -> Self {
823        Self {
824            label: String::from("Chat"),
825            filter: ChatFilter::default(),
826        }
827    }
828}
829
830impl PressBehavior {
831    pub fn update(&self, keystate: bool, setting: &mut bool, f: impl FnOnce(bool)) {
832        match (self, keystate) {
833            // flip the state on key press in toggle mode
834            (PressBehavior::Toggle, true) => {
835                *setting ^= true;
836                f(*setting);
837            },
838            // do nothing on key release in toggle mode
839            (PressBehavior::Toggle, false) => {},
840            // set the setting to the key state in hold mode
841            (PressBehavior::Hold, state) => {
842                *setting = state;
843                f(*setting);
844            },
845        }
846    }
847}
848
849#[derive(Default, Clone)]
850pub struct MapMarkers {
851    owned: Option<Vec2<i32>>,
852    group: HashMap<Uid, Vec2<i32>>,
853}
854
855impl MapMarkers {
856    pub fn update(&mut self, event: comp::MapMarkerUpdate) {
857        match event {
858            comp::MapMarkerUpdate::Owned(event) => match event {
859                MapMarkerChange::Update(waypoint) => self.owned = Some(waypoint),
860                MapMarkerChange::Remove => self.owned = None,
861            },
862            comp::MapMarkerUpdate::GroupMember(user, event) => match event {
863                MapMarkerChange::Update(waypoint) => {
864                    self.group.insert(user, waypoint);
865                },
866                MapMarkerChange::Remove => {
867                    self.group.remove(&user);
868                },
869            },
870            comp::MapMarkerUpdate::ClearGroup => {
871                self.group.clear();
872            },
873        }
874    }
875}
876
877/// (target slot, input value, inventory quantity, is our inventory, error,
878/// trade.offers index of trade slot)
879pub struct TradeAmountInput {
880    slot: InvSlotId,
881    input: String,
882    inv: u32,
883    ours: bool,
884    err: Option<String>,
885    who: usize,
886    input_painted: bool,
887    submit_action: Option<TradeAction>,
888}
889
890impl TradeAmountInput {
891    pub fn new(slot: InvSlotId, input: String, inv: u32, ours: bool, who: usize) -> Self {
892        Self {
893            slot,
894            input,
895            inv,
896            ours,
897            who,
898            err: None,
899            input_painted: false,
900            submit_action: None,
901        }
902    }
903}
904
905#[derive(Debug, Clone, Copy, PartialEq, Eq)]
906pub enum WindowId {
907    None,
908    Bag,
909}
910
911pub struct Show {
912    ui: bool,
913    intro: bool,
914    crafting: bool,
915    bag: bool,
916    bag_menu_split: bool,
917    bag_details: bool,
918    trade: bool,
919    trade_details: bool,
920    social: bool,
921    diary: bool,
922    group: bool,
923    quest: bool,
924    group_menu: bool,
925    esc_menu: bool,
926    open_windows: Windows,
927    map: bool,
928    ingame: bool,
929    chat_tab_settings_index: Option<usize>,
930    settings_tab: SettingsTab,
931    diary_fields: diary::DiaryShow,
932    crafting_fields: crafting::CraftingShow,
933    social_search_key: Option<String>,
934    want_grab: bool,
935    stats: bool,
936    free_look: bool,
937    auto_walk: bool,
938    zoom_lock: ChangeNotification,
939    camera_clamp: bool,
940    prompt_dialog: Option<PromptDialogSettings>,
941    trade_amount_input_key: Option<TradeAmountInput>,
942    // A stack of open menus; the menu in focus should be on top
943    focus: Vec<WindowId>,
944}
945
946impl Default for Show {
947    fn default() -> Self { Self::new() }
948}
949
950impl Show {
951    pub fn new() -> Self {
952        Self {
953            ui: true,
954            intro: false,
955            crafting: false,
956            bag: false,
957            bag_menu_split: false,
958            bag_details: false,
959            trade: false,
960            trade_details: false,
961            social: false,
962            diary: false,
963            group: false,
964            quest: false,
965            group_menu: false,
966            esc_menu: false,
967            open_windows: Windows::None,
968            map: false,
969            ingame: true,
970            chat_tab_settings_index: None,
971            settings_tab: SettingsTab::Interface,
972            diary_fields: diary::DiaryShow::default(),
973            crafting_fields: crafting::CraftingShow::default(),
974            social_search_key: None,
975            want_grab: true,
976            stats: false,
977            free_look: false,
978            auto_walk: false,
979            zoom_lock: ChangeNotification::default(),
980            camera_clamp: false,
981            prompt_dialog: None,
982            trade_amount_input_key: None,
983            focus: Vec::new(),
984        }
985    }
986
987    // Changing a window state must go through these functions
988    fn set_bag_state(&mut self, state: bool) {
989        if state {
990            self.focus.push(WindowId::Bag); // use hashset to avoid duplicates?
991            self.bag = true;
992        } else {
993            self.focus.retain(|x| *x != WindowId::Bag);
994            self.bag = false;
995        }
996    }
997
998    fn bag(&mut self, open: bool) {
999        if !self.esc_menu {
1000            self.set_bag_state(open);
1001            self.map = false;
1002            self.crafting_fields.salvage = false;
1003
1004            if !open {
1005                self.crafting = false;
1006            }
1007
1008            self.want_grab = !self.any_window_requires_cursor();
1009        }
1010    }
1011
1012    pub fn bag_print(&self) -> bool { self.bag }
1013
1014    fn trade(&mut self, open: bool) {
1015        if !self.esc_menu {
1016            self.set_bag_state(open);
1017            self.trade = open;
1018            self.map = false;
1019            self.want_grab = !self.any_window_requires_cursor();
1020        }
1021    }
1022
1023    fn map(&mut self, open: bool) {
1024        if !self.esc_menu {
1025            self.map = open;
1026            self.set_bag_state(false);
1027            self.crafting = false;
1028            self.crafting_fields.salvage = false;
1029            self.social = false;
1030            self.quest = false;
1031            self.diary = false;
1032            self.want_grab = !self.any_window_requires_cursor();
1033        }
1034    }
1035
1036    fn social(&mut self, open: bool) {
1037        if !self.esc_menu {
1038            if !self.social && open {
1039                // rising edge detector
1040                self.search_social_players(None);
1041            }
1042            self.social = open;
1043            self.diary = false;
1044            self.want_grab = !self.any_window_requires_cursor();
1045        }
1046    }
1047
1048    fn quest(&mut self, open: bool) {
1049        if !self.esc_menu {
1050            self.quest = open;
1051            self.diary = false;
1052            self.map = false;
1053            self.want_grab = !self.any_window_requires_cursor();
1054        }
1055    }
1056
1057    fn crafting(&mut self, open: bool) {
1058        if !self.esc_menu {
1059            if !self.crafting && open {
1060                // rising edge detector
1061                self.search_crafting_recipe(None);
1062            }
1063            self.crafting = open;
1064            self.crafting_fields.salvage = false;
1065            self.crafting_fields.recipe_inputs = HashMap::new();
1066            self.set_bag_state(open);
1067            self.map = false;
1068            self.want_grab = !self.any_window_requires_cursor();
1069        }
1070    }
1071
1072    pub fn open_crafting_tab(
1073        &mut self,
1074        tab: CraftingTab,
1075        craft_sprite: Option<(VolumePos, SpriteKind)>,
1076    ) {
1077        self.selected_crafting_tab(tab);
1078        self.crafting(true);
1079        self.crafting_fields.craft_sprite = self.crafting_fields.craft_sprite.or(craft_sprite);
1080        self.crafting_fields.salvage = matches!(
1081            self.crafting_fields.craft_sprite,
1082            Some((_, SpriteKind::DismantlingBench))
1083        ) && matches!(tab, CraftingTab::Dismantle);
1084        self.crafting_fields.initialize_repair = matches!(
1085            self.crafting_fields.craft_sprite,
1086            Some((_, SpriteKind::RepairBench))
1087        );
1088    }
1089
1090    fn diary(&mut self, open: bool) {
1091        if !self.esc_menu {
1092            self.social = false;
1093            self.quest = false;
1094            self.crafting = false;
1095            self.crafting_fields.salvage = false;
1096            self.set_bag_state(false);
1097            self.map = false;
1098            self.diary_fields = diary::DiaryShow::default();
1099            self.diary = open;
1100            self.want_grab = !self.any_window_requires_cursor();
1101        }
1102    }
1103
1104    fn settings(&mut self, open: bool) {
1105        if !self.esc_menu {
1106            self.open_windows = if open {
1107                Windows::Settings
1108            } else {
1109                Windows::None
1110            };
1111            self.set_bag_state(false);
1112            self.social = false;
1113            self.quest = false;
1114            self.crafting = false;
1115            self.crafting_fields.salvage = false;
1116            self.diary = false;
1117            self.want_grab = !self.any_window_requires_cursor();
1118        }
1119    }
1120
1121    fn toggle_trade(&mut self) { self.trade(!self.trade); }
1122
1123    fn toggle_map(&mut self) { self.map(!self.map) }
1124
1125    fn toggle_social(&mut self) { self.social(!self.social); }
1126
1127    fn toggle_crafting(&mut self) { self.crafting(!self.crafting) }
1128
1129    fn toggle_diary(&mut self) { self.diary(!self.diary) }
1130
1131    fn toggle_ui(&mut self) { self.ui = !self.ui; }
1132
1133    fn toggle_settings(&mut self, global_state: &GlobalState) {
1134        match self.open_windows {
1135            Windows::Settings => {
1136                #[cfg(feature = "singleplayer")]
1137                global_state.unpause();
1138
1139                self.settings(false);
1140            },
1141            _ => {
1142                #[cfg(feature = "singleplayer")]
1143                global_state.pause();
1144
1145                self.settings(true)
1146            },
1147        };
1148        #[cfg(not(feature = "singleplayer"))]
1149        let _global_state = global_state;
1150    }
1151
1152    // TODO: Add self updating key-bindings element
1153
1154    fn any_window_requires_cursor(&self) -> bool {
1155        self.bag
1156            || self.trade
1157            || self.esc_menu
1158            || self.map
1159            || self.social
1160            || self.crafting
1161            || self.diary
1162            || self.intro
1163            || self.quest
1164            || !matches!(self.open_windows, Windows::None)
1165    }
1166
1167    fn toggle_windows(&mut self, global_state: &mut GlobalState) {
1168        if self.any_window_requires_cursor() {
1169            self.set_bag_state(false);
1170            self.trade = false;
1171            self.esc_menu = false;
1172            self.intro = false;
1173            self.map = false;
1174            self.social = false;
1175            self.quest = false;
1176            self.diary = false;
1177            self.crafting = false;
1178            self.open_windows = Windows::None;
1179            self.want_grab = true;
1180
1181            // Unpause the game if we are on singleplayer
1182            #[cfg(feature = "singleplayer")]
1183            global_state.unpause();
1184        } else {
1185            self.esc_menu = true;
1186            self.want_grab = false;
1187
1188            // Pause the game if we are on singleplayer
1189            #[cfg(feature = "singleplayer")]
1190            global_state.pause();
1191        }
1192        #[cfg(not(feature = "singleplayer"))]
1193        let _global_state = global_state;
1194    }
1195
1196    fn open_setting_tab(&mut self, tab: SettingsTab) {
1197        self.open_windows = Windows::Settings;
1198        self.esc_menu = false;
1199        self.settings_tab = tab;
1200        self.set_bag_state(false);
1201        self.want_grab = false;
1202    }
1203
1204    fn open_skill_tree(&mut self, tree_sel: SelectedSkillTree) {
1205        self.diary_fields.skilltreetab = tree_sel;
1206        self.social = false;
1207    }
1208
1209    fn selected_crafting_tab(&mut self, sel_cat: CraftingTab) {
1210        self.crafting_fields.crafting_tab = sel_cat;
1211    }
1212
1213    fn search_crafting_recipe(&mut self, search_key: Option<String>) {
1214        self.crafting_fields.crafting_search_key = search_key;
1215    }
1216
1217    fn search_social_players(&mut self, search_key: Option<String>) {
1218        self.social_search_key = search_key;
1219    }
1220}
1221
1222pub struct PromptDialogSettings {
1223    message: String,
1224    affirmative_event: Event,
1225    negative_option: bool,
1226    negative_event: Option<Event>,
1227    outcome_via_keypress: Option<bool>,
1228}
1229
1230impl PromptDialogSettings {
1231    pub fn new(message: String, affirmative_event: Event, negative_event: Option<Event>) -> Self {
1232        Self {
1233            message,
1234            affirmative_event,
1235            negative_option: true,
1236            negative_event,
1237            outcome_via_keypress: None,
1238        }
1239    }
1240
1241    pub fn set_outcome_via_keypress(&mut self, outcome: bool) {
1242        self.outcome_via_keypress = Some(outcome);
1243    }
1244
1245    #[must_use]
1246    pub fn with_no_negative_option(mut self) -> Self {
1247        self.negative_option = false;
1248        self
1249    }
1250}
1251
1252pub struct Floaters {
1253    pub exp_floaters: Vec<ExpFloater>,
1254    pub skill_point_displays: Vec<SkillPointGain>,
1255    pub combo_floater: Option<ComboFloater>,
1256    pub block_floaters: Vec<BlockFloater>,
1257}
1258
1259#[derive(Clone)]
1260pub enum HudLootOwner {
1261    Name(Content),
1262    Group,
1263    Unknown,
1264}
1265
1266#[derive(Clone)]
1267pub enum HudCollectFailedReason {
1268    InventoryFull,
1269    LootOwned {
1270        owner: HudLootOwner,
1271        expiry_secs: u64,
1272    },
1273}
1274
1275impl HudCollectFailedReason {
1276    pub fn from_server_reason(reason: &CollectFailedReason, ecs: &specs::World) -> Self {
1277        match reason {
1278            CollectFailedReason::InventoryFull => HudCollectFailedReason::InventoryFull,
1279            CollectFailedReason::LootOwned { owner, expiry_secs } => {
1280                let owner = match owner {
1281                    LootOwnerKind::Player(owner_uid) => {
1282                        let maybe_owner_name = ecs.entity_from_uid(*owner_uid).and_then(|entity| {
1283                            ecs.read_storage::<comp::Stats>()
1284                                .get(entity)
1285                                .map(|stats| stats.name.clone())
1286                        });
1287
1288                        if let Some(name) = maybe_owner_name {
1289                            HudLootOwner::Name(name)
1290                        } else {
1291                            HudLootOwner::Unknown
1292                        }
1293                    },
1294                    LootOwnerKind::Group(_) => HudLootOwner::Group,
1295                };
1296
1297                HudCollectFailedReason::LootOwned {
1298                    owner,
1299                    expiry_secs: *expiry_secs,
1300                }
1301            },
1302        }
1303    }
1304}
1305#[derive(Clone)]
1306pub struct CollectFailedData {
1307    pulse: f32,
1308    reason: HudCollectFailedReason,
1309}
1310
1311impl CollectFailedData {
1312    pub fn new(pulse: f32, reason: HudCollectFailedReason) -> Self { Self { pulse, reason } }
1313}
1314
1315/// Stores HUD related state which should be persisted even if the HUD is
1316/// temporarily hidden (by ie. going to the character screen).
1317#[derive(Default)]
1318pub struct PersistedHudState {
1319    /// Stores messages sent while the chat is hidden (either disabled in the
1320    /// HUD state, or by being outside of the HUD).
1321    ///
1322    /// This is needed because messages in [`Hud::new_messages`] are also shown
1323    /// as new chat bubbles, so `new_messages` must be cleared every frame.
1324    pub message_backlog: MessageBacklog,
1325    pub location_markers: MapMarkers,
1326}
1327
1328pub struct Hud {
1329    ui: Ui,
1330    ids: Ids,
1331    world_map: (/* Id */ Vec<Rotations>, Vec2<u32>),
1332    imgs: Imgs,
1333    item_imgs: ItemImgs,
1334    item_i18n: ItemI18n,
1335    fonts: Fonts,
1336    rot_imgs: ImgsRot,
1337    failed_block_pickups: HashMap<VolumePos, CollectFailedData>,
1338    failed_entity_pickups: HashMap<EcsEntity, CollectFailedData>,
1339    new_loot_messages: VecDeque<LootMessage>,
1340    new_messages: VecDeque<comp::ChatMsg>,
1341    new_notifications: VecDeque<UserNotification>,
1342    speech_bubbles: HashMap<Uid, comp::SpeechBubble>,
1343    content_bubbles: Vec<(Vec3<f32>, comp::SpeechBubble)>,
1344    pub persisted_state: Rc<RefCell<PersistedHudState>>,
1345    pub show: Show,
1346    to_focus: Option<Option<widget::Id>>,
1347    force_ungrab: bool,
1348    force_chat_input: Option<String>,
1349    force_chat_cursor: Option<Index>,
1350    tab_complete: Option<String>,
1351    pulse: f32,
1352    hp_pulse: f32,
1353    slot_manager: slots::SlotManager,
1354    hotbar: hotbar::State,
1355    events: Vec<Event>,
1356    menu_events: Vec<MenuInput>,
1357    crosshair_opacity: f32,
1358    floaters: Floaters,
1359    voxel_minimap: VoxelMinimap,
1360    map_drag: Vec2<f64>,
1361    force_chat: bool,
1362    clear_chat: bool,
1363    current_dialogue: Option<(EcsEntity, Instant, rtsim::Dialogue<true>)>,
1364    extra_markers: Vec<map::ExtraMarker>,
1365}
1366
1367impl Hud {
1368    pub fn new(
1369        global_state: &mut GlobalState,
1370        persisted_state: Rc<RefCell<PersistedHudState>>,
1371        client: &Client,
1372    ) -> Self {
1373        let window = &mut global_state.window;
1374        let settings = &global_state.settings;
1375
1376        let mut ui = Ui::new(window).unwrap();
1377        ui.set_scaling_mode(settings.interface.ui_scale);
1378        // Generate ids.
1379        let ids = Ids::new(ui.id_generator());
1380        // Load world map
1381        let mut layers = Vec::new();
1382        for layer in client.world_data().map_layers() {
1383            // NOTE: Use a border the same color as the LOD ocean color (but with a
1384            // translucent alpha since UI have transparency and LOD doesn't).
1385            layers.push(ui.add_graphic_with_rotations(Graphic::Image(
1386                Arc::clone(layer),
1387                Some(default_water_color()),
1388            )));
1389        }
1390        let world_map = (layers, client.world_data().chunk_size().map(|e| e as u32));
1391        // Load images.
1392        let imgs = Imgs::load(&mut ui).expect("Failed to load images!");
1393        // Load rotation images.
1394        let rot_imgs = ImgsRot::load(&mut ui).expect("Failed to load rot images!");
1395        // Load item images.
1396        let item_imgs = ItemImgs::new(&mut ui, imgs.not_found);
1397        // Load item text ("reference" to name and description)
1398        let item_i18n = ItemI18n::new_expect();
1399        // Load fonts.
1400        let fonts = Fonts::load(global_state.i18n.read().fonts(), &mut ui)
1401            .expect("Impossible to load fonts!");
1402        // Get the server name.
1403        let server = &client.server_info().name;
1404        // Get the id, unwrap is safe because this CANNOT be None at this
1405        // point.
1406
1407        let character_id = match client.presence().unwrap() {
1408            PresenceKind::Character(id) => Some(id),
1409            PresenceKind::LoadingCharacter(id) => Some(id),
1410            PresenceKind::Spectator => None,
1411            PresenceKind::Possessor => None,
1412        };
1413
1414        // Create a new HotbarState from the persisted slots.
1415        let hotbar_state =
1416            HotbarState::new(global_state.profile.get_hotbar_slots(server, character_id));
1417
1418        let slot_manager = slots::SlotManager::new(
1419            ui.id_generator(),
1420            Vec2::broadcast(40.0),
1421            global_state.settings.interface.slots_use_prefixes,
1422            global_state.settings.interface.slots_prefix_switch_point,
1423            // TODO(heyzoos) Will be useful for whoever works on rendering the number of items
1424            // "in hand".
1425            // fonts.cyri.conrod_id,
1426            // Vec2::new(1.0, 1.0),
1427            // fonts.cyri.scale(12),
1428            // TEXT_COLOR,
1429        );
1430
1431        Self {
1432            voxel_minimap: VoxelMinimap::new(&mut ui),
1433            ui,
1434            imgs,
1435            world_map,
1436            rot_imgs,
1437            item_imgs,
1438            item_i18n,
1439            fonts,
1440            ids,
1441            failed_block_pickups: HashMap::default(),
1442            failed_entity_pickups: HashMap::default(),
1443            new_loot_messages: VecDeque::new(),
1444            new_messages: VecDeque::new(),
1445            new_notifications: VecDeque::new(),
1446            persisted_state,
1447            speech_bubbles: HashMap::new(),
1448            content_bubbles: Vec::new(),
1449            show: Show::new(),
1450            to_focus: None,
1451            force_ungrab: false,
1452            force_chat_input: None,
1453            force_chat_cursor: None,
1454            tab_complete: None,
1455            pulse: 0.0,
1456            hp_pulse: 0.0,
1457            slot_manager,
1458            hotbar: hotbar_state,
1459            events: Vec::new(),
1460            menu_events: Vec::new(),
1461            crosshair_opacity: 0.0,
1462            floaters: Floaters {
1463                exp_floaters: Vec::new(),
1464                skill_point_displays: Vec::new(),
1465                combo_floater: None,
1466                block_floaters: Vec::new(),
1467            },
1468            map_drag: Vec2::zero(),
1469            force_chat: false,
1470            clear_chat: false,
1471            current_dialogue: None,
1472            extra_markers: Vec::new(),
1473        }
1474    }
1475
1476    pub fn clear_chat(&mut self) { self.clear_chat = true; }
1477
1478    pub fn set_prompt_dialog(&mut self, prompt_dialog: PromptDialogSettings) {
1479        self.show.prompt_dialog = Some(prompt_dialog);
1480    }
1481
1482    pub fn update_fonts(&mut self, i18n: &Localization) {
1483        self.fonts = Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!");
1484    }
1485
1486    pub fn set_slots_use_prefixes(&mut self, use_prefixes: bool) {
1487        self.slot_manager.set_use_prefixes(use_prefixes);
1488    }
1489
1490    pub fn set_slots_prefix_switch_point(&mut self, prefix_switch_point: u32) {
1491        self.slot_manager
1492            .set_prefix_switch_point(prefix_switch_point);
1493    }
1494
1495    pub fn current_dialogue(&self) -> Option<EcsEntity> {
1496        self.current_dialogue.as_ref().map(|(e, _, _)| *e)
1497    }
1498
1499    #[expect(clippy::single_match)] // TODO: Pending review in #587
1500    fn update_layout(
1501        &mut self,
1502        client: &Client,
1503        global_state: &mut GlobalState,
1504        debug_info: &Option<DebugInfo>,
1505        dt: Duration,
1506        info: HudInfo,
1507        camera: &Camera,
1508        (entity_interactables, block_interactables): (
1509            HashMap<specs::Entity, Vec<interactable::EntityInteraction>>,
1510            HashMap<VolumePos, (Block, Vec<&interactable::BlockInteraction>)>,
1511        ),
1512    ) -> Vec<Event> {
1513        span!(_guard, "update_layout", "Hud::update_layout");
1514        let mut events = core::mem::take(&mut self.events);
1515        if global_state.settings.interface.map_show_voxel_map {
1516            self.voxel_minimap.maintain(client, &mut self.ui);
1517        }
1518        let scale = self.ui.scale();
1519        let (ui_widgets, item_tooltip_manager, tooltip_manager) = &mut self.ui.set_widgets();
1520        // self.ui.set_item_widgets(); pulse time for pulsating elements
1521        self.pulse += dt.as_secs_f32();
1522        // FPS
1523        let fps = global_state.clock.stats().average_tps;
1524        let version = format!("Veloren {}", *common::util::DISPLAY_VERSION);
1525        let i18n = &global_state.i18n.read();
1526
1527        if self.show.ingame {
1528            prof_span!("ingame elements");
1529
1530            let ecs = client.state().ecs();
1531            let pos = ecs.read_storage::<comp::Pos>();
1532            let stats = ecs.read_storage::<comp::Stats>();
1533            let skill_sets = ecs.read_storage::<comp::SkillSet>();
1534            let healths = ecs.read_storage::<Health>();
1535            let hardcore = ecs.read_storage::<comp::Hardcore>();
1536            let buffs = ecs.read_storage::<comp::Buffs>();
1537            let energy = ecs.read_storage::<comp::Energy>();
1538            let mut hp_floater_lists = ecs.write_storage::<HpFloaterList>();
1539            let uids = ecs.read_storage::<Uid>();
1540            let interpolated = ecs.read_storage::<vcomp::Interpolated>();
1541            let scales = ecs.read_storage::<comp::Scale>();
1542            let bodies = ecs.read_storage::<comp::Body>();
1543            let items = ecs.read_storage::<PickupItem>();
1544            let inventories = ecs.read_storage::<comp::Inventory>();
1545            let msm = ecs.read_resource::<MaterialStatManifest>();
1546            let entities = ecs.entities();
1547            let me = info.viewpoint_entity;
1548            let poises = ecs.read_storage::<comp::Poise>();
1549            let is_mounts = ecs.read_storage::<Is<Mount>>();
1550            let is_riders = ecs.read_storage::<Is<Rider>>();
1551            let stances = ecs.read_storage::<comp::Stance>();
1552            let char_activities = ecs.read_storage::<comp::CharacterActivity>();
1553            let time = ecs.read_resource::<Time>();
1554            let id_maps = ecs.read_resource::<common::uid::IdMaps>();
1555            let terrain = ecs.read_resource::<common::terrain::TerrainGrid>();
1556            let colliders = ecs.read_storage::<comp::Collider>();
1557            let char_states = ecs.read_storage::<comp::CharacterState>();
1558
1559            // Check if there was a persistence load error of the skillset, and if so
1560            // display a dialog prompt
1561            if self.show.prompt_dialog.is_none()
1562                && let Some(persistence_error) = info.persistence_load_error
1563            {
1564                let persistence_error = match persistence_error {
1565                    SkillsPersistenceError::HashMismatch => "hud-skill-persistence-hash_mismatch",
1566                    SkillsPersistenceError::DeserializationFailure => {
1567                        "hud-skill-persistence-deserialization_failure"
1568                    },
1569                    SkillsPersistenceError::SpentExpMismatch => {
1570                        "hud-skill-persistence-spent_experience_missing"
1571                    },
1572                    SkillsPersistenceError::SkillsUnlockFailed => {
1573                        "hud-skill-persistence-skills_unlock_failed"
1574                    },
1575                };
1576                let persistence_error = global_state
1577                    .i18n
1578                    .read()
1579                    .get_content(&Content::localized(persistence_error));
1580
1581                let common_message = global_state
1582                    .i18n
1583                    .read()
1584                    .get_content(&Content::localized("hud-skill-persistence-common_message"));
1585
1586                warn!("{}\n{}", persistence_error, common_message);
1587                // TODO: Let the player see the more detailed message `persistence_error`?
1588                let prompt_dialog = PromptDialogSettings::new(
1589                    format!("{}\n", common_message),
1590                    Event::AcknowledgePersistenceLoadError,
1591                    None,
1592                )
1593                .with_no_negative_option();
1594                // self.set_prompt_dialog(prompt_dialog);
1595                self.show.prompt_dialog = Some(prompt_dialog);
1596            }
1597
1598            if (client.pending_trade().is_some() && !self.show.trade)
1599                || (client.pending_trade().is_none() && self.show.trade)
1600            {
1601                self.show.toggle_trade();
1602            }
1603
1604            //self.input = client.read_storage::<comp::ControllerInputs>();
1605            if let Some(health) = healths.get(me) {
1606                // Hurt Frame
1607                let hp_percentage = health.current() / health.maximum() * 100.0;
1608                self.hp_pulse += dt.as_secs_f32() * 10.0 / hp_percentage.clamp(3.0, 7.0);
1609                if hp_percentage < 10.0 && !health.is_dead {
1610                    let hurt_fade = (self.hp_pulse).sin() * 0.5 + 0.6; //Animation timer
1611                    Image::new(self.imgs.hurt_bg)
1612                        .wh_of(ui_widgets.window)
1613                        .middle_of(ui_widgets.window)
1614                        .graphics_for(ui_widgets.window)
1615                        .color(Some(Color::Rgba(1.0, 1.0, 1.0, hurt_fade)))
1616                        .set(self.ids.hurt_bg, ui_widgets);
1617                }
1618
1619                // Version info
1620                Text::new(&version)
1621                    .font_id(self.fonts.cyri.conrod_id)
1622                    .font_size(self.fonts.cyri.scale(11))
1623                    .color(TEXT_COLOR)
1624                    .mid_top_with_margin_on(ui_widgets.window, 2.0)
1625                    .set(self.ids.version, ui_widgets);
1626
1627                // Death Frame
1628                if health.is_dead {
1629                    Image::new(self.imgs.death_bg)
1630                        .wh_of(ui_widgets.window)
1631                        .middle_of(ui_widgets.window)
1632                        .graphics_for(ui_widgets.window)
1633                        .color(Some(Color::Rgba(0.0, 0.0, 0.0, 1.0)))
1634                        .set(self.ids.death_bg, ui_widgets);
1635                }
1636                // Crosshair
1637                let show_crosshair = (info.is_aiming || info.is_first_person) && !health.is_dead;
1638                self.crosshair_opacity = Lerp::lerp(
1639                    self.crosshair_opacity,
1640                    if show_crosshair { 1.0 } else { 0.0 },
1641                    5.0 * dt.as_secs_f32(),
1642                );
1643
1644                Image::new(
1645                    // TODO: Do we want to match on this every frame?
1646                    match global_state.settings.interface.crosshair_type {
1647                        CrosshairType::Round => self.imgs.crosshair_outer_round,
1648                        CrosshairType::RoundEdges => self.imgs.crosshair_outer_round_edges,
1649                        CrosshairType::Edges => self.imgs.crosshair_outer_edges,
1650                    },
1651                )
1652                .w_h(21.0 * 1.5, 21.0 * 1.5)
1653                .middle_of(ui_widgets.window)
1654                .color(Some(Color::Rgba(
1655                    1.0,
1656                    1.0,
1657                    1.0,
1658                    self.crosshair_opacity * global_state.settings.interface.crosshair_opacity,
1659                )))
1660                .set(self.ids.crosshair_outer, ui_widgets);
1661                Image::new(self.imgs.crosshair_inner)
1662                    .w_h(21.0 * 2.0, 21.0 * 2.0)
1663                    .middle_of(self.ids.crosshair_outer)
1664                    .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.6)))
1665                    .set(self.ids.crosshair_inner, ui_widgets);
1666
1667                if let Some(charge) = char_states.get(me).and_then(|cs| cs.charge_frac()) {
1668                    Image::new(match charge {
1669                        _ if charge > 0.999 => self.imgs.crosshair_charge_8,
1670                        _ if charge > 0.875 => self.imgs.crosshair_charge_7,
1671                        _ if charge > 0.75 => self.imgs.crosshair_charge_6,
1672                        _ if charge > 0.625 => self.imgs.crosshair_charge_5,
1673                        _ if charge > 0.5 => self.imgs.crosshair_charge_4,
1674                        _ if charge > 0.375 => self.imgs.crosshair_charge_3,
1675                        _ if charge > 0.25 => self.imgs.crosshair_charge_2,
1676                        _ if charge > 0.125 => self.imgs.crosshair_charge_1,
1677                        _ => self.imgs.crosshair_charge_0,
1678                    })
1679                    .w_h(21.0 * 1.5, 21.0 * 1.5)
1680                    .middle_of(ui_widgets.window)
1681                    .color(Some(Color::Rgba(
1682                        1.0,
1683                        1.0,
1684                        1.0,
1685                        self.crosshair_opacity * global_state.settings.interface.crosshair_opacity,
1686                    )))
1687                    .set(self.ids.crosshair_charge, ui_widgets);
1688                }
1689            }
1690
1691            // Max amount the sct font size increases when "flashing"
1692            const FLASH_MAX: u32 = 2;
1693
1694            // Get player position.
1695            let player_pos = client
1696                .state()
1697                .ecs()
1698                .read_storage::<comp::Pos>()
1699                .get(client.entity())
1700                .map_or(Vec3::zero(), |pos| pos.0);
1701            // SCT Output values are called hp_damage and floater.info.amount
1702            // Numbers are currently divided by 10 and rounded
1703            if global_state.settings.interface.sct {
1704                // Render Player SCT numbers
1705                let mut player_sct_bg_id_walker = self.ids.player_sct_bgs.walk();
1706                let mut player_sct_id_walker = self.ids.player_scts.walk();
1707                if let (Some(HpFloaterList { floaters, .. }), Some(health)) = (
1708                    hp_floater_lists
1709                        .get_mut(me)
1710                        .filter(|fl| !fl.floaters.is_empty()),
1711                    healths.get(me),
1712                ) {
1713                    let player_font_col = |precise: bool| {
1714                        if precise {
1715                            Rgb::new(1.0, 0.9, 0.0)
1716                        } else {
1717                            Rgb::new(1.0, 0.1, 0.0)
1718                        }
1719                    };
1720
1721                    fn calc_fade(floater: &HpFloater) -> f32 {
1722                        ((crate::ecs::sys::floater::MY_HP_SHOWTIME - floater.timer) * 0.25) + 0.2
1723                    }
1724
1725                    floaters.retain(|fl| calc_fade(fl) > 0.0);
1726
1727                    for floater in floaters {
1728                        let number_speed = 50.0; // Player number speed
1729                        let player_sct_bg_id = player_sct_bg_id_walker.next(
1730                            &mut self.ids.player_sct_bgs,
1731                            &mut ui_widgets.widget_id_generator(),
1732                        );
1733                        let player_sct_id = player_sct_id_walker.next(
1734                            &mut self.ids.player_scts,
1735                            &mut ui_widgets.widget_id_generator(),
1736                        );
1737                        // Clamp the amount so you don't have absurdly large damage numbers
1738                        let max_hp_frac = floater
1739                            .info
1740                            .amount
1741                            .abs()
1742                            .clamp(Health::HEALTH_EPSILON, health.maximum() * 1.25)
1743                            / health.maximum();
1744                        let hp_dmg_text = if floater.info.amount.abs() < 0.1 {
1745                            String::new()
1746                        } else if global_state.settings.interface.sct_damage_rounding
1747                            && floater.info.amount.abs() >= 1.0
1748                        {
1749                            format!("{:.0}", floater.info.amount.abs())
1750                        } else {
1751                            format!("{:.1}", floater.info.amount.abs())
1752                        };
1753                        let precise = floater.info.precise;
1754
1755                        // Timer sets text transparency
1756                        let hp_fade = calc_fade(floater);
1757
1758                        // Increase font size based on fraction of maximum health
1759                        // "flashes" by having a larger size in the first 100ms
1760                        let font_size =
1761                            30 + (if precise {
1762                                (max_hp_frac * 10.0) as u32 * 3 + 10
1763                            } else {
1764                                (max_hp_frac * 10.0) as u32 * 3
1765                            }) + if floater.jump_timer < 0.1 {
1766                                FLASH_MAX
1767                                    * (((1.0 - floater.jump_timer * 10.0)
1768                                        * 10.0
1769                                        * if precise { 1.25 } else { 1.0 })
1770                                        as u32)
1771                            } else {
1772                                0
1773                            };
1774                        let font_col = player_font_col(precise);
1775                        // Timer sets the widget offset
1776                        let y = if floater.info.amount < 0.0 {
1777                            floater.timer as f64
1778                                * number_speed
1779                                * floater.info.amount.signum() as f64
1780                                //* -1.0
1781                                + 300.0
1782                                - ui_widgets.win_h * 0.5
1783                        } else {
1784                            -(floater.timer as f64
1785                                * number_speed
1786                                * floater.info.amount.signum() as f64)
1787                                + 300.0
1788                                - ui_widgets.win_h * 0.5
1789                        };
1790                        // Healing is offset randomly
1791                        let x = if floater.info.amount < 0.0 {
1792                            0.0
1793                        } else {
1794                            (floater.rand as f64 - 0.5) * 0.08 * ui_widgets.win_w
1795                                + (0.03 * ui_widgets.win_w * (floater.rand as f64 - 0.5).signum())
1796                        };
1797                        Text::new(&hp_dmg_text)
1798                            .font_size(font_size)
1799                            .font_id(self.fonts.cyri.conrod_id)
1800                            .color(Color::Rgba(0.0, 0.0, 0.0, hp_fade))
1801                            .x_y(x, y - 3.0)
1802                            .set(player_sct_bg_id, ui_widgets);
1803                        Text::new(&hp_dmg_text)
1804                            .font_size(font_size)
1805                            .font_id(self.fonts.cyri.conrod_id)
1806                            .color(if floater.info.amount < 0.0 {
1807                                Color::Rgba(font_col.r, font_col.g, font_col.b, hp_fade)
1808                            } else {
1809                                Color::Rgba(0.1, 1.0, 0.1, hp_fade)
1810                            })
1811                            .x_y(x, y)
1812                            .set(player_sct_id, ui_widgets);
1813                    }
1814                }
1815                // EXP Numbers
1816                self.floaters.exp_floaters.iter_mut().for_each(|f| {
1817                    f.timer -= dt.as_secs_f32();
1818                    f.jump_timer += dt.as_secs_f32();
1819                });
1820                self.floaters.exp_floaters.retain(|f| f.timer > 0.0);
1821                for floater in self.floaters.exp_floaters.iter_mut() {
1822                    let number_speed = 50.0; // Number Speed for Single EXP
1823                    let player_sct_bg_id = player_sct_bg_id_walker.next(
1824                        &mut self.ids.player_sct_bgs,
1825                        &mut ui_widgets.widget_id_generator(),
1826                    );
1827                    let player_sct_id = player_sct_id_walker.next(
1828                        &mut self.ids.player_scts,
1829                        &mut ui_widgets.widget_id_generator(),
1830                    );
1831                    /*let player_sct_icon_id = player_sct_id_walker.next(
1832                        &mut self.ids.player_scts,
1833                        &mut ui_widgets.widget_id_generator(),
1834                    );*/
1835                    // Increase font size based on fraction of maximum Experience
1836                    // "flashes" by having a larger size in the first 100ms
1837                    let font_size_xp = 30
1838                        + ((floater.exp_change as f32 / 300.0).min(1.0) * 50.0) as u32
1839                        + if floater.jump_timer < 0.1 {
1840                            FLASH_MAX * (((1.0 - floater.jump_timer * 10.0) * 10.0) as u32)
1841                        } else {
1842                            0
1843                        };
1844                    let y = floater.timer as f64 * number_speed; // Timer sets the widget offset
1845                    //let fade = ((4.0 - floater.timer as f32) * 0.25) + 0.2; // Timer sets
1846                    // text transparency
1847                    let fade = floater.timer.min(1.0);
1848
1849                    if floater.exp_change > 0 {
1850                        let xp_pool = &floater.xp_pools;
1851                        let exp_string =
1852                            &i18n.get_msg_ctx("hud-sct-experience", &i18n::fluent_args! {
1853                                // Don't show 0 Exp
1854                                "amount" => &floater.exp_change.max(1),
1855                            });
1856                        Text::new(exp_string)
1857                            .font_size(font_size_xp)
1858                            .font_id(self.fonts.cyri.conrod_id)
1859                            .color(Color::Rgba(0.0, 0.0, 0.0, fade))
1860                            .x_y(
1861                                ui_widgets.win_w * (0.5 * floater.rand_offset.0 as f64 - 0.25),
1862                                ui_widgets.win_h * (0.15 * floater.rand_offset.1 as f64) + y - 3.0,
1863                            )
1864                            .set(player_sct_bg_id, ui_widgets);
1865                        Text::new(exp_string)
1866                            .font_size(font_size_xp)
1867                            .font_id(self.fonts.cyri.conrod_id)
1868                            .color(
1869                                if xp_pool.contains(&SkillGroupKind::Weapon(ToolKind::Pick)) {
1870                                    Color::Rgba(0.18, 0.32, 0.9, fade)
1871                                } else {
1872                                    Color::Rgba(0.59, 0.41, 0.67, fade)
1873                                },
1874                            )
1875                            .x_y(
1876                                ui_widgets.win_w * (0.5 * floater.rand_offset.0 as f64 - 0.25),
1877                                ui_widgets.win_h * (0.15 * floater.rand_offset.1 as f64) + y,
1878                            )
1879                            .set(player_sct_id, ui_widgets);
1880                        // Exp Source Image (TODO: fix widget id crash)
1881                        /*if xp_pool.contains(&SkillGroupKind::Weapon(ToolKind::Pick)) {
1882                            Image::new(self.imgs.pickaxe_ico)
1883                                .w_h(font_size_xp as f64, font_size_xp as f64)
1884                                .left_from(player_sct_id, 5.0)
1885                                .set(player_sct_icon_id, ui_widgets);
1886                        }*/
1887                    }
1888                }
1889
1890                // Skill points
1891                self.floaters
1892                    .skill_point_displays
1893                    .iter_mut()
1894                    .for_each(|f| f.timer -= dt.as_secs_f32());
1895                self.floaters
1896                    .skill_point_displays
1897                    .retain(|d| d.timer > 0_f32);
1898                if let Some(display) = self.floaters.skill_point_displays.first_mut() {
1899                    let fade = if display.timer < 3.0 {
1900                        display.timer * 0.33
1901                    } else if display.timer < 2.0 {
1902                        display.timer * 0.33 * 0.1
1903                    } else {
1904                        1.0
1905                    };
1906                    // Background image
1907                    let offset = if display.timer < 2.0 {
1908                        300.0 - (display.timer as f64 - 2.0) * -300.0
1909                    } else {
1910                        300.0
1911                    };
1912                    Image::new(self.imgs.level_up)
1913                        .w_h(328.0, 126.0)
1914                        .mid_top_with_margin_on(ui_widgets.window, offset)
1915                        .graphics_for(ui_widgets.window)
1916                        .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
1917                        .set(self.ids.player_rank_up, ui_widgets);
1918                    // Rank Number
1919                    let rank = display.total_points;
1920                    let fontsize = match rank {
1921                        1..=99 => (20, 8.0),
1922                        100..=999 => (18, 9.0),
1923                        1000..=9999 => (17, 10.0),
1924                        _ => (14, 12.0),
1925                    };
1926                    Text::new(&format!("{}", rank))
1927                        .font_size(fontsize.0)
1928                        .font_id(self.fonts.cyri.conrod_id)
1929                        .color(Color::Rgba(1.0, 1.0, 1.0, fade))
1930                        .mid_top_with_margin_on(self.ids.player_rank_up, fontsize.1)
1931                        .set(self.ids.player_rank_up_txt_number, ui_widgets);
1932                    // Static "New Rank!" text
1933                    Text::new(&i18n.get_msg("hud-rank_up"))
1934                        .font_size(40)
1935                        .font_id(self.fonts.cyri.conrod_id)
1936                        .color(Color::Rgba(0.0, 0.0, 0.0, fade))
1937                        .mid_bottom_with_margin_on(self.ids.player_rank_up, 20.0)
1938                        .set(self.ids.player_rank_up_txt_0_bg, ui_widgets);
1939                    Text::new(&i18n.get_msg("hud-rank_up"))
1940                        .font_size(40)
1941                        .font_id(self.fonts.cyri.conrod_id)
1942                        .color(Color::Rgba(1.0, 1.0, 1.0, fade))
1943                        .bottom_left_with_margins_on(self.ids.player_rank_up_txt_0_bg, 2.0, 2.0)
1944                        .set(self.ids.player_rank_up_txt_0, ui_widgets);
1945                    // Variable skilltree text
1946                    let skill = match display.skill_tree {
1947                        General => i18n.get_msg("common-weapons-general"),
1948                        Weapon(ToolKind::Hammer) => i18n.get_msg("common-weapons-hammer"),
1949                        Weapon(ToolKind::Axe) => i18n.get_msg("common-weapons-axe"),
1950                        Weapon(ToolKind::Sword) => i18n.get_msg("common-weapons-sword"),
1951                        Weapon(ToolKind::Sceptre) => i18n.get_msg("common-weapons-sceptre"),
1952                        Weapon(ToolKind::Bow) => i18n.get_msg("common-weapons-bow"),
1953                        Weapon(ToolKind::Staff) => i18n.get_msg("common-weapons-staff"),
1954                        Weapon(ToolKind::Pick) => i18n.get_msg("common-tool-mining"),
1955                        _ => Cow::Borrowed("Unknown"),
1956                    };
1957                    Text::new(&skill)
1958                        .font_size(20)
1959                        .font_id(self.fonts.cyri.conrod_id)
1960                        .color(Color::Rgba(0.0, 0.0, 0.0, fade))
1961                        .mid_top_with_margin_on(self.ids.player_rank_up, 45.0)
1962                        .set(self.ids.player_rank_up_txt_1_bg, ui_widgets);
1963                    Text::new(&skill)
1964                        .font_size(20)
1965                        .font_id(self.fonts.cyri.conrod_id)
1966                        .color(Color::Rgba(1.0, 1.0, 1.0, fade))
1967                        .bottom_left_with_margins_on(self.ids.player_rank_up_txt_1_bg, 2.0, 2.0)
1968                        .set(self.ids.player_rank_up_txt_1, ui_widgets);
1969                    // Variable skilltree icon
1970                    use crate::hud::SkillGroupKind::{General, Weapon};
1971                    Image::new(match display.skill_tree {
1972                        General => self.imgs.swords_crossed,
1973                        Weapon(ToolKind::Hammer) => self.imgs.hammer,
1974                        Weapon(ToolKind::Axe) => self.imgs.axe,
1975                        Weapon(ToolKind::Sword) => self.imgs.sword,
1976                        Weapon(ToolKind::Sceptre) => self.imgs.sceptre,
1977                        Weapon(ToolKind::Bow) => self.imgs.bow,
1978                        Weapon(ToolKind::Staff) => self.imgs.staff,
1979                        Weapon(ToolKind::Pick) => self.imgs.mining,
1980                        _ => self.imgs.swords_crossed,
1981                    })
1982                    .w_h(20.0, 20.0)
1983                    .left_from(self.ids.player_rank_up_txt_1_bg, 5.0)
1984                    .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
1985                    .set(self.ids.player_rank_up_icon, ui_widgets);
1986                }
1987
1988                // Scrolling Combat Text for Parrying an attack
1989                self.floaters
1990                    .block_floaters
1991                    .iter_mut()
1992                    .for_each(|f| f.timer -= dt.as_secs_f32());
1993                self.floaters.block_floaters.retain(|f| f.timer > 0_f32);
1994                for floater in self.floaters.block_floaters.iter_mut() {
1995                    let number_speed = 50.0;
1996                    let player_sct_bg_id = player_sct_bg_id_walker.next(
1997                        &mut self.ids.player_sct_bgs,
1998                        &mut ui_widgets.widget_id_generator(),
1999                    );
2000                    let player_sct_id = player_sct_id_walker.next(
2001                        &mut self.ids.player_scts,
2002                        &mut ui_widgets.widget_id_generator(),
2003                    );
2004                    let font_size = 30;
2005                    let y = floater.timer as f64 * number_speed; // Timer sets the widget offset
2006                    // text transparency
2007                    let fade = if floater.timer < 0.25 {
2008                        floater.timer / 0.25
2009                    } else {
2010                        1.0
2011                    };
2012
2013                    Text::new(&i18n.get_msg("hud-sct-block"))
2014                        .font_size(font_size)
2015                        .font_id(self.fonts.cyri.conrod_id)
2016                        .color(Color::Rgba(0.0, 0.0, 0.0, fade))
2017                        .x_y(
2018                            ui_widgets.win_w * (0.0),
2019                            ui_widgets.win_h * (-0.3) + y - 3.0,
2020                        )
2021                        .set(player_sct_bg_id, ui_widgets);
2022                    Text::new(&i18n.get_msg("hud-sct-block"))
2023                        .font_size(font_size)
2024                        .font_id(self.fonts.cyri.conrod_id)
2025                        .color(Color::Rgba(0.69, 0.82, 0.88, fade))
2026                        .x_y(ui_widgets.win_w * 0.0, ui_widgets.win_h * -0.3 + y)
2027                        .set(player_sct_id, ui_widgets);
2028                }
2029            }
2030
2031            // Pop speech bubbles
2032            let now = Instant::now();
2033            self.speech_bubbles
2034                .retain(|_uid, bubble| bubble.timeout > now);
2035            self.content_bubbles
2036                .retain(|(_pos, bubble)| bubble.timeout > now);
2037
2038            // Don't show messages from muted players
2039            self.new_messages
2040                .retain(|msg| !chat::is_muted(client, &global_state.profile, msg));
2041
2042            // Push speech bubbles
2043            for msg in self.new_messages.iter() {
2044                global_state.profile.tutorial.event_chat_msg(msg);
2045                if let Some((bubble, uid)) = msg.to_bubble() {
2046                    self.speech_bubbles.insert(uid, bubble);
2047                }
2048            }
2049
2050            let mut overhead_walker = self.ids.overheads.walk();
2051            let mut overitem_walker = self.ids.overitems.walk();
2052            let mut sct_walker = self.ids.scts.walk();
2053            let mut sct_bg_walker = self.ids.sct_bgs.walk();
2054            let pulse = self.pulse;
2055
2056            let make_overitem =
2057                |item: &PickupItem, pos, distance, properties, fonts, interaction_options| {
2058                    let quality = get_quality_col(item.quality());
2059
2060                    // Item
2061                    overitem::Overitem::new(
2062                        util::describe(item, i18n, &self.item_i18n).into(),
2063                        quality,
2064                        distance,
2065                        fonts,
2066                        i18n,
2067                        properties,
2068                        pulse,
2069                        interaction_options,
2070                        &self.imgs,
2071                        global_state,
2072                    )
2073                    .x_y(0.0, 100.0)
2074                    .position_ingame(pos)
2075                };
2076
2077            self.failed_block_pickups
2078                .retain(|_, t| pulse - t.pulse < overitem::PICKUP_FAILED_FADE_OUT_TIME);
2079            self.failed_entity_pickups
2080                .retain(|_, t| pulse - t.pulse < overitem::PICKUP_FAILED_FADE_OUT_TIME);
2081
2082            // Render overitem: name, etc.
2083            for (entity, pos, item, distance) in (&entities, &pos, &items)
2084                .join()
2085                .map(|(entity, pos, item)| (entity, pos, item, pos.0.distance_squared(player_pos)))
2086                .filter(|(_, _, _, distance)| distance < &MAX_PICKUP_RANGE.powi(2))
2087            {
2088                let overitem_id = overitem_walker.next(
2089                    &mut self.ids.overitems,
2090                    &mut ui_widgets.widget_id_generator(),
2091                );
2092
2093                make_overitem(
2094                    item,
2095                    pos.0 + Vec3::unit_z() * 1.2,
2096                    distance,
2097                    overitem::OveritemProperties {
2098                        active: entity_interactables.contains_key(&entity),
2099                        pickup_failed_pulse: self.failed_entity_pickups.get(&entity).cloned(),
2100                    },
2101                    &self.fonts,
2102                    vec![(
2103                        Some(GameInput::Interact),
2104                        i18n.get_msg("hud-pick_up").to_string(),
2105                        overitem::TEXT_COLOR,
2106                    )],
2107                )
2108                .set(overitem_id, ui_widgets);
2109            }
2110
2111            // Render overitem for interactable blocks
2112            for (mat, pos, interactions, block) in
2113                block_interactables
2114                    .iter()
2115                    .filter_map(|(position, (block, interactions))| {
2116                        position
2117                            .get_block_and_transform(
2118                                &terrain,
2119                                &id_maps,
2120                                // Use the visual position of voxel collider entity
2121                                |e| {
2122                                    interpolated.get(e).map(|interpolated| {
2123                                        (comp::Pos(interpolated.pos), interpolated.ori)
2124                                    })
2125                                },
2126                                &colliders,
2127                            )
2128                            .map(|(mat, _)| (mat, *position, interactions, *block))
2129                    })
2130            {
2131                let overitem_id = overitem_walker.next(
2132                    &mut self.ids.overitems,
2133                    &mut ui_widgets.widget_id_generator(),
2134                );
2135
2136                let overitem_properties = overitem::OveritemProperties {
2137                    active: true,
2138                    pickup_failed_pulse: self.failed_block_pickups.get(&pos).cloned(),
2139                };
2140
2141                let pos = mat.mul_point(Vec3::broadcast(0.5));
2142                let over_pos = pos + Vec3::unit_z() * 0.7;
2143
2144                let interaction_text = |interaction: &BlockInteraction| match interaction {
2145                    BlockInteraction::Collect { steal } => (
2146                        Some(GameInput::Interact),
2147                        i18n.get_msg(if *steal { "hud-steal" } else { "hud-collect" })
2148                            .to_string(),
2149                        if *steal {
2150                            overitem::NEGATIVE_TEXT_COLOR
2151                        } else {
2152                            overitem::TEXT_COLOR
2153                        },
2154                    ),
2155                    BlockInteraction::Craft(_) => (
2156                        Some(GameInput::Interact),
2157                        i18n.get_msg("hud-use").to_string(),
2158                        overitem::TEXT_COLOR,
2159                    ),
2160                    BlockInteraction::Unlock { kind, steal } => {
2161                        let item_name = |item_id: &ItemDefinitionIdOwned| {
2162                            // TODO: get ItemKey and use it with i18n?
2163                            item_id
2164                                .as_ref()
2165                                .itemdef_id()
2166                                .map(|id| {
2167                                    let item = Item::new_from_asset_expect(id);
2168                                    util::describe(&item, i18n, &self.item_i18n)
2169                                })
2170                                .unwrap_or_else(|| "modular item".to_string())
2171                        };
2172
2173                        (
2174                            Some(GameInput::Interact),
2175                            match kind {
2176                                UnlockKind::Free => i18n
2177                                    .get_msg(if *steal { "hud-steal" } else { "hud-open" })
2178                                    .to_string(),
2179                                UnlockKind::Requires(item_id) => i18n
2180                                    .get_msg_ctx(
2181                                        if *steal {
2182                                            "hud-steal-requires"
2183                                        } else {
2184                                            "hud-unlock-requires"
2185                                        },
2186                                        &i18n::fluent_args! {
2187                                            "item" => item_name(item_id),
2188                                        },
2189                                    )
2190                                    .to_string(),
2191                                UnlockKind::Consumes(item_id) => i18n
2192                                    .get_msg_ctx(
2193                                        if *steal {
2194                                            "hud-steal-consumes"
2195                                        } else {
2196                                            "hud-unlock-consumes"
2197                                        },
2198                                        &i18n::fluent_args! {
2199                                            "item" => item_name(item_id),
2200                                        },
2201                                    )
2202                                    .to_string(),
2203                            },
2204                            if *steal {
2205                                overitem::NEGATIVE_TEXT_COLOR
2206                            } else {
2207                                overitem::TEXT_COLOR
2208                            },
2209                        )
2210                    },
2211                    BlockInteraction::Mine(mine_tool) => {
2212                        match (mine_tool, &info.active_mine_tool) {
2213                            (ToolKind::Pick, Some(ToolKind::Pick)) => (
2214                                Some(GameInput::Primary),
2215                                i18n.get_msg("hud-mine").to_string(),
2216                                overitem::TEXT_COLOR,
2217                            ),
2218                            (ToolKind::Pick, _) => (
2219                                None,
2220                                i18n.get_msg("hud-mine-needs_pickaxe").to_string(),
2221                                overitem::TEXT_COLOR,
2222                            ),
2223                            (ToolKind::Shovel, Some(ToolKind::Shovel)) => (
2224                                Some(GameInput::Primary),
2225                                i18n.get_msg("hud-dig").to_string(),
2226                                overitem::TEXT_COLOR,
2227                            ),
2228                            (ToolKind::Shovel, _) => (
2229                                None,
2230                                i18n.get_msg("hud-mine-needs_shovel").to_string(),
2231                                overitem::TEXT_COLOR,
2232                            ),
2233                            _ => (
2234                                None,
2235                                i18n.get_msg("hud-mine-needs_unhandled_case").to_string(),
2236                                overitem::TEXT_COLOR,
2237                            ),
2238                        }
2239                    },
2240                    BlockInteraction::Mount => {
2241                        let key = match block.get_sprite() {
2242                            Some(sprite) if sprite.is_controller() => "hud-steer",
2243                            Some(sprite) if sprite.is_bed() => "hud-rest",
2244                            _ => "hud-sit",
2245                        };
2246                        (
2247                            Some(GameInput::Mount),
2248                            i18n.get_msg(key).to_string(),
2249                            overitem::TEXT_COLOR,
2250                        )
2251                    },
2252                    BlockInteraction::Read(_) => (
2253                        Some(GameInput::Interact),
2254                        i18n.get_msg("hud-read").to_string(),
2255                        overitem::TEXT_COLOR,
2256                    ),
2257                    // TODO: change to turn on/turn off?
2258                    BlockInteraction::LightToggle(enable) => (
2259                        Some(GameInput::Interact),
2260                        i18n.get_msg(if *enable {
2261                            "hud-activate"
2262                        } else {
2263                            "hud-deactivate"
2264                        })
2265                        .to_string(),
2266                        overitem::TEXT_COLOR,
2267                    ),
2268                };
2269
2270                if let Some(sprite) = block.get_sprite() {
2271                    // TODO: Handle this better. The items returned from `try_reclaim_from_block`
2272                    // are based on rng. We probably want some function to get only gauranteed items
2273                    // from `LootSpec`.
2274                    let interactable_item = if sprite.should_drop_mystery() {
2275                        None
2276                    } else {
2277                        Item::try_reclaim_from_block(block, None).and_then(|mut items| {
2278                            debug_assert!(
2279                                items.len() <= 1,
2280                                "The amount of items returned from Item::try_reclaim_from_block \
2281                                 for non-container items must not be higher than one"
2282                            );
2283                            let (amount, mut item) = items.pop()?;
2284                            item.set_amount(amount.clamp(1, item.max_amount())).expect(
2285                                "Setting an item amount between 1 and item.max_amount() must \
2286                                 succeed",
2287                            );
2288                            Some(item)
2289                        })
2290                    };
2291
2292                    let (desc, quality) = interactable_item.map_or_else(
2293                        || (get_sprite_desc(sprite, i18n), overitem::TEXT_COLOR),
2294                        |item| {
2295                            (
2296                                Some(util::describe(&item, i18n, &self.item_i18n).into()),
2297                                get_quality_col(item.quality()),
2298                            )
2299                        },
2300                    );
2301                    let desc = desc.unwrap_or(Cow::Borrowed(""));
2302                    overitem::Overitem::new(
2303                        desc,
2304                        quality,
2305                        pos.distance_squared(player_pos),
2306                        &self.fonts,
2307                        i18n,
2308                        overitem_properties,
2309                        self.pulse,
2310                        interactions
2311                            .iter()
2312                            .map(|interaction| interaction_text(interaction))
2313                            .collect(),
2314                        &self.imgs,
2315                        global_state,
2316                    )
2317                    .x_y(0.0, 100.0)
2318                    .position_ingame(over_pos)
2319                    .set(overitem_id, ui_widgets);
2320                }
2321            }
2322
2323            // show hud for campfires and portals
2324            for (entity, interaction) in
2325                entity_interactables
2326                    .iter()
2327                    .filter_map(|(entity, interactions)| {
2328                        interactions.iter().find_map(|interaction| {
2329                            matches!(
2330                                interaction,
2331                                EntityInteraction::CampfireSit | EntityInteraction::ActivatePortal
2332                            )
2333                            .then_some((*entity, *interaction))
2334                        })
2335                    })
2336            {
2337                let overitem_id = overitem_walker.next(
2338                    &mut self.ids.overitems,
2339                    &mut ui_widgets.widget_id_generator(),
2340                );
2341
2342                let overitem_properties = overitem::OveritemProperties {
2343                    active: true,
2344                    pickup_failed_pulse: None,
2345                };
2346                let pos = client
2347                    .state()
2348                    .ecs()
2349                    .read_storage::<comp::Pos>()
2350                    .get(entity)
2351                    .map_or(Vec3::zero(), |e| e.0);
2352                let over_pos = pos + Vec3::unit_z() * 1.5;
2353
2354                let (name, interaction_text) = match interaction {
2355                    EntityInteraction::CampfireSit => {
2356                        ("hud-crafting-campfire", "hud-waypoint_interact")
2357                    },
2358                    EntityInteraction::ActivatePortal => ("hud-portal", "hud-activate"),
2359                    _ => unreachable!(),
2360                };
2361
2362                overitem::Overitem::new(
2363                    i18n.get_msg(name),
2364                    overitem::TEXT_COLOR,
2365                    pos.distance_squared(player_pos),
2366                    &self.fonts,
2367                    i18n,
2368                    overitem_properties,
2369                    self.pulse,
2370                    vec![(
2371                        Some(interaction.game_input()),
2372                        i18n.get_msg(interaction_text).to_string(),
2373                        overitem::TEXT_COLOR,
2374                    )],
2375                    &self.imgs,
2376                    global_state,
2377                )
2378                .x_y(0.0, 100.0)
2379                .position_ingame(over_pos)
2380                .set(overitem_id, ui_widgets);
2381            }
2382
2383            let speech_bubbles = &self.speech_bubbles;
2384            let my_stats = stats.get(me);
2385            // Render overhead name tags and health bars
2386            for (
2387                entity,
2388                pos,
2389                info,
2390                bubble,
2391                _,
2392                _,
2393                health,
2394                _,
2395                scale,
2396                body,
2397                hpfl,
2398                in_group,
2399                character_activity,
2400            ) in (
2401                &entities,
2402                &pos,
2403                interpolated.maybe(),
2404                &stats,
2405                &skill_sets,
2406                healths.maybe(),
2407                &buffs,
2408                energy.maybe(),
2409                scales.maybe(),
2410                &bodies,
2411                &mut hp_floater_lists,
2412                &uids,
2413                &inventories,
2414                char_activities.maybe(),
2415                poises.maybe(),
2416                (is_mounts.maybe(), is_riders.maybe(), stances.maybe()),
2417            )
2418                .join()
2419                .filter(|t| {
2420                    let health = t.5;
2421                    !health.is_some_and(|h| h.is_dead)
2422                })
2423                .filter_map(
2424                    |(
2425                        entity,
2426                        pos,
2427                        interpolated,
2428                        stats,
2429                        skill_set,
2430                        health,
2431                        buffs,
2432                        energy,
2433                        scale,
2434                        body,
2435                        hpfl,
2436                        uid,
2437                        inventory,
2438                        character_activity,
2439                        poise,
2440                        (is_mount, is_rider, stance),
2441                    )| {
2442                        // Use interpolated position if available
2443                        let pos = interpolated.map_or(pos.0, |i| i.pos);
2444                        let in_group = client.group_members().contains_key(uid);
2445                        let is_me = entity == me;
2446                        let dist_sqr = pos.distance_squared(player_pos);
2447
2448                        let is_marked = my_stats.is_some_and(|s| s.marked_entities.contains(uid));
2449
2450                        // Determine whether to display nametag and healthbar based on whether the
2451                        // entity is mounted, has been damaged, is targeted/selected, or is in your
2452                        // group
2453                        // Note: even if this passes the healthbar can
2454                        // be hidden in some cases if it is at maximum
2455                        let display_overhead_info = !is_me
2456                            && (is_mount.is_none()
2457                                || health.is_none_or(overhead::should_show_healthbar))
2458                            && is_rider
2459                                .is_none_or(|is_rider| Some(&is_rider.mount) != uids.get(me))
2460                            && ((info.target_entity == Some(entity))
2461                                || info.selected_entity.is_some_and(|s| s.0 == entity)
2462                                || health.is_none_or(overhead::should_show_healthbar)
2463                                || in_group
2464                                || is_marked)
2465                            && dist_sqr
2466                                < (if in_group {
2467                                    NAMETAG_GROUP_RANGE
2468                                } else if hpfl
2469                                    .time_since_last_dmg_by_me
2470                                    .is_some_and(|t| t < NAMETAG_DMG_TIME)
2471                                {
2472                                    NAMETAG_DMG_RANGE
2473                                } else {
2474                                    NAMETAG_RANGE
2475                                })
2476                                .powi(2);
2477
2478                        let info = display_overhead_info.then(|| overhead::Info {
2479                            name: Some(i18n.get_content(&stats.name)),
2480                            health,
2481                            buffs: Some(buffs),
2482                            energy,
2483                            combat_rating: if let (Some(health), Some(energy), Some(poise)) =
2484                                (health, energy, poise)
2485                            {
2486                                Some(combat::combat_rating(
2487                                    inventory, health, energy, poise, skill_set, *body, &msm,
2488                                ))
2489                            } else {
2490                                None
2491                            },
2492                            hardcore: hardcore.contains(entity),
2493                            stance,
2494                            marked: is_marked,
2495                        });
2496                        // Only render bubble if nearby or if its me and setting is on
2497                        let bubble = if (dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) && !is_me)
2498                            || (is_me && global_state.settings.interface.speech_bubble_self)
2499                        {
2500                            speech_bubbles.get(uid)
2501                        } else {
2502                            None
2503                        };
2504                        (info.is_some() || bubble.is_some()).then_some({
2505                            (
2506                                entity,
2507                                pos,
2508                                info,
2509                                bubble,
2510                                stats,
2511                                skill_set,
2512                                health,
2513                                buffs,
2514                                scale,
2515                                body,
2516                                hpfl,
2517                                in_group,
2518                                character_activity,
2519                            )
2520                        })
2521                    },
2522                )
2523            {
2524                let overhead_id = overhead_walker.next(
2525                    &mut self.ids.overheads,
2526                    &mut ui_widgets.widget_id_generator(),
2527                );
2528
2529                let height_offset = body.height() * scale.map_or(1.0, |s| s.0) + 0.5;
2530                let ingame_pos = pos + Vec3::unit_z() * height_offset;
2531
2532                let interaction_options =
2533                    entity_interactables
2534                        .get(&entity)
2535                        .map_or_else(Vec::new, |interactions| {
2536                            interactions
2537                                .iter()
2538                                .filter_map(|interaction| {
2539                                    let message = match interaction {
2540                                        EntityInteraction::HelpDowned => "hud-help",
2541                                        EntityInteraction::Pet => "hud-pet",
2542                                        EntityInteraction::Trade => "hud-trade",
2543                                        EntityInteraction::Mount => "hud-mount",
2544                                        EntityInteraction::Talk => "hud-talk",
2545                                        EntityInteraction::StayFollow => {
2546                                            let is_staying = character_activity
2547                                                .is_some_and(|activity| activity.is_pet_staying);
2548
2549                                            if is_staying { "hud-follow" } else { "hud-stay" }
2550                                        },
2551                                        // Handled by overitem HUDs
2552                                        EntityInteraction::PickupItem
2553                                        | EntityInteraction::CampfireSit
2554                                        | EntityInteraction::ActivatePortal => return None,
2555                                    };
2556
2557                                    Some((
2558                                        interaction.game_input(),
2559                                        i18n.get_msg(message).to_string(),
2560                                    ))
2561                                })
2562                                .collect()
2563                        });
2564
2565                // Speech bubble, name, level, and hp bars
2566                overhead::Overhead::new(
2567                    info,
2568                    bubble,
2569                    in_group,
2570                    self.pulse,
2571                    interaction_options,
2572                    i18n,
2573                    &self.imgs,
2574                    &self.fonts,
2575                    &time,
2576                    global_state,
2577                )
2578                .x_y(0.0, 100.0)
2579                .position_ingame(ingame_pos)
2580                .set(overhead_id, ui_widgets);
2581
2582                // Enemy SCT
2583                if global_state.settings.interface.sct && !hpfl.floaters.is_empty() {
2584                    fn calc_fade(floater: &HpFloater) -> f32 {
2585                        if floater.info.precise {
2586                            ((crate::ecs::sys::floater::PRECISE_SHOWTIME - floater.timer) * 0.75)
2587                                + 0.5
2588                        } else {
2589                            ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer) * 0.25) + 0.2
2590                        }
2591                    }
2592
2593                    hpfl.floaters.retain(|fl| calc_fade(fl) > 0.0);
2594                    let floaters = &hpfl.floaters;
2595
2596                    // Colors
2597                    const WHITE: Rgb<f32> = Rgb::new(1.0, 0.9, 0.8);
2598                    const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
2599                    const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
2600                    const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
2601                    const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
2602                    const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
2603                    const DAMAGE_COLORS: [Rgb<f32>; 6] = [
2604                        WHITE,
2605                        LIGHT_OR,
2606                        LIGHT_MED_OR,
2607                        MED_OR,
2608                        DARK_ORANGE,
2609                        RED_ORANGE,
2610                    ];
2611                    // Largest value that select the first color is 40, then it shifts colors
2612                    // every 5
2613                    let font_col = |font_size: u32, precise: bool| {
2614                        if precise {
2615                            Rgb::new(1.0, 0.9, 0.0)
2616                        } else {
2617                            DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize]
2618                        }
2619                    };
2620
2621                    for floater in floaters {
2622                        let number_speed = 250.0; // Enemy number speed
2623                        let sct_id = sct_walker
2624                            .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
2625                        let sct_bg_id = sct_bg_walker
2626                            .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
2627                        // Clamp the amount so you don't have absurdly large damage numbers
2628                        let max_hp_frac = floater
2629                            .info
2630                            .amount
2631                            .abs()
2632                            .clamp(Health::HEALTH_EPSILON, health.map_or(1.0, |h| h.maximum()))
2633                            / health.map_or(1.0, |h| h.maximum());
2634                        let hp_dmg_text = if floater.info.amount.abs() < 0.1 {
2635                            String::new()
2636                        } else if global_state.settings.interface.sct_damage_rounding
2637                            && floater.info.amount.abs() >= 1.0
2638                        {
2639                            format!("{:.0}", floater.info.amount.abs())
2640                        } else {
2641                            format!("{:.1}", floater.info.amount.abs())
2642                        };
2643                        let precise = floater.info.precise;
2644                        // Timer sets text transparency
2645                        let fade = calc_fade(floater);
2646                        // Increase font size based on fraction of maximum health
2647                        // "flashes" by having a larger size in the first 100ms
2648                        let font_size =
2649                            30 + (if precise {
2650                                (max_hp_frac * 10.0) as u32 * 3 + 10
2651                            } else {
2652                                (max_hp_frac * 10.0) as u32 * 3
2653                            }) + if floater.jump_timer < 0.1 {
2654                                FLASH_MAX
2655                                    * (((1.0 - floater.jump_timer * 10.0)
2656                                        * 10.0
2657                                        * if precise { 1.25 } else { 1.0 })
2658                                        as u32)
2659                            } else {
2660                                0
2661                            };
2662                        let font_col = font_col(font_size, precise);
2663                        // Timer sets the widget offset
2664                        let y = if precise {
2665                            ui_widgets.win_h * (floater.rand as f64 % 0.075)
2666                                + ui_widgets.win_h * 0.05
2667                        } else {
2668                            (floater.timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64
2669                                * number_speed)
2670                                + 100.0
2671                        };
2672
2673                        let x = if !precise {
2674                            0.0
2675                        } else {
2676                            (floater.rand as f64 - 0.5) * 0.075 * ui_widgets.win_w
2677                                + (0.03 * ui_widgets.win_w * (floater.rand as f64 - 0.5).signum())
2678                        };
2679
2680                        Text::new(&hp_dmg_text)
2681                            .font_size(font_size)
2682                            .font_id(self.fonts.cyri.conrod_id)
2683                            .color(if floater.info.amount < 0.0 {
2684                                Color::Rgba(0.0, 0.0, 0.0, fade)
2685                            } else {
2686                                Color::Rgba(0.0, 0.0, 0.0, 1.0)
2687                            })
2688                            .x_y(x, y - 3.0)
2689                            .position_ingame(ingame_pos)
2690                            .set(sct_bg_id, ui_widgets);
2691                        Text::new(&hp_dmg_text)
2692                            .font_size(font_size)
2693                            .font_id(self.fonts.cyri.conrod_id)
2694                            .x_y(x, y)
2695                            .color(if floater.info.amount < 0.0 {
2696                                Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
2697                            } else {
2698                                Color::Rgba(0.1, 1.0, 0.1, 1.0)
2699                            })
2700                            .position_ingame(ingame_pos)
2701                            .set(sct_id, ui_widgets);
2702                    }
2703                }
2704            }
2705
2706            for (pos, bubble) in &self.content_bubbles {
2707                let overhead_id = overhead_walker.next(
2708                    &mut self.ids.overheads,
2709                    &mut ui_widgets.widget_id_generator(),
2710                );
2711
2712                overhead::Overhead::new(
2713                    None,
2714                    Some(bubble),
2715                    false,
2716                    self.pulse,
2717                    Vec::new(),
2718                    i18n,
2719                    &self.imgs,
2720                    &self.fonts,
2721                    &time,
2722                    global_state,
2723                )
2724                .x_y(0.0, 100.0)
2725                .position_ingame(*pos)
2726                .set(overhead_id, ui_widgets);
2727            }
2728        }
2729
2730        // Display debug window.
2731        // TODO:
2732        // Make it use i18n keys.
2733        if let Some(debug_info) = debug_info {
2734            prof_span!("debug info");
2735
2736            const V_PAD: f64 = 5.0;
2737            const H_PAD: f64 = 5.0;
2738            const FONT_SCALE: u32 = 14;
2739            let mut largest_str_len: usize = 0;
2740            let mut debug_msg_line_count: usize = 0;
2741
2742            // Ticks per second
2743            let debug_msg_ticks_per_sec = format!(
2744                "FPS: {:.0} ({}ms) (~{}ms)",
2745                debug_info.tps,
2746                debug_info.frame_time.as_millis(),
2747                debug_info.frame_variance.as_millis(),
2748            );
2749            Text::new(&debug_msg_ticks_per_sec)
2750                .color(TEXT_COLOR)
2751                .top_left_with_margins_on(self.ids.debug_bg, V_PAD, H_PAD)
2752                .font_id(self.fonts.cyri.conrod_id)
2753                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2754                .set(self.ids.fps_counter, ui_widgets);
2755            largest_str_len = usize::max(largest_str_len, debug_msg_ticks_per_sec.len());
2756            debug_msg_line_count += 1;
2757
2758            // Ping
2759            let debug_msg_ping = format!("Ping: {:.0}ms", debug_info.ping_ms);
2760            Text::new(&debug_msg_ping)
2761                .color(TEXT_COLOR)
2762                .down_from(self.ids.fps_counter, V_PAD)
2763                .font_id(self.fonts.cyri.conrod_id)
2764                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2765                .set(self.ids.ping, ui_widgets);
2766            largest_str_len = usize::max(largest_str_len, debug_msg_ping.len());
2767            debug_msg_line_count += 1;
2768
2769            // Player's position
2770            let coordinates_text = match debug_info.coordinates {
2771                Some(coordinates) => format!(
2772                    "Coordinates: ({:.0}, {:.0}, {:.0})",
2773                    coordinates.0.x, coordinates.0.y, coordinates.0.z,
2774                ),
2775                None => "Player has no Pos component".to_owned(),
2776            };
2777            Text::new(&coordinates_text)
2778                .color(TEXT_COLOR)
2779                .down_from(self.ids.ping, V_PAD)
2780                .font_id(self.fonts.cyri.conrod_id)
2781                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2782                .set(self.ids.coordinates, ui_widgets);
2783            largest_str_len = usize::max(largest_str_len, coordinates_text.len());
2784            debug_msg_line_count += 1;
2785
2786            // Player's velocity
2787            let (velocity_text, glide_ratio_text) = match debug_info.velocity {
2788                Some(velocity) => {
2789                    let velocity = velocity.0;
2790                    let velocity_text = format!(
2791                        "Velocity: ({:.1}, {:.1}, {:.1}) [{:.1} u/s]",
2792                        velocity.x,
2793                        velocity.y,
2794                        velocity.z,
2795                        velocity.magnitude()
2796                    );
2797                    let horizontal_velocity = velocity.xy().magnitude();
2798                    let dz = velocity.z;
2799                    // don't divide by zero
2800                    let glide_ratio_text = if dz.abs() > 0.0001 {
2801                        format!("Glide Ratio: {:.1}", -(horizontal_velocity / dz))
2802                    } else {
2803                        "Glide Ratio: Altitude is constant".to_owned()
2804                    };
2805
2806                    (velocity_text, glide_ratio_text)
2807                },
2808                None => {
2809                    let err = "Player has no Vel component";
2810                    (err.to_owned(), err.to_owned())
2811                },
2812            };
2813            Text::new(&velocity_text)
2814                .color(TEXT_COLOR)
2815                .down_from(self.ids.coordinates, V_PAD)
2816                .font_id(self.fonts.cyri.conrod_id)
2817                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2818                .set(self.ids.velocity, ui_widgets);
2819            largest_str_len = usize::max(largest_str_len, velocity_text.len());
2820            debug_msg_line_count += 1;
2821
2822            Text::new(&glide_ratio_text)
2823                .color(TEXT_COLOR)
2824                .down_from(self.ids.velocity, V_PAD)
2825                .font_id(self.fonts.cyri.conrod_id)
2826                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2827                .set(self.ids.glide_ratio, ui_widgets);
2828            largest_str_len = usize::max(largest_str_len, glide_ratio_text.len());
2829            debug_msg_line_count += 1;
2830
2831            // Glide Angle of Attack
2832            let glide_angle_text = angle_of_attack_text(
2833                debug_info.in_fluid,
2834                debug_info.velocity,
2835                debug_info.character_state.as_ref(),
2836            );
2837            Text::new(&glide_angle_text)
2838                .color(TEXT_COLOR)
2839                .down_from(self.ids.glide_ratio, V_PAD)
2840                .font_id(self.fonts.cyri.conrod_id)
2841                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2842                .set(self.ids.glide_aoe, ui_widgets);
2843            largest_str_len = usize::max(largest_str_len, glide_angle_text.len());
2844            debug_msg_line_count += 1;
2845
2846            // Air velocity
2847            let air_vel_text = air_velocity(debug_info.in_fluid);
2848            Text::new(&air_vel_text)
2849                .color(TEXT_COLOR)
2850                .down_from(self.ids.glide_aoe, V_PAD)
2851                .font_id(self.fonts.cyri.conrod_id)
2852                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2853                .set(self.ids.air_vel, ui_widgets);
2854            largest_str_len = usize::max(largest_str_len, air_vel_text.len());
2855            debug_msg_line_count += 1;
2856
2857            // Player's orientation vector
2858            let orientation_text = match debug_info.ori {
2859                Some(ori) => {
2860                    let orientation = ori.look_dir();
2861                    format!(
2862                        "Orientation: ({:.2}, {:.2}, {:.2})",
2863                        orientation.x, orientation.y, orientation.z,
2864                    )
2865                },
2866                None => "Player has no Ori component".to_owned(),
2867            };
2868            Text::new(&orientation_text)
2869                .color(TEXT_COLOR)
2870                .down_from(self.ids.air_vel, V_PAD)
2871                .font_id(self.fonts.cyri.conrod_id)
2872                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2873                .set(self.ids.orientation, ui_widgets);
2874            largest_str_len = usize::max(largest_str_len, orientation_text.len());
2875            debug_msg_line_count += 1;
2876
2877            let look_dir_text = {
2878                let look_vec = debug_info.look_dir.to_vec();
2879
2880                format!(
2881                    "Look Direction: ({:.2}, {:.2}, {:.2})",
2882                    look_vec.x, look_vec.y, look_vec.z,
2883                )
2884            };
2885            Text::new(&look_dir_text)
2886                .color(TEXT_COLOR)
2887                .down_from(self.ids.orientation, V_PAD)
2888                .font_id(self.fonts.cyri.conrod_id)
2889                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2890                .set(self.ids.look_direction, ui_widgets);
2891            largest_str_len = usize::max(largest_str_len, look_dir_text.len());
2892            debug_msg_line_count += 1;
2893
2894            // Loaded distance
2895            let debug_msg_loaded_distance = format!(
2896                "View distance: {:.2} blocks ({:.2} chunks)",
2897                client.loaded_distance(),
2898                client.loaded_distance() / TerrainChunk::RECT_SIZE.x as f32,
2899            );
2900            Text::new(&debug_msg_loaded_distance)
2901                .color(TEXT_COLOR)
2902                .down_from(self.ids.look_direction, V_PAD)
2903                .font_id(self.fonts.cyri.conrod_id)
2904                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2905                .set(self.ids.loaded_distance, ui_widgets);
2906            largest_str_len = usize::max(largest_str_len, debug_msg_loaded_distance.len());
2907            debug_msg_line_count += 1;
2908
2909            // Time
2910            let time_in_seconds = client.state().get_time_of_day();
2911            let current_time = NaiveTime::from_num_seconds_from_midnight_opt(
2912                // Wraps around back to 0s if it exceeds 24 hours (24 hours = 86400s)
2913                (time_in_seconds as u64 % 86400) as u32,
2914                0,
2915            )
2916            .expect("time always valid");
2917            let debug_msg_time = format!("Time: {}", current_time.format("%H:%M"));
2918            Text::new(&debug_msg_time)
2919                .color(TEXT_COLOR)
2920                .down_from(self.ids.loaded_distance, V_PAD)
2921                .font_id(self.fonts.cyri.conrod_id)
2922                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2923                .set(self.ids.time, ui_widgets);
2924            largest_str_len = usize::max(largest_str_len, debug_msg_time.len());
2925            debug_msg_line_count += 1;
2926
2927            // Weather
2928            let weather = client.weather_at_player();
2929            let debug_msg_weather = format!(
2930                "Weather({kind}): {{cloud: {cloud:.2}, rain: {rain:.2}, wind: <{wind_x:.0}, \
2931                 {wind_y:.0}>}}",
2932                kind = weather.get_kind(),
2933                cloud = weather.cloud,
2934                rain = weather.rain,
2935                wind_x = weather.wind.x,
2936                wind_y = weather.wind.y
2937            );
2938            Text::new(&debug_msg_weather)
2939                .color(TEXT_COLOR)
2940                .down_from(self.ids.time, V_PAD)
2941                .font_id(self.fonts.cyri.conrod_id)
2942                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2943                .set(self.ids.weather, ui_widgets);
2944            largest_str_len = usize::max(largest_str_len, debug_msg_weather.len());
2945            debug_msg_line_count += 1;
2946
2947            // Number of entities
2948            let entity_count = client.state().ecs().entities().join().count();
2949            let debug_msg_entity_count = format!("Entity count: {}", entity_count);
2950            Text::new(&debug_msg_entity_count)
2951                .color(TEXT_COLOR)
2952                .down_from(self.ids.weather, V_PAD)
2953                .font_id(self.fonts.cyri.conrod_id)
2954                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2955                .set(self.ids.entity_count, ui_widgets);
2956            largest_str_len = usize::max(largest_str_len, debug_msg_entity_count.len());
2957            debug_msg_line_count += 1;
2958
2959            // Number of chunks
2960            let debug_msg_num_chunks = format!(
2961                "Chunks: {} ({} visible) & {} (shadow)",
2962                debug_info.num_chunks, debug_info.num_visible_chunks, debug_info.num_shadow_chunks,
2963            );
2964            Text::new(&debug_msg_num_chunks)
2965                .color(TEXT_COLOR)
2966                .down_from(self.ids.entity_count, V_PAD)
2967                .font_id(self.fonts.cyri.conrod_id)
2968                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2969                .set(self.ids.num_chunks, ui_widgets);
2970            largest_str_len = usize::max(largest_str_len, debug_msg_num_chunks.len());
2971            debug_msg_line_count += 1;
2972
2973            // Type of biome
2974            let debug_msg_biome_type = format!("Biome: {:?}", client.current_biome());
2975            Text::new(&debug_msg_biome_type)
2976                .color(TEXT_COLOR)
2977                .down_from(self.ids.num_chunks, V_PAD)
2978                .font_id(self.fonts.cyri.conrod_id)
2979                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2980                .set(self.ids.current_biome, ui_widgets);
2981            largest_str_len = usize::max(largest_str_len, debug_msg_biome_type.len());
2982            debug_msg_line_count += 1;
2983
2984            // Type of site
2985            let debug_msg_site_type = format!("Site: {:?}", client.current_site());
2986            Text::new(&debug_msg_site_type)
2987                .color(TEXT_COLOR)
2988                .down_from(self.ids.current_biome, V_PAD)
2989                .font_id(self.fonts.cyri.conrod_id)
2990                .font_size(self.fonts.cyri.scale(FONT_SCALE))
2991                .set(self.ids.current_site, ui_widgets);
2992            largest_str_len = usize::max(largest_str_len, debug_msg_site_type.len());
2993            debug_msg_line_count += 1;
2994
2995            // Current song info
2996            let debug_msg_current_song = format!(
2997                "Now playing: {} [{}]",
2998                debug_info.current_track, debug_info.current_artist,
2999            );
3000            Text::new(&debug_msg_current_song)
3001                .color(TEXT_COLOR)
3002                .down_from(self.ids.current_site, V_PAD)
3003                .font_id(self.fonts.cyri.conrod_id)
3004                .font_size(self.fonts.cyri.scale(FONT_SCALE))
3005                .set(self.ids.song_info, ui_widgets);
3006            largest_str_len = usize::max(largest_str_len, debug_msg_current_song.len());
3007            debug_msg_line_count += 1;
3008
3009            let debug_msg_active_channels = format!(
3010                "Active channels: M{}, A{}, S{}, U{}, CPU: {:2.0}%",
3011                debug_info.active_channels.music,
3012                debug_info.active_channels.ambience,
3013                debug_info.active_channels.sfx,
3014                debug_info.active_channels.ui,
3015                debug_info.audio_cpu_usage * 100.0,
3016            );
3017            Text::new(&debug_msg_active_channels)
3018                .color(TEXT_COLOR)
3019                .down_from(self.ids.song_info, V_PAD)
3020                .font_id(self.fonts.cyri.conrod_id)
3021                .font_size(self.fonts.cyri.scale(FONT_SCALE))
3022                .set(self.ids.active_channels, ui_widgets);
3023            largest_str_len = usize::max(largest_str_len, debug_msg_active_channels.len());
3024            debug_msg_line_count += 1;
3025
3026            // Number of lights
3027            let debug_msg_num_lights = format!("Lights: {}", debug_info.num_lights,);
3028            Text::new(&debug_msg_num_lights)
3029                .color(TEXT_COLOR)
3030                .down_from(self.ids.active_channels, V_PAD)
3031                .font_id(self.fonts.cyri.conrod_id)
3032                .font_size(self.fonts.cyri.scale(FONT_SCALE))
3033                .set(self.ids.num_lights, ui_widgets);
3034            largest_str_len = usize::max(largest_str_len, debug_msg_num_lights.len());
3035            debug_msg_line_count += 1;
3036
3037            // Number of figures
3038            let debug_msg_num_figures = format!(
3039                "Figures: {} ({} visible)",
3040                debug_info.num_figures, debug_info.num_figures_visible,
3041            );
3042            Text::new(&debug_msg_num_figures)
3043                .color(TEXT_COLOR)
3044                .down_from(self.ids.num_lights, V_PAD)
3045                .font_id(self.fonts.cyri.conrod_id)
3046                .font_size(self.fonts.cyri.scale(FONT_SCALE))
3047                .set(self.ids.num_figures, ui_widgets);
3048            largest_str_len = usize::max(largest_str_len, debug_msg_num_figures.len());
3049            debug_msg_line_count += 1;
3050
3051            // Number of particles
3052            let debug_msg_num_particles = format!(
3053                "Particles: {} ({} visible)",
3054                debug_info.num_particles, debug_info.num_particles_visible,
3055            );
3056            Text::new(&debug_msg_num_particles)
3057                .color(TEXT_COLOR)
3058                .down_from(self.ids.num_figures, V_PAD)
3059                .font_id(self.fonts.cyri.conrod_id)
3060                .font_size(self.fonts.cyri.scale(FONT_SCALE))
3061                .set(self.ids.num_particles, ui_widgets);
3062            largest_str_len = usize::max(largest_str_len, debug_msg_num_particles.len());
3063            debug_msg_line_count += 1;
3064
3065            // Graphics backend
3066            let debug_msg_graphics_backend = format!(
3067                "Graphics backend: {}",
3068                global_state.window.renderer().graphics_backend(),
3069            );
3070            Text::new(&debug_msg_graphics_backend)
3071                .color(TEXT_COLOR)
3072                .down_from(self.ids.num_particles, V_PAD)
3073                .font_id(self.fonts.cyri.conrod_id)
3074                .font_size(self.fonts.cyri.scale(FONT_SCALE))
3075                .set(self.ids.graphics_backend, ui_widgets);
3076            largest_str_len = usize::max(largest_str_len, debug_msg_graphics_backend.len());
3077            debug_msg_line_count += 1;
3078
3079            let gpu_timings = global_state.window.renderer().timings();
3080
3081            // GPU timing for different pipelines
3082            if !gpu_timings.is_empty() {
3083                let num_timings = gpu_timings.len();
3084                // Make sure we have enough ids
3085                if self.ids.gpu_timings.len() < num_timings {
3086                    self.ids
3087                        .gpu_timings
3088                        .resize(num_timings, &mut ui_widgets.widget_id_generator());
3089                }
3090
3091                for (i, timing) in gpu_timings.iter().enumerate() {
3092                    let label = timing.1;
3093                    // We skip displaying these since they aren't present every frame.
3094                    if label.starts_with(crate::render::UI_PREMULTIPLY_PASS) {
3095                        continue;
3096                    }
3097                    let timings_text = &format!("{label:16}{:.3} ms", timing.2 * 1000.0);
3098                    let timings_widget = Text::new(timings_text)
3099                        .color(TEXT_COLOR)
3100                        .down(V_PAD)
3101                        .x_place_on(
3102                            self.ids.debug_bg,
3103                            conrod_core::position::Place::Start(Some(
3104                                H_PAD + 10.0 * timing.0 as f64,
3105                            )),
3106                        )
3107                        .font_id(self.fonts.cyri.conrod_id)
3108                        .font_size(self.fonts.cyri.scale(FONT_SCALE));
3109
3110                    largest_str_len = usize::max(largest_str_len, timings_text.len());
3111                    debug_msg_line_count += 1;
3112
3113                    timings_widget.set(self.ids.gpu_timings[i], ui_widgets);
3114                }
3115            }
3116
3117            // TODO: Use a more accurate method for calculating background width from text
3118            // content/length. Multiplying by font scale then dividing by 2.0 is
3119            // only an ad-hoc approach.
3120            let debug_bg_width =
3121                (H_PAD * 2.0) + (largest_str_len as f64) * (FONT_SCALE as f64) / 2.0;
3122            let debug_bg_height =
3123                (V_PAD * 2.0) + (debug_msg_line_count as f64) * ((FONT_SCALE as f64) + V_PAD);
3124            let debug_bg_size = [debug_bg_width, debug_bg_height];
3125
3126            Rectangle::fill(debug_bg_size)
3127                .rgba(0.0, 0.0, 0.0, global_state.settings.chat.chat_opacity)
3128                .top_left_with_margins_on(ui_widgets.window, 10.0, 10.0)
3129                .set(self.ids.debug_bg, ui_widgets);
3130        }
3131
3132        // Bag button and nearby icons
3133        let ecs = client.state().ecs();
3134        // let entity = info.viewpoint_entity;
3135        let stats = ecs.read_storage::<comp::Stats>();
3136        let skill_sets = ecs.read_storage::<comp::SkillSet>();
3137        let buffs = ecs.read_storage::<comp::Buffs>();
3138        let msm = ecs.read_resource::<MaterialStatManifest>();
3139        let time = ecs.read_resource::<Time>();
3140
3141        // Action text in bottom right corner
3142        DynamicTutorial::new(global_state, client, &self.fonts, &self.imgs, i18n)
3143            .set(self.ids.buttons, ui_widgets);
3144
3145        // Group Window
3146        for event in Group::new(
3147            &mut self.show,
3148            client,
3149            &global_state.settings,
3150            &self.imgs,
3151            &self.rot_imgs,
3152            &self.fonts,
3153            i18n,
3154            self.pulse,
3155            global_state,
3156            tooltip_manager,
3157            &msm,
3158            &time,
3159        )
3160        .set(self.ids.group_window, ui_widgets)
3161        {
3162            match event {
3163                group::Event::Accept => events.push(Event::AcceptInvite),
3164                group::Event::Decline => events.push(Event::DeclineInvite),
3165                group::Event::Kick(uid) => events.push(Event::KickMember(uid)),
3166                group::Event::LeaveGroup => events.push(Event::LeaveGroup),
3167                group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)),
3168            }
3169        }
3170        // Popup (waypoint saved and similar notifications)
3171        Popup::new(
3172            i18n,
3173            client,
3174            &self.new_notifications,
3175            &self.fonts,
3176            &self.show,
3177            global_state,
3178        )
3179        .set(self.ids.popup, ui_widgets);
3180
3181        if let Some(prompt_dialog_settings) = &self.show.prompt_dialog {
3182            // Prompt Dialog
3183            match PromptDialog::new(
3184                &self.imgs,
3185                &self.fonts,
3186                &global_state.i18n,
3187                &global_state.settings,
3188                prompt_dialog_settings,
3189            )
3190            .set(self.ids.prompt_dialog, ui_widgets)
3191            {
3192                Some(dialog_outcome_event) => {
3193                    match dialog_outcome_event {
3194                        DialogOutcomeEvent::Affirmative(event) => events.push(event),
3195                        DialogOutcomeEvent::Negative(event) => {
3196                            if let Some(event) = event {
3197                                events.push(event);
3198                            };
3199                        },
3200                    };
3201
3202                    // Close the prompt dialog once an option has been chosen
3203                    self.show.prompt_dialog = None;
3204                },
3205                None => {},
3206            }
3207        }
3208
3209        // Skillbar
3210        // Get player stats
3211        let ecs = client.state().ecs();
3212        let entity = info.viewpoint_entity;
3213        let healths = ecs.read_storage::<Health>();
3214        let inventories = ecs.read_storage::<comp::Inventory>();
3215        let rbm = ecs.read_resource::<RecipeBookManifest>();
3216        let energies = ecs.read_storage::<comp::Energy>();
3217        let skillsets = ecs.read_storage::<comp::SkillSet>();
3218        let active_abilities = ecs.read_storage::<comp::ActiveAbilities>();
3219        let bodies = ecs.read_storage::<comp::Body>();
3220        let poises = ecs.read_storage::<comp::Poise>();
3221        let uids = ecs.read_storage::<Uid>();
3222        let combos = ecs.read_storage::<comp::Combo>();
3223        let combo = combos.get(entity);
3224        let time = ecs.read_resource::<Time>();
3225        let stances = ecs.read_storage::<comp::Stance>();
3226        let char_states = ecs.read_storage::<comp::CharacterState>();
3227        // Combo floater stuffs
3228        self.floaters.combo_floater = self.floaters.combo_floater.map(|mut f| {
3229            f.timer -= dt.as_secs_f64();
3230            f
3231        });
3232        self.floaters.combo_floater = self.floaters.combo_floater.filter(|f| f.timer > 0_f64);
3233
3234        if let (
3235            Some(health),
3236            Some(inventory),
3237            Some(energy),
3238            Some(poise),
3239            Some(skillset),
3240            Some(body),
3241        ) = (
3242            healths.get(entity),
3243            inventories.get(entity),
3244            energies.get(entity),
3245            poises.get(entity),
3246            skillsets.get(entity),
3247            bodies.get(entity),
3248        ) {
3249            let skillbar_events = Skillbar::new(
3250                client,
3251                &info,
3252                global_state,
3253                &self.imgs,
3254                &self.item_imgs,
3255                &self.fonts,
3256                &self.rot_imgs,
3257                health,
3258                inventory,
3259                energy,
3260                poise,
3261                skillset,
3262                active_abilities.get(entity),
3263                body,
3264                //&character_state,
3265                self.pulse,
3266                //&controller,
3267                &self.hotbar,
3268                tooltip_manager,
3269                item_tooltip_manager,
3270                &mut self.slot_manager,
3271                i18n,
3272                &self.item_i18n,
3273                &msm,
3274                &rbm,
3275                self.floaters.combo_floater,
3276                combo,
3277                char_states.get(entity),
3278                stances.get(entity),
3279                stats.get(entity),
3280                buffs.get(entity),
3281            )
3282            .set(self.ids.skillbar, ui_widgets);
3283
3284            for event in skillbar_events {
3285                match event {
3286                    skillbar::Event::OpenDiary(skillgroup) => {
3287                        self.show.diary(true);
3288                        self.show.open_skill_tree(skillgroup);
3289                    },
3290                    skillbar::Event::OpenBag => self.show.bag(!self.show.bag),
3291                }
3292            }
3293        }
3294
3295        // Buffs
3296        if let (Some(player_buffs), Some(health), Some(energy), Some(poise)) = (
3297            buffs.get(info.viewpoint_entity),
3298            healths.get(entity),
3299            energies.get(entity),
3300            poises.get(entity),
3301        ) {
3302            for event in BuffsBar::new(
3303                &self.imgs,
3304                &self.fonts,
3305                &self.rot_imgs,
3306                tooltip_manager,
3307                i18n,
3308                player_buffs,
3309                stances.get(entity),
3310                self.pulse,
3311                global_state,
3312                health,
3313                energy,
3314                poise,
3315                &time,
3316            )
3317            .set(self.ids.buffs, ui_widgets)
3318            {
3319                match event {
3320                    buffs::Event::RemoveBuff(buff_id) => events.push(Event::RemoveBuff(buff_id)),
3321                    buffs::Event::LeaveStance => events.push(Event::LeaveStance),
3322                }
3323            }
3324        }
3325        // Crafting
3326        if self.show.crafting
3327            && let Some(inventory) = inventories.get(entity)
3328        {
3329            for event in Crafting::new(
3330                //&self.show,
3331                client,
3332                global_state,
3333                &info,
3334                &self.imgs,
3335                &self.fonts,
3336                i18n,
3337                &self.item_i18n,
3338                self.pulse,
3339                &self.rot_imgs,
3340                item_tooltip_manager,
3341                &mut self.slot_manager,
3342                &self.item_imgs,
3343                inventory,
3344                &rbm,
3345                &msm,
3346                tooltip_manager,
3347                &mut self.show,
3348                &global_state.settings,
3349            )
3350            .set(self.ids.crafting_window, ui_widgets)
3351            {
3352                match event {
3353                    crafting::Event::CraftRecipe {
3354                        recipe_name,
3355                        amount,
3356                    } => {
3357                        events.push(Event::CraftRecipe {
3358                            recipe_name,
3359                            craft_sprite: self.show.crafting_fields.craft_sprite,
3360                            amount,
3361                        });
3362                    },
3363                    crafting::Event::CraftModularWeapon {
3364                        primary_slot,
3365                        secondary_slot,
3366                    } => {
3367                        events.push(Event::CraftModularWeapon {
3368                            primary_slot,
3369                            secondary_slot,
3370                            craft_sprite: self
3371                                .show
3372                                .crafting_fields
3373                                .craft_sprite
3374                                .map(|(pos, _sprite)| pos),
3375                        });
3376                    },
3377                    crafting::Event::CraftModularWeaponComponent {
3378                        toolkind,
3379                        material,
3380                        modifier,
3381                    } => {
3382                        events.push(Event::CraftModularWeaponComponent {
3383                            toolkind,
3384                            material,
3385                            modifier,
3386                            craft_sprite: self
3387                                .show
3388                                .crafting_fields
3389                                .craft_sprite
3390                                .map(|(pos, _sprite)| pos),
3391                        });
3392                    },
3393                    crafting::Event::Close => {
3394                        self.show.stats = false;
3395                        self.show.crafting(false);
3396                        if !self.show.social {
3397                            self.show.want_grab = true;
3398                            self.force_ungrab = false;
3399                        } else {
3400                            self.force_ungrab = true
3401                        };
3402                    },
3403                    crafting::Event::ChangeCraftingTab(sel_cat) => {
3404                        self.show.open_crafting_tab(sel_cat, None);
3405                    },
3406                    crafting::Event::Focus(widget_id) => {
3407                        self.to_focus = Some(Some(widget_id));
3408                    },
3409                    crafting::Event::SearchRecipe(search_key) => {
3410                        self.show.search_crafting_recipe(search_key);
3411                    },
3412                    crafting::Event::ClearRecipeInputs => {
3413                        self.show.crafting_fields.recipe_inputs.clear();
3414                    },
3415                    crafting::Event::RepairItem { slot } => {
3416                        if let Some(sprite_pos) = self
3417                            .show
3418                            .crafting_fields
3419                            .craft_sprite
3420                            .map(|(pos, _sprite)| pos)
3421                        {
3422                            events.push(Event::RepairItem {
3423                                item: slot,
3424                                sprite_pos,
3425                            });
3426                        }
3427                    },
3428                    crafting::Event::ShowAllRecipes(show) => {
3429                        events.push(Event::SettingsChange(SettingsChange::Gameplay(
3430                            crate::session::settings_change::Gameplay::ChangeShowAllRecipes(show),
3431                        )));
3432                    },
3433                    crafting::Event::MoveCrafting(pos) => {
3434                        global_state.settings.hud_position.crafting = pos;
3435                    },
3436                }
3437            }
3438        }
3439
3440        if global_state.settings.audio.subtitles {
3441            Subtitles::new(
3442                client,
3443                &global_state.settings,
3444                global_state.audio.get_listener_pos(),
3445                global_state.audio.get_listener_ori(),
3446                &mut global_state.audio.subtitles,
3447                &self.fonts,
3448                i18n,
3449            )
3450            .set(self.ids.subtitles, ui_widgets);
3451        }
3452        let inventory = inventories.get(entity);
3453        //Loot
3454        LootScroller::new(
3455            &mut self.new_loot_messages,
3456            client,
3457            &info,
3458            &self.show,
3459            &self.imgs,
3460            &self.item_imgs,
3461            &self.rot_imgs,
3462            &self.fonts,
3463            i18n,
3464            &self.item_i18n,
3465            &msm,
3466            &rbm,
3467            inventory,
3468            item_tooltip_manager,
3469            self.pulse,
3470        )
3471        .set(self.ids.loot_scroller, ui_widgets);
3472
3473        self.new_loot_messages.clear();
3474
3475        let persisted_state = self.persisted_state.borrow();
3476        // MiniMap
3477        for event in MiniMap::new(
3478            client,
3479            &self.imgs,
3480            &self.rot_imgs,
3481            &self.world_map,
3482            &self.fonts,
3483            self.pulse,
3484            camera.get_orientation(),
3485            global_state,
3486            &persisted_state.location_markers,
3487            &self.voxel_minimap,
3488            &self.extra_markers,
3489        )
3490        .set(self.ids.minimap, ui_widgets)
3491        {
3492            match event {
3493                minimap::Event::SettingsChange(interface_change) => {
3494                    events.push(Event::SettingsChange(interface_change.into()));
3495                },
3496                minimap::Event::MoveMiniMap(pos) => {
3497                    global_state.settings.hud_position.minimap = pos;
3498                },
3499            }
3500        }
3501        drop(persisted_state);
3502
3503        // Bag contents
3504        if self.show.bag
3505            && let (
3506                Some(player_stats),
3507                Some(skill_set),
3508                Some(health),
3509                Some(energy),
3510                Some(body),
3511                Some(poise),
3512            ) = (
3513                stats.get(info.viewpoint_entity),
3514                skill_sets.get(info.viewpoint_entity),
3515                healths.get(entity),
3516                energies.get(entity),
3517                bodies.get(entity),
3518                poises.get(entity),
3519            )
3520        {
3521            for event in BagManager::new(
3522                global_state,
3523                client,
3524                &info,
3525                &self.imgs,
3526                &self.item_imgs,
3527                &self.fonts,
3528                &self.rot_imgs,
3529                tooltip_manager,
3530                item_tooltip_manager,
3531                &mut self.slot_manager,
3532                self.pulse,
3533                i18n,
3534                &self.item_i18n,
3535                player_stats,
3536                skill_set,
3537                health,
3538                energy,
3539                &self.show,
3540                body,
3541                &msm,
3542                &rbm,
3543                poise,
3544                &self.menu_events,
3545            )
3546            .set(self.ids.bag, ui_widgets)
3547            {
3548                match event {
3549                    bag::BagEvent::Close => {
3550                        self.show.stats = false;
3551                        Self::show_bag(&mut self.slot_manager, &mut self.show, false);
3552                        if !self.show.social {
3553                            self.show.want_grab = true;
3554                            self.force_ungrab = false;
3555                        } else {
3556                            self.force_ungrab = true
3557                        };
3558                        // Also closes any open trade windows
3559                        if self.show.trade {
3560                            self.events.push(Event::TradeAction(TradeAction::Decline));
3561                        }
3562                    },
3563                    bag::BagEvent::MoveBag(pos) => {
3564                        global_state.settings.hud_position.bag.own = pos;
3565                    },
3566                    bag::BagEvent::BagExpand => {
3567                        self.show.bag_menu_split = !self.show.bag_menu_split
3568                    },
3569                    bag::BagEvent::SetDetailsMode(mode) => self.show.bag_details = mode,
3570                    bag::BagEvent::ChangeInventorySortOrder(sort_order) => {
3571                        self.events
3572                            .push(Event::SettingsChange(SettingsChange::Inventory(
3573                                Inventory::ChangeSortOrder(sort_order),
3574                            )));
3575                    },
3576                    bag::BagEvent::SortInventory(sort_order) => {
3577                        self.events.push(Event::SortInventory(sort_order))
3578                    },
3579                    bag::BagEvent::SwapEquippedWeapons => {
3580                        self.events.push(Event::SwapEquippedWeapons)
3581                    },
3582                }
3583            }
3584        }
3585
3586        // Trade window
3587        if self.show.trade {
3588            for event in Trade::new(
3589                client,
3590                global_state,
3591                &info,
3592                &self.imgs,
3593                &self.item_imgs,
3594                &self.fonts,
3595                &self.rot_imgs,
3596                tooltip_manager,
3597                item_tooltip_manager,
3598                &mut self.slot_manager,
3599                i18n,
3600                &self.item_i18n,
3601                &msm,
3602                &rbm,
3603                self.pulse,
3604                &mut self.show,
3605            )
3606            .set(self.ids.trade, ui_widgets)
3607            {
3608                match event {
3609                    trade::TradeEvent::HudUpdate(update) => match update {
3610                        trade::HudUpdate::Focus(idx) => self.to_focus = Some(Some(idx)),
3611                        trade::HudUpdate::Submit => {
3612                            let key = self.show.trade_amount_input_key.take();
3613                            key.map(|k| {
3614                                k.submit_action.map(|action| {
3615                                    self.events.push(Event::TradeAction(action));
3616                                });
3617                            });
3618                        },
3619                    },
3620                    trade::TradeEvent::TradeAction(action) => {
3621                        if let TradeAction::Decline = action {
3622                            self.show.stats = false;
3623                            self.show.trade(false);
3624                            if !self.show.social {
3625                                self.show.want_grab = true;
3626                                self.force_ungrab = false;
3627                            } else {
3628                                self.force_ungrab = true
3629                            };
3630                            self.show.prompt_dialog = None;
3631                        }
3632                        events.push(Event::TradeAction(action));
3633                    },
3634                    trade::TradeEvent::SetDetailsMode(mode) => {
3635                        self.show.trade_details = mode;
3636                    },
3637                    trade::TradeEvent::ShowPrompt(prompt) => {
3638                        self.show.prompt_dialog = Some(prompt);
3639                    },
3640                    trade::TradeEvent::MoveBag(pos) => {
3641                        global_state.settings.hud_position.bag.other = pos;
3642                    },
3643                }
3644            }
3645        }
3646
3647        self.new_messages.retain(chat::show_in_chatbox);
3648
3649        // Chat box
3650        // Draw this after loot scroller and subtitles so it can be dragged
3651        // even when hovering over them
3652        // TODO look into parenting and then settings movable widgets to floating
3653        if global_state.settings.interface.toggle_chat || self.force_chat {
3654            // `rev` since we push to the front of the queue and want the oldest message to
3655            // be the last pushed to the front.
3656            for hidden in self
3657                .persisted_state
3658                .borrow_mut()
3659                .message_backlog
3660                .0
3661                .drain(..)
3662                .rev()
3663            {
3664                self.new_messages.push_front(hidden);
3665            }
3666            for event in Chat::new(
3667                &mut self.new_messages,
3668                client,
3669                global_state,
3670                self.pulse,
3671                &self.imgs,
3672                &self.fonts,
3673                i18n,
3674                scale,
3675                self.clear_chat,
3676            )
3677            .and_then(self.force_chat_input.take(), |c, input| c.input(input))
3678            .and_then(self.tab_complete.take(), |c, input| {
3679                c.prepare_tab_completion(input)
3680            })
3681            .and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos))
3682            .set(self.ids.chat, ui_widgets)
3683            {
3684                match event {
3685                    chat::Event::TabCompletionStart(input) => {
3686                        self.tab_complete = Some(input);
3687                    },
3688                    chat::Event::SendMessage(message) => {
3689                        events.push(Event::SendMessage(message));
3690                    },
3691                    chat::Event::SendCommand(name, args) => {
3692                        events.push(Event::SendCommand(name, args));
3693                    },
3694                    chat::Event::Focus(focus_id) => {
3695                        self.to_focus = Some(Some(focus_id));
3696                    },
3697                    chat::Event::ChangeChatTab(tab) => {
3698                        events.push(Event::SettingsChange(ChatChange::ChangeChatTab(tab).into()));
3699                    },
3700                    chat::Event::ShowChatTabSettings(tab) => {
3701                        self.show.chat_tab_settings_index = Some(tab);
3702                        self.show.settings_tab = SettingsTab::Chat;
3703                        self.show.settings(true);
3704                    },
3705                    chat::Event::ResizeChat(size) => {
3706                        global_state.settings.chat.chat_size_x = size.x;
3707                        global_state.settings.chat.chat_size_y = size.y;
3708                    },
3709                    chat::Event::MoveChat(pos) => {
3710                        global_state.settings.chat.chat_pos_x = pos.x;
3711                        global_state.settings.chat.chat_pos_y = pos.y;
3712                    },
3713                    chat::Event::DisableForceChat => {
3714                        self.force_chat = false;
3715                    },
3716                }
3717            }
3718
3719            // Set to false only after the chat widget is cleared and updated
3720            self.clear_chat = false;
3721        } else {
3722            let mut persisted_state = self.persisted_state.borrow_mut();
3723            for message in self.new_messages.drain(..) {
3724                persisted_state
3725                    .message_backlog
3726                    .new_message(client, &global_state.profile, message);
3727            }
3728        }
3729
3730        self.new_messages.clear();
3731        self.new_notifications.clear();
3732
3733        // Settings
3734        if let Windows::Settings = self.show.open_windows {
3735            for event in SettingsWindow::new(
3736                global_state,
3737                &self.show,
3738                &self.imgs,
3739                &self.fonts,
3740                i18n,
3741                client.server_view_distance_limit(),
3742                fps as f32,
3743            )
3744            .set(self.ids.settings_window, ui_widgets)
3745            {
3746                match event {
3747                    settings_window::Event::ChangeTab(tab) => self.show.open_setting_tab(tab),
3748                    settings_window::Event::Close => {
3749                        // Unpause the game if we are on singleplayer so that we can logout
3750                        #[cfg(feature = "singleplayer")]
3751                        global_state.unpause();
3752                        self.show.want_grab = true;
3753                        self.force_ungrab = false;
3754
3755                        self.show.settings(false)
3756                    },
3757                    settings_window::Event::ChangeChatSettingsTab(tab) => {
3758                        self.show.chat_tab_settings_index = tab;
3759                    },
3760                    settings_window::Event::SettingsChange(settings_change) => {
3761                        events.push(Event::SettingsChange(settings_change));
3762                    },
3763                    settings_window::Event::ResetBindingMode => {
3764                        // Disables gamepad mapping mode to avoid issues
3765                        global_state.window.reset_mapping_mode();
3766                    },
3767                }
3768            }
3769        }
3770        // Quest Window
3771        let stats = client.state().ecs().read_storage::<comp::Stats>();
3772        let interpolated = client.state().ecs().read_storage::<vcomp::Interpolated>();
3773        if let Some((sender, _, dialogue)) = &self.current_dialogue
3774            && let Some(i) = interpolated.get(*sender)
3775            && let Some(player_i) = interpolated.get(client.entity())
3776            && i.pos.distance_squared(player_i.pos) > MAX_NPCINTERACT_RANGE.powi(2)
3777        {
3778            self.show.quest(false);
3779            events.push(Event::Dialogue(*sender, rtsim::Dialogue {
3780                id: dialogue.id,
3781                kind: rtsim::DialogueKind::End,
3782            }));
3783        }
3784
3785        let dialogue_open = if self.show.quest
3786            && let Some((sender, time, dialogue)) = &self.current_dialogue
3787        {
3788            match Quest::new(
3789                &self.show,
3790                client,
3791                &self.imgs,
3792                &self.fonts,
3793                i18n,
3794                global_state,
3795                &self.rot_imgs,
3796                tooltip_manager,
3797                &self.item_imgs,
3798                *sender,
3799                dialogue,
3800                *time,
3801                self.pulse,
3802            )
3803            .set(self.ids.quest_window, ui_widgets)
3804            {
3805                Some(quest::Event::Dialogue(target, dialogue)) => {
3806                    events.push(Event::Dialogue(target, dialogue));
3807                    true
3808                },
3809                Some(quest::Event::Close) => {
3810                    self.show.quest(false);
3811                    if !self.show.bag {
3812                        self.show.want_grab = true;
3813                        self.force_ungrab = false;
3814                    } else {
3815                        self.force_ungrab = true
3816                    };
3817                    false
3818                },
3819                None => true,
3820            }
3821        } else {
3822            false
3823        };
3824
3825        Tutorial::new(
3826            &self.show,
3827            client,
3828            &self.imgs,
3829            &self.fonts,
3830            i18n,
3831            global_state,
3832            &self.rot_imgs,
3833            tooltip_manager,
3834            &self.item_imgs,
3835            self.pulse,
3836            dt,
3837            self.show.esc_menu,
3838        )
3839        .set(self.ids.tutorial_window, ui_widgets);
3840
3841        if !dialogue_open && let Some((sender, _, dialogue)) = self.current_dialogue.take() {
3842            events.push(Event::Dialogue(sender, rtsim::Dialogue {
3843                id: dialogue.id,
3844                kind: rtsim::DialogueKind::End,
3845            }));
3846        }
3847
3848        // Social Window
3849        if self.show.social {
3850            let ecs = client.state().ecs();
3851            let _stats = ecs.read_storage::<comp::Stats>();
3852            for event in Social::new(
3853                &self.show,
3854                client,
3855                &self.imgs,
3856                &self.fonts,
3857                i18n,
3858                info.selected_entity,
3859                &self.rot_imgs,
3860                tooltip_manager,
3861                global_state,
3862            )
3863            .set(self.ids.social_window, ui_widgets)
3864            {
3865                match event {
3866                    social::Event::Close => {
3867                        self.show.social(false);
3868                        if !self.show.bag {
3869                            self.show.want_grab = true;
3870                            self.force_ungrab = false;
3871                        } else {
3872                            self.force_ungrab = true
3873                        };
3874                    },
3875                    social::Event::Focus(widget_id) => {
3876                        self.to_focus = Some(Some(widget_id));
3877                    },
3878                    social::Event::Invite(uid) => events.push(Event::InviteMember(uid)),
3879                    social::Event::SearchPlayers(search_key) => {
3880                        self.show.search_social_players(search_key)
3881                    },
3882                    social::Event::SetBattleMode(mode) => {
3883                        events.push(Event::SetBattleMode(mode));
3884                    },
3885                    social::Event::MoveSocial(pos) => {
3886                        global_state.settings.hud_position.social = pos;
3887                    },
3888                }
3889            }
3890        }
3891
3892        // Diary
3893        if self.show.diary {
3894            let entity = info.viewpoint_entity;
3895            let skill_sets = ecs.read_storage::<comp::SkillSet>();
3896            if let (
3897                Some(skill_set),
3898                Some(inventory),
3899                Some(char_state),
3900                Some(health),
3901                Some(energy),
3902                Some(body),
3903                Some(poise),
3904                Some(uid),
3905            ) = (
3906                skill_sets.get(entity),
3907                inventories.get(entity),
3908                char_states.get(entity),
3909                healths.get(entity),
3910                energies.get(entity),
3911                bodies.get(entity),
3912                poises.get(entity),
3913                uids.get(entity),
3914            ) {
3915                for event in Diary::new(
3916                    &self.show,
3917                    client,
3918                    global_state,
3919                    skill_set,
3920                    active_abilities.get(entity).unwrap_or(&Default::default()),
3921                    inventory,
3922                    char_state,
3923                    health,
3924                    energy,
3925                    poise,
3926                    body,
3927                    uid,
3928                    &msm,
3929                    &self.imgs,
3930                    &self.item_imgs,
3931                    &self.fonts,
3932                    i18n,
3933                    &self.item_i18n,
3934                    &self.rot_imgs,
3935                    tooltip_manager,
3936                    &mut self.slot_manager,
3937                    self.pulse,
3938                    stances.get(entity),
3939                    combo,
3940                    stats.get(entity),
3941                    buffs.get(entity),
3942                )
3943                .set(self.ids.diary, ui_widgets)
3944                {
3945                    match event {
3946                        diary::Event::Close => {
3947                            self.show.diary(false);
3948                            self.show.want_grab = true;
3949                            self.force_ungrab = false;
3950                        },
3951                        diary::Event::ChangeSkillTree(tree_sel) => {
3952                            self.show.open_skill_tree(tree_sel)
3953                        },
3954                        diary::Event::UnlockSkill(skill) => events.push(Event::UnlockSkill(skill)),
3955                        diary::Event::ChangeSection(section) => {
3956                            self.show.diary_fields.section = section;
3957                        },
3958                        diary::Event::SelectExpBar(xp_bar) => {
3959                            events.push(Event::SelectExpBar(xp_bar))
3960                        },
3961                    }
3962                }
3963            }
3964        }
3965        // Map
3966        if self.show.map {
3967            let mut persisted_state = self.persisted_state.borrow_mut();
3968            for event in Map::new(
3969                client,
3970                &self.imgs,
3971                &self.rot_imgs,
3972                &self.world_map,
3973                &self.fonts,
3974                self.pulse,
3975                i18n,
3976                global_state,
3977                tooltip_manager,
3978                &persisted_state.location_markers,
3979                self.map_drag,
3980                &self.extra_markers,
3981            )
3982            .set(self.ids.map, ui_widgets)
3983            {
3984                match event {
3985                    map::Event::Close => {
3986                        self.show.map(false);
3987                        self.show.want_grab = true;
3988                        self.force_ungrab = false;
3989                    },
3990                    map::Event::SettingsChange(settings_change) => {
3991                        events.push(Event::SettingsChange(settings_change.into()));
3992                    },
3993                    map::Event::RequestSiteInfo(id) => {
3994                        events.push(Event::RequestSiteInfo(id));
3995                    },
3996                    map::Event::SetLocationMarker(pos) => {
3997                        events.push(Event::MapMarkerEvent(MapMarkerChange::Update(pos)));
3998                        persisted_state
3999                            .location_markers
4000                            .update(comp::MapMarkerUpdate::Owned(MapMarkerChange::Update(pos)));
4001                    },
4002                    map::Event::MapDrag(new_drag) => {
4003                        self.map_drag = new_drag;
4004                    },
4005                    map::Event::RemoveMarker => {
4006                        persisted_state
4007                            .location_markers
4008                            .update(comp::MapMarkerUpdate::Owned(MapMarkerChange::Remove));
4009                        events.push(Event::MapMarkerEvent(MapMarkerChange::Remove));
4010                    },
4011                }
4012            }
4013        } else {
4014            // Reset the map position when it's not showing
4015            self.map_drag = Vec2::zero();
4016        }
4017
4018        if self.show.esc_menu {
4019            match EscMenu::new(&self.imgs, &self.fonts, i18n).set(self.ids.esc_menu, ui_widgets) {
4020                Some(esc_menu::Event::OpenSettings(tab)) => {
4021                    self.show.open_setting_tab(tab);
4022                },
4023                Some(esc_menu::Event::Close) => {
4024                    self.show.esc_menu = false;
4025                    self.show.want_grab = true;
4026                    self.force_ungrab = false;
4027
4028                    // Unpause the game if we are on singleplayer
4029                    #[cfg(feature = "singleplayer")]
4030                    global_state.unpause();
4031                },
4032                Some(esc_menu::Event::Logout) => {
4033                    // Unpause the game if we are on singleplayer so that we can logout
4034                    #[cfg(feature = "singleplayer")]
4035                    global_state.unpause();
4036
4037                    events.push(Event::Logout);
4038                },
4039                Some(esc_menu::Event::Quit) => events.push(Event::Quit),
4040                Some(esc_menu::Event::CharacterSelection) => {
4041                    // Unpause the game if we are on singleplayer so that we can logout
4042                    #[cfg(feature = "singleplayer")]
4043                    global_state.unpause();
4044
4045                    events.push(Event::CharacterSelection)
4046                },
4047                None => {},
4048            }
4049        }
4050
4051        let mut indicator_offset = 40.0;
4052
4053        // Free look indicator
4054        if let Some(freelook_key) = global_state
4055            .settings
4056            .controls
4057            .get_binding(GameInput::FreeLook)
4058            && self.show.free_look
4059        {
4060            let msg = i18n.get_msg_ctx("hud-free_look_indicator", &i18n::fluent_args! {
4061                "key" => freelook_key.display_string(),
4062                "toggle" => global_state.settings.gameplay.free_look_behavior as usize,
4063            });
4064            Text::new(&msg)
4065                .color(TEXT_BG)
4066                .mid_top_with_margin_on(ui_widgets.window, indicator_offset)
4067                .font_id(self.fonts.cyri.conrod_id)
4068                .font_size(self.fonts.cyri.scale(20))
4069                .set(self.ids.free_look_bg, ui_widgets);
4070            indicator_offset += 30.0;
4071            Text::new(&msg)
4072                .color(KILL_COLOR)
4073                .top_left_with_margins_on(self.ids.free_look_bg, -1.0, -1.0)
4074                .font_id(self.fonts.cyri.conrod_id)
4075                .font_size(self.fonts.cyri.scale(20))
4076                .set(self.ids.free_look_txt, ui_widgets);
4077        };
4078
4079        // Auto walk indicator
4080        if self.show.auto_walk {
4081            Text::new(&i18n.get_msg("hud-auto_walk_indicator"))
4082                .color(TEXT_BG)
4083                .mid_top_with_margin_on(ui_widgets.window, indicator_offset)
4084                .font_id(self.fonts.cyri.conrod_id)
4085                .font_size(self.fonts.cyri.scale(20))
4086                .set(self.ids.auto_walk_bg, ui_widgets);
4087            indicator_offset += 30.0;
4088            Text::new(&i18n.get_msg("hud-auto_walk_indicator"))
4089                .color(KILL_COLOR)
4090                .top_left_with_margins_on(self.ids.auto_walk_bg, -1.0, -1.0)
4091                .font_id(self.fonts.cyri.conrod_id)
4092                .font_size(self.fonts.cyri.scale(20))
4093                .set(self.ids.auto_walk_txt, ui_widgets);
4094        }
4095
4096        // Camera zoom lock
4097        self.show.zoom_lock.update(dt);
4098
4099        if let Some(zoom_lock) = self.show.zoom_lock.reason {
4100            let zoom_lock_message = match zoom_lock {
4101                NotificationReason::Remind => "hud-zoom_lock_indicator-remind",
4102                NotificationReason::Enable => "hud-zoom_lock_indicator-enable",
4103                NotificationReason::Disable => "hud-zoom_lock_indicator-disable",
4104            };
4105
4106            Text::new(&i18n.get_msg(zoom_lock_message))
4107                .color(TEXT_BG.alpha(self.show.zoom_lock.alpha))
4108                .mid_top_with_margin_on(ui_widgets.window, indicator_offset)
4109                .font_id(self.fonts.cyri.conrod_id)
4110                .font_size(self.fonts.cyri.scale(20))
4111                .set(self.ids.zoom_lock_bg, ui_widgets);
4112            indicator_offset += 30.0;
4113            Text::new(&i18n.get_msg(zoom_lock_message))
4114                .color(TEXT_COLOR.alpha(self.show.zoom_lock.alpha))
4115                .top_left_with_margins_on(self.ids.zoom_lock_bg, -1.0, -1.0)
4116                .font_id(self.fonts.cyri.conrod_id)
4117                .font_size(self.fonts.cyri.scale(20))
4118                .set(self.ids.zoom_lock_txt, ui_widgets);
4119        }
4120
4121        // Camera clamp indicator
4122        if let Some(cameraclamp_key) = global_state
4123            .settings
4124            .controls
4125            .get_binding(GameInput::CameraClamp)
4126            && self.show.camera_clamp
4127        {
4128            let msg = i18n.get_msg_ctx("hud-camera_clamp_indicator", &i18n::fluent_args! {
4129                "key" => cameraclamp_key.display_string(),
4130            });
4131            Text::new(&msg)
4132                .color(TEXT_BG)
4133                .mid_top_with_margin_on(ui_widgets.window, indicator_offset)
4134                .font_id(self.fonts.cyri.conrod_id)
4135                .font_size(self.fonts.cyri.scale(20))
4136                .set(self.ids.camera_clamp_bg, ui_widgets);
4137            Text::new(&msg)
4138                .color(KILL_COLOR)
4139                .top_left_with_margins_on(self.ids.camera_clamp_bg, -1.0, -1.0)
4140                .font_id(self.fonts.cyri.conrod_id)
4141                .font_size(self.fonts.cyri.scale(20))
4142                .set(self.ids.camera_clamp_txt, ui_widgets);
4143        }
4144
4145        // Maintain slot manager
4146        'slot_events: for event in self.slot_manager.maintain(ui_widgets) {
4147            use slots::{AbilitySlot, InventorySlot, SlotKind::*};
4148            let to_slot = |slot_kind| match slot_kind {
4149                Inventory(
4150                    i @ InventorySlot {
4151                        slot: Slot::Inventory(_) | Slot::Overflow(_),
4152                        ours: true,
4153                        ..
4154                    },
4155                ) => Some(i.slot),
4156                Inventory(InventorySlot {
4157                    slot: Slot::Equip(_),
4158                    ours: true,
4159                    ..
4160                }) => None,
4161                Inventory(InventorySlot { ours: false, .. }) => None,
4162                Equip(e) => Some(Slot::Equip(e)),
4163                Hotbar(_) => None,
4164                Trade(_) => None,
4165                Ability(_) => None,
4166                Crafting(_) => None,
4167            };
4168            match event {
4169                slot::Event::Dragged(a, b) => {
4170                    // Swap between slots
4171                    if let (Some(a), Some(b)) = (to_slot(a), to_slot(b)) {
4172                        events.push(Event::SwapSlots {
4173                            slot_a: a,
4174                            slot_b: b,
4175                            bypass_dialog: false,
4176                        });
4177                    } else if let (
4178                        Inventory(InventorySlot {
4179                            slot, ours: true, ..
4180                        }),
4181                        Hotbar(h),
4182                    ) = (a, b)
4183                    {
4184                        if let Slot::Inventory(slot) = slot
4185                            && let Some(item) = inventories
4186                                .get(info.viewpoint_entity)
4187                                .and_then(|inv| inv.get(slot))
4188                        {
4189                            self.hotbar.add_inventory_link(h, item);
4190                            events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
4191                        }
4192                    } else if let (Hotbar(a), Hotbar(b)) = (a, b) {
4193                        self.hotbar.swap(a, b);
4194                        events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
4195                    } else if let (Inventory(i), Trade(t)) = (a, b) {
4196                        if i.ours == t.ours
4197                            && let (Some(inventory), Slot::Inventory(slot)) =
4198                                (inventories.get(t.entity), i.slot)
4199                        {
4200                            events.push(Event::TradeAction(TradeAction::AddItem {
4201                                item: slot,
4202                                quantity: i.amount(inventory).unwrap_or(1),
4203                                ours: i.ours,
4204                            }));
4205                        }
4206                    } else if let (Trade(t), Inventory(i)) = (a, b) {
4207                        if i.ours == t.ours
4208                            && let Some(inventory) = inventories.get(t.entity)
4209                            && let Some(invslot) = t.invslot
4210                        {
4211                            events.push(Event::TradeAction(TradeAction::RemoveItem {
4212                                item: invslot,
4213                                quantity: t.amount(inventory).unwrap_or(1),
4214                                ours: t.ours,
4215                            }));
4216                        }
4217                    } else if let (Ability(a), Ability(b)) = (a, b) {
4218                        match (a, b) {
4219                            (AbilitySlot::Ability(ability), AbilitySlot::Slot(index)) => {
4220                                events.push(Event::ChangeAbility(index, ability));
4221                            },
4222                            (AbilitySlot::Slot(a), AbilitySlot::Slot(b)) => {
4223                                let me = info.viewpoint_entity;
4224                                if let Some(active_abilities) = active_abilities.get(me) {
4225                                    let ability_a = active_abilities
4226                                        .auxiliary_set(inventories.get(me), skill_sets.get(me))
4227                                        .get(a)
4228                                        .copied()
4229                                        .unwrap_or(AuxiliaryAbility::Empty);
4230                                    let ability_b = active_abilities
4231                                        .auxiliary_set(inventories.get(me), skill_sets.get(me))
4232                                        .get(b)
4233                                        .copied()
4234                                        .unwrap_or(AuxiliaryAbility::Empty);
4235                                    events.push(Event::ChangeAbility(a, ability_b));
4236                                    events.push(Event::ChangeAbility(b, ability_a));
4237                                }
4238                            },
4239                            (AbilitySlot::Slot(index), _) => {
4240                                events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
4241                            },
4242                            (AbilitySlot::Ability(_), AbilitySlot::Ability(_)) => {},
4243                        }
4244                    } else if let (Inventory(i), Crafting(c)) = (a, b) {
4245                        if let Slot::Inventory(slot) = i.slot {
4246                            // Add item to crafting input
4247                            if inventories
4248                                .get(info.viewpoint_entity)
4249                                .and_then(|inv| inv.get(slot))
4250                                .is_some_and(|item| {
4251                                    (c.requirement)(item, client.component_recipe_book(), c.info)
4252                                })
4253                            {
4254                                self.show
4255                                    .crafting_fields
4256                                    .recipe_inputs
4257                                    .insert(c.index, i.slot);
4258                            }
4259                        }
4260                    } else if let (Equip(e), Crafting(c)) = (a, b) {
4261                        // Add item to crafting input
4262                        if inventories
4263                            .get(client.entity())
4264                            .and_then(|inv| inv.equipped(e))
4265                            .is_some_and(|item| {
4266                                (c.requirement)(item, client.component_recipe_book(), c.info)
4267                            })
4268                        {
4269                            self.show
4270                                .crafting_fields
4271                                .recipe_inputs
4272                                .insert(c.index, Slot::Equip(e));
4273                        }
4274                    } else if let (Crafting(c), Inventory(_)) = (a, b) {
4275                        // Remove item from crafting input
4276                        self.show.crafting_fields.recipe_inputs.remove(&c.index);
4277                    } else if let (Ability(AbilitySlot::Ability(ability)), Hotbar(slot)) = (a, b)
4278                        && let Some(Some(HotbarSlotContents::Ability(index))) =
4279                            self.hotbar.slots.get(slot as usize)
4280                    {
4281                        events.push(Event::ChangeAbility(*index, ability));
4282                    }
4283                },
4284                slot::Event::Dropped(from) => {
4285                    // Drop item
4286                    if let Some(from) = to_slot(from) {
4287                        events.push(Event::DropSlot(from));
4288                    } else if let Hotbar(h) = from {
4289                        self.hotbar.clear_slot(h);
4290                        events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
4291                    } else if let Trade(t) = from {
4292                        if let Some(inventory) = inventories.get(t.entity)
4293                            && let Some(invslot) = t.invslot
4294                        {
4295                            events.push(Event::TradeAction(TradeAction::RemoveItem {
4296                                item: invslot,
4297                                quantity: t.amount(inventory).unwrap_or(1),
4298                                ours: t.ours,
4299                            }));
4300                        }
4301                    } else if let Ability(AbilitySlot::Slot(index)) = from {
4302                        events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
4303                    } else if let Crafting(c) = from {
4304                        // Remove item from crafting input
4305                        self.show.crafting_fields.recipe_inputs.remove(&c.index);
4306                    }
4307                },
4308                slot::Event::SplitDropped(from) => {
4309                    // Drop item
4310                    if let Some(from) = to_slot(from) {
4311                        events.push(Event::SplitDropSlot(from));
4312                    } else if let Hotbar(h) = from {
4313                        self.hotbar.clear_slot(h);
4314                        events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
4315                    } else if let Ability(AbilitySlot::Slot(index)) = from {
4316                        events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
4317                    }
4318                },
4319                slot::Event::SplitDragged(a, b) => {
4320                    // Swap between slots
4321                    if let (Some(a), Some(b)) = (to_slot(a), to_slot(b)) {
4322                        events.push(Event::SplitSwapSlots {
4323                            slot_a: a,
4324                            slot_b: b,
4325                            bypass_dialog: false,
4326                        });
4327                    } else if let (Inventory(i), Hotbar(h)) = (a, b) {
4328                        if let Slot::Inventory(slot) = i.slot
4329                            && let Some(item) = inventories
4330                                .get(info.viewpoint_entity)
4331                                .and_then(|inv| inv.get(slot))
4332                        {
4333                            self.hotbar.add_inventory_link(h, item);
4334                            events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
4335                        }
4336                    } else if let (Hotbar(a), Hotbar(b)) = (a, b) {
4337                        self.hotbar.swap(a, b);
4338                        events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
4339                    } else if let (Inventory(i), Trade(t)) = (a, b) {
4340                        if i.ours == t.ours
4341                            && let (Some(inventory), Slot::Inventory(slot)) =
4342                                (inventories.get(t.entity), i.slot)
4343                        {
4344                            events.push(Event::TradeAction(TradeAction::AddItem {
4345                                item: slot,
4346                                quantity: i.amount(inventory).unwrap_or(1) / 2,
4347                                ours: i.ours,
4348                            }));
4349                        }
4350                    } else if let (Trade(t), Inventory(i)) = (a, b) {
4351                        if i.ours == t.ours
4352                            && let Some(inventory) = inventories.get(t.entity)
4353                            && let Some(invslot) = t.invslot
4354                        {
4355                            events.push(Event::TradeAction(TradeAction::RemoveItem {
4356                                item: invslot,
4357                                quantity: t.amount(inventory).unwrap_or(1) / 2,
4358                                ours: t.ours,
4359                            }));
4360                        }
4361                    } else if let (Ability(a), Ability(b)) = (a, b) {
4362                        match (a, b) {
4363                            (AbilitySlot::Ability(ability), AbilitySlot::Slot(index)) => {
4364                                events.push(Event::ChangeAbility(index, ability));
4365                            },
4366                            (AbilitySlot::Slot(a), AbilitySlot::Slot(b)) => {
4367                                let me = info.viewpoint_entity;
4368                                if let Some(active_abilities) = active_abilities.get(me) {
4369                                    let ability_a = active_abilities
4370                                        .auxiliary_set(inventories.get(me), skill_sets.get(me))
4371                                        .get(a)
4372                                        .copied()
4373                                        .unwrap_or(AuxiliaryAbility::Empty);
4374                                    let ability_b = active_abilities
4375                                        .auxiliary_set(inventories.get(me), skill_sets.get(me))
4376                                        .get(b)
4377                                        .copied()
4378                                        .unwrap_or(AuxiliaryAbility::Empty);
4379                                    events.push(Event::ChangeAbility(a, ability_b));
4380                                    events.push(Event::ChangeAbility(b, ability_a));
4381                                }
4382                            },
4383                            (AbilitySlot::Slot(index), _) => {
4384                                events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
4385                            },
4386                            (AbilitySlot::Ability(_), AbilitySlot::Ability(_)) => {},
4387                        }
4388                    }
4389                },
4390                slot::Event::Used(from) => {
4391                    // Item used (selected and then clicked again)
4392                    if let Some(from) = to_slot(from) {
4393                        if self.show.crafting_fields.salvage
4394                            && matches!(
4395                                self.show.crafting_fields.crafting_tab,
4396                                CraftingTab::Dismantle
4397                            )
4398                        {
4399                            if let (Slot::Inventory(slot), Some((salvage_pos, _sprite_kind))) =
4400                                (from, self.show.crafting_fields.craft_sprite)
4401                            {
4402                                events.push(Event::SalvageItem { slot, salvage_pos })
4403                            }
4404                        } else {
4405                            events.push(Event::UseSlot {
4406                                slot: from,
4407                                bypass_dialog: false,
4408                            });
4409                        }
4410                    } else if let Hotbar(h) = from {
4411                        // Used from hotbar
4412                        self.hotbar.get(h).map(|s| match s {
4413                            hotbar::SlotContents::Inventory(i, _) => {
4414                                if let Some(inv) = inventories.get(info.viewpoint_entity) {
4415                                    // If the item in the inactive main hand is the same as the item
4416                                    // pressed in the hotbar, then swap active and inactive hands
4417                                    // instead of looking for
4418                                    // the item in the inventory
4419                                    if inv
4420                                        .equipped(comp::slot::EquipSlot::InactiveMainhand)
4421                                        .is_some_and(|item| item.item_hash() == i)
4422                                    {
4423                                        events.push(Event::SwapEquippedWeapons);
4424                                    } else if let Some(slot) = inv.get_slot_from_hash(i) {
4425                                        events.push(Event::UseSlot {
4426                                            slot: Slot::Inventory(slot),
4427                                            bypass_dialog: false,
4428                                        });
4429                                    }
4430                                }
4431                            },
4432                            hotbar::SlotContents::Ability(_) => {},
4433                        });
4434                    } else if let Ability(AbilitySlot::Slot(index)) = from {
4435                        events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
4436                    } else if let Crafting(c) = from {
4437                        // Remove item from crafting input
4438                        self.show.crafting_fields.recipe_inputs.remove(&c.index);
4439                    }
4440                },
4441                slot::Event::Request {
4442                    slot,
4443                    auto_quantity,
4444                } => {
4445                    if let Some((_, trade, prices)) = client.pending_trade() {
4446                        let ecs = client.state().ecs();
4447                        let inventories = ecs.read_component::<comp::Inventory>();
4448                        let get_inventory = |uid: Uid| {
4449                            if let Some(entity) = ecs.entity_from_uid(uid) {
4450                                inventories.get(entity)
4451                            } else {
4452                                None
4453                            }
4454                        };
4455                        let mut r_inventories = [None, None];
4456                        for (i, party) in trade.parties.iter().enumerate() {
4457                            match get_inventory(*party) {
4458                                Some(inventory) => {
4459                                    r_inventories[i] = Some(ReducedInventory::from(inventory))
4460                                },
4461                                None => continue 'slot_events,
4462                            };
4463                        }
4464                        let who = match ecs
4465                            .uid_from_entity(info.viewpoint_entity)
4466                            .and_then(|uid| trade.which_party(uid))
4467                        {
4468                            Some(who) => who,
4469                            None => continue 'slot_events,
4470                        };
4471                        let do_auto_quantity =
4472                            |inventory: &comp::Inventory,
4473                             slot,
4474                             ours,
4475                             remove,
4476                             quantity: &mut u32| {
4477                                if let Some(prices) = prices
4478                                    && let Some((balance0, balance1)) = prices
4479                                        .balance(&trade.offers, &r_inventories, who, true)
4480                                        .zip(prices.balance(
4481                                            &trade.offers,
4482                                            &r_inventories,
4483                                            1 - who,
4484                                            false,
4485                                        ))
4486                                    && let Some(item) = inventory.get(slot)
4487                                    && let Some(materials) =
4488                                        TradePricing::get_materials(&item.item_definition_id())
4489                                {
4490                                    let unit_price: f32 = materials
4491                                        .iter()
4492                                        .map(|e| {
4493                                            prices.values.get(&e.1).cloned().unwrap_or_default()
4494                                                * e.0
4495                                        })
4496                                        .sum::<f32>()
4497                                        * (if ours {
4498                                            TradePricing::good_from_itemdef_id(
4499                                                item.item_definition_id(),
4500                                            )
4501                                            .sell_discount(item.quality())
4502                                        } else {
4503                                            1.0
4504                                        });
4505
4506                                    let mut float_delta = if ours ^ remove {
4507                                        (balance1 - balance0) / unit_price
4508                                    } else {
4509                                        (balance0 - balance1) / unit_price
4510                                    };
4511                                    if ours ^ remove {
4512                                        float_delta = float_delta.ceil();
4513                                    } else {
4514                                        float_delta = float_delta.floor();
4515                                    }
4516                                    *quantity = float_delta.max(0.0) as u32;
4517                                }
4518                            };
4519                        match slot {
4520                            Inventory(i) => {
4521                                if let Some(inventory) = inventories.get(i.entity)
4522                                    && let Slot::Inventory(slot) = i.slot
4523                                {
4524                                    let mut quantity = 1;
4525                                    if auto_quantity {
4526                                        do_auto_quantity(
4527                                            inventory,
4528                                            slot,
4529                                            i.ours,
4530                                            false,
4531                                            &mut quantity,
4532                                        );
4533                                        let inv_quantity = i.amount(inventory).unwrap_or(1);
4534                                        quantity = quantity.min(inv_quantity);
4535                                    }
4536
4537                                    events.push(Event::TradeAction(TradeAction::AddItem {
4538                                        item: slot,
4539                                        quantity,
4540                                        ours: i.ours,
4541                                    }));
4542                                }
4543                            },
4544                            Trade(t) => {
4545                                if let Some(inventory) = inventories.get(t.entity)
4546                                    && let Some(invslot) = t.invslot
4547                                {
4548                                    let mut quantity = 1;
4549                                    if auto_quantity {
4550                                        do_auto_quantity(
4551                                            inventory,
4552                                            invslot,
4553                                            t.ours,
4554                                            true,
4555                                            &mut quantity,
4556                                        );
4557                                        let inv_quantity = t.amount(inventory).unwrap_or(1);
4558                                        quantity = quantity.min(inv_quantity);
4559                                    }
4560                                    events.push(Event::TradeAction(TradeAction::RemoveItem {
4561                                        item: invslot,
4562                                        quantity,
4563                                        ours: t.ours,
4564                                    }));
4565                                }
4566                            },
4567                            _ => {},
4568                        }
4569                    }
4570                },
4571            }
4572        }
4573        self.hotbar.maintain_abilities(client, &info);
4574
4575        // Temporary Example Quest
4576        let arrow_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer
4577        let show_intro = self.show.intro; // borrow check doesn't understand closures
4578        if let Some(toggle_cursor_key) = global_state
4579            .settings
4580            .controls
4581            .get_binding(GameInput::ToggleCursor)
4582            .filter(|_| !show_intro)
4583        {
4584            prof_span!("temporary example quest");
4585            match global_state.settings.interface.intro_show {
4586                Intro::Show => {
4587                    if Button::image(self.imgs.button)
4588                        .w_h(200.0, 60.0)
4589                        .hover_image(self.imgs.button_hover)
4590                        .press_image(self.imgs.button_press)
4591                        .bottom_left_with_margins_on(ui_widgets.window, 350.0, 150.0)
4592                        .label(&i18n.get_msg("hud-tutorial_btn"))
4593                        .label_font_id(self.fonts.cyri.conrod_id)
4594                        .label_font_size(self.fonts.cyri.scale(18))
4595                        .label_color(TEXT_COLOR)
4596                        .label_y(conrod_core::position::Relative::Scalar(2.0))
4597                        .image_color(ENEMY_HP_COLOR)
4598                        .set(self.ids.intro_button, ui_widgets)
4599                        .was_clicked()
4600                    {
4601                        self.show.intro = true;
4602                        self.show.want_grab = true;
4603                    }
4604                    let tutorial_click_msg =
4605                        i18n.get_msg_ctx("hud-tutorial_click_here", &i18n::fluent_args! {
4606                            "key" => toggle_cursor_key.display_string(),
4607                        });
4608                    Image::new(self.imgs.sp_indicator_arrow)
4609                        .w_h(20.0, 11.0)
4610                        .mid_top_with_margin_on(self.ids.intro_button, -20.0 + arrow_ani as f64)
4611                        .color(Some(QUALITY_LEGENDARY))
4612                        .set(self.ids.tut_arrow, ui_widgets);
4613                    Text::new(&tutorial_click_msg)
4614                        .mid_top_with_margin_on(self.ids.tut_arrow, -40.0)
4615                        .font_id(self.fonts.cyri.conrod_id)
4616                        .font_size(self.fonts.cyri.scale(14))
4617                        .center_justify()
4618                        .color(BLACK)
4619                        .set(self.ids.tut_arrow_txt_bg, ui_widgets);
4620                    Text::new(&tutorial_click_msg)
4621                        .bottom_right_with_margins_on(self.ids.tut_arrow_txt_bg, 1.0, 1.0)
4622                        .center_justify()
4623                        .font_id(self.fonts.cyri.conrod_id)
4624                        .font_size(self.fonts.cyri.scale(14))
4625                        .color(QUALITY_LEGENDARY)
4626                        .set(self.ids.tut_arrow_txt, ui_widgets);
4627                },
4628                Intro::Never => {
4629                    self.show.intro = false;
4630                },
4631            }
4632        }
4633        // TODO: Add event/stat based tutorial system
4634        if self.show.intro && !self.show.esc_menu {
4635            prof_span!("intro show");
4636            match global_state.settings.interface.intro_show {
4637                Intro::Show => {
4638                    if self.show.intro {
4639                        self.show.want_grab = false;
4640                        let quest_headline = i18n.get_msg("hud-temp_quest_headline");
4641                        let quest_text = i18n.get_msg("hud-temp_quest_text");
4642                        Image::new(self.imgs.quest_bg0)
4643                            .w_h(404.0, 858.0)
4644                            .middle_of(ui_widgets.window)
4645                            .set(self.ids.quest_bg, ui_widgets);
4646
4647                        Text::new(&quest_headline)
4648                            .mid_top_with_margin_on(self.ids.quest_bg, 310.0)
4649                            .font_size(self.fonts.cyri.scale(30))
4650                            .font_id(self.fonts.cyri.conrod_id)
4651                            .color(TEXT_BG)
4652                            .set(self.ids.q_headline_bg, ui_widgets);
4653                        Text::new(&quest_headline)
4654                            .bottom_left_with_margins_on(self.ids.q_headline_bg, 1.0, 1.0)
4655                            .font_size(self.fonts.cyri.scale(30))
4656                            .font_id(self.fonts.cyri.conrod_id)
4657                            .color(TEXT_COLOR)
4658                            .set(self.ids.q_headline, ui_widgets);
4659
4660                        Text::new(&quest_text)
4661                            .mid_top_with_margin_on(self.ids.quest_bg, 360.0)
4662                            .w(350.0)
4663                            .font_size(self.fonts.cyri.scale(17))
4664                            .font_id(self.fonts.cyri.conrod_id)
4665                            .color(TEXT_BG)
4666                            .set(self.ids.q_text_bg, ui_widgets);
4667                        Text::new(&quest_text)
4668                            .bottom_left_with_margins_on(self.ids.q_text_bg, 1.0, 1.0)
4669                            .w(350.0)
4670                            .font_size(self.fonts.cyri.scale(17))
4671                            .font_id(self.fonts.cyri.conrod_id)
4672                            .color(TEXT_COLOR)
4673                            .set(self.ids.q_text, ui_widgets);
4674
4675                        if Button::image(self.imgs.button)
4676                            .w_h(212.0, 52.0)
4677                            .hover_image(self.imgs.button_hover)
4678                            .press_image(self.imgs.button_press)
4679                            .mid_bottom_with_margin_on(self.ids.q_text_bg, -80.0)
4680                            .label(&i18n.get_msg("common-close"))
4681                            .label_font_id(self.fonts.cyri.conrod_id)
4682                            .label_font_size(self.fonts.cyri.scale(22))
4683                            .label_color(TEXT_COLOR)
4684                            .label_y(conrod_core::position::Relative::Scalar(2.0))
4685                            .set(self.ids.accept_button, ui_widgets)
4686                            .was_clicked()
4687                        {
4688                            self.show.intro = false;
4689                            events.push(Event::SettingsChange(
4690                                InterfaceChange::Intro(Intro::Never).into(),
4691                            ));
4692                            self.show.want_grab = true;
4693                        }
4694                        if !self.show.crafting && !self.show.bag {
4695                            Image::new(self.imgs.sp_indicator_arrow)
4696                                .w_h(20.0, 11.0)
4697                                .bottom_right_with_margins_on(
4698                                    ui_widgets.window,
4699                                    40.0 + arrow_ani as f64,
4700                                    205.0,
4701                                )
4702                                .color(Some(QUALITY_LEGENDARY))
4703                                .set(self.ids.tut_arrow, ui_widgets);
4704                            Text::new(&i18n.get_msg("hud-tutorial_elements"))
4705                                .mid_top_with_margin_on(self.ids.tut_arrow, -50.0)
4706                                .font_id(self.fonts.cyri.conrod_id)
4707                                .font_size(self.fonts.cyri.scale(40))
4708                                .color(BLACK)
4709                                .floating(true)
4710                                .set(self.ids.tut_arrow_txt_bg, ui_widgets);
4711                            Text::new(&i18n.get_msg("hud-tutorial_elements"))
4712                                .bottom_right_with_margins_on(self.ids.tut_arrow_txt_bg, 1.0, 1.0)
4713                                .font_id(self.fonts.cyri.conrod_id)
4714                                .font_size(self.fonts.cyri.scale(40))
4715                                .color(QUALITY_LEGENDARY)
4716                                .floating(true)
4717                                .set(self.ids.tut_arrow_txt, ui_widgets);
4718                        }
4719                    }
4720                },
4721                Intro::Never => {
4722                    self.show.intro = false;
4723                },
4724            }
4725        }
4726
4727        // if a menu is open, notify window so it can restrict GameInputs
4728        global_state.window.menu_open = !self.show.focus.is_empty();
4729
4730        self.menu_events.clear(); // clear all menu inputs after they have been read
4731        events
4732    }
4733
4734    fn show_bag(slot_manager: &mut slots::SlotManager, show: &mut Show, state: bool) {
4735        show.bag(state);
4736        if !state {
4737            slot_manager.idle();
4738        }
4739    }
4740
4741    pub fn add_failed_block_pickup(&mut self, pos: VolumePos, reason: HudCollectFailedReason) {
4742        self.failed_block_pickups
4743            .insert(pos, CollectFailedData::new(self.pulse, reason));
4744    }
4745
4746    pub fn add_failed_entity_pickup(&mut self, entity: EcsEntity, reason: HudCollectFailedReason) {
4747        self.failed_entity_pickups
4748            .insert(entity, CollectFailedData::new(self.pulse, reason));
4749    }
4750
4751    pub fn new_loot_message(&mut self, item: LootMessage) {
4752        self.new_loot_messages.push_back(item);
4753    }
4754
4755    pub fn dialogue(
4756        &mut self,
4757        sender: EcsEntity,
4758        player_pos: Vec3<f32>,
4759        dialogue: rtsim::Dialogue<true>,
4760        global_state: &mut GlobalState,
4761    ) {
4762        match dialogue.kind {
4763            rtsim::DialogueKind::Marker(marker) => {
4764                // Remove any existing markers with the same ID
4765                self.extra_markers.retain(|em| !em.marker.is_same(&marker));
4766                global_state.profile.tutorial.event_map_marker();
4767                self.extra_markers.push(map::ExtraMarker {
4768                    recv_pos: player_pos.xy(),
4769                    marker,
4770                });
4771            },
4772            rtsim::DialogueKind::End => {
4773                if self
4774                    .current_dialogue
4775                    .take_if(|(old_sender, _, _)| *old_sender == sender)
4776                    .is_some()
4777                {
4778                    self.show.quest(false);
4779                }
4780            },
4781            _ => {
4782                if !self.show.quest
4783                    || self
4784                        .current_dialogue
4785                        .as_ref()
4786                        .is_none_or(|(old_sender, _, _)| *old_sender == sender)
4787                {
4788                    self.show.quest(true);
4789                    self.current_dialogue = Some((sender, Instant::now(), dialogue));
4790                }
4791            },
4792        }
4793    }
4794
4795    pub fn new_message(&mut self, msg: comp::ChatMsg) { self.new_messages.push_back(msg); }
4796
4797    pub fn new_notification(&mut self, msg: UserNotification) {
4798        self.new_notifications.push_back(msg);
4799    }
4800
4801    pub fn set_scaling_mode(&mut self, scale_mode: ScaleMode) {
4802        self.ui.set_scaling_mode(scale_mode);
4803    }
4804
4805    pub fn scale_change(&mut self, scale_change: ScaleChange) -> ScaleMode {
4806        let scale_mode = match scale_change {
4807            ScaleChange::Adjust(scale) => ScaleMode::Absolute(scale),
4808            ScaleChange::ToAbsolute => self.ui.scale().scaling_mode_as_absolute(),
4809            ScaleChange::ToRelative => self.ui.scale().scaling_mode_as_relative(),
4810        };
4811        self.ui.set_scaling_mode(scale_mode);
4812        scale_mode
4813    }
4814
4815    /// Checks if a TextEdit widget has the keyboard captured.
4816    fn typing(&self) -> bool { Hud::is_captured::<widget::TextEdit>(&self.ui.ui) }
4817
4818    /// Checks if a widget of type `W` has captured the keyboard
4819    fn is_captured<W: Widget>(ui: &conrod_core::Ui) -> bool {
4820        if let Some(id) = ui.global_input().current.widget_capturing_keyboard {
4821            ui.widget_graph()
4822                .widget(id)
4823                .filter(|c| c.type_id == std::any::TypeId::of::<<W as Widget>::State>())
4824                .is_some()
4825        } else {
4826            false
4827        }
4828    }
4829
4830    pub fn handle_event(
4831        &mut self,
4832        event: WinEvent,
4833        global_state: &mut GlobalState,
4834        client_inventory: Option<&comp::Inventory>,
4835    ) -> bool {
4836        // Helper
4837        fn handle_slot(
4838            slot: hotbar::Slot,
4839            state: bool,
4840            events: &mut Vec<Event>,
4841            slot_manager: &mut slots::SlotManager,
4842            hotbar: &mut hotbar::State,
4843            client_inventory: Option<&comp::Inventory>,
4844        ) {
4845            use slots::InventorySlot;
4846            if let Some(slots::SlotKind::Inventory(InventorySlot {
4847                slot: Slot::Inventory(i),
4848                ours: true,
4849                ..
4850            })) = slot_manager.selected()
4851            {
4852                if let Some(item) = client_inventory.and_then(|inv| inv.get(i)) {
4853                    hotbar.add_inventory_link(slot, item);
4854                    events.push(Event::ChangeHotbarState(Box::new(hotbar.to_owned())));
4855                    slot_manager.idle();
4856                }
4857            } else {
4858                let just_pressed = hotbar.process_input(slot, state);
4859                hotbar.get(slot).map(|s| match s {
4860                    hotbar::SlotContents::Inventory(i, _) => {
4861                        if just_pressed && let Some(inv) = client_inventory {
4862                            // If the item in the inactive main hand is the same as the item
4863                            // pressed in the hotbar, then swap active and inactive hands
4864                            // instead of looking for the item
4865                            // in the inventory
4866                            if inv
4867                                .equipped(comp::slot::EquipSlot::InactiveMainhand)
4868                                .is_some_and(|item| item.item_hash() == i)
4869                            {
4870                                events.push(Event::SwapEquippedWeapons);
4871                            } else if let Some(slot) = inv.get_slot_from_hash(i) {
4872                                events.push(Event::UseSlot {
4873                                    slot: comp::slot::Slot::Inventory(slot),
4874                                    bypass_dialog: false,
4875                                });
4876                            }
4877                        }
4878                    },
4879                    hotbar::SlotContents::Ability(idx) => {
4880                        events.push(Event::Ability { idx, state })
4881                    },
4882                });
4883            }
4884        }
4885
4886        #[instrument(skip(show, global_state))]
4887        fn handle_map_zoom(
4888            factor: f64,
4889            world_size: Vec2<u32>,
4890            show: &Show,
4891            global_state: &mut GlobalState,
4892        ) -> bool {
4893            trace!("Handling map Zoom");
4894
4895            let max_zoom = world_size.reduce_partial_max() as f64;
4896
4897            if show.map {
4898                let new_zoom_lvl = (global_state.settings.interface.map_zoom * factor)
4899                    .clamped(1.25, max_zoom / 64.0);
4900                global_state.settings.interface.map_zoom = new_zoom_lvl;
4901            } else if global_state.settings.interface.minimap_show {
4902                // Duplicated code from voxygen/src/hud/minimap.rs:522 in the update fn
4903                // TODO: Consolidate minimap zooming, because having duplicate handlers for
4904                // hotkey and interface is error prone. Find the other occurrence by searching
4905                // for this comment. Don't forget to update the code in
4906                // minimap.rs when updating this!
4907                let min_zoom = 1.0;
4908                let max_zoom = world_size
4909                    .reduce_partial_max() as f64/*.min(f64::MAX)*/;
4910
4911                let new_zoom_lvl = (global_state.settings.interface.minimap_zoom * factor)
4912                    .clamped(min_zoom, max_zoom);
4913                global_state.settings.interface.minimap_zoom = new_zoom_lvl;
4914            }
4915
4916            show.map && global_state.settings.interface.minimap_show
4917        }
4918
4919        let cursor_grabbed = global_state.window.is_cursor_grabbed();
4920        let handled = match event {
4921            WinEvent::Ui(event) => {
4922                if (self.typing() && event.is_keyboard() && self.show.ui)
4923                    || !(cursor_grabbed && event.is_keyboard_or_mouse())
4924                {
4925                    self.ui.handle_event(event);
4926                }
4927                true
4928            },
4929            WinEvent::ScaleFactorChanged(scale_factor) => {
4930                self.ui.scale_factor_changed(scale_factor);
4931                false
4932            },
4933            WinEvent::InputUpdate(GameInput::ToggleInterface, true) if !self.typing() => {
4934                self.show.toggle_ui();
4935                true
4936            },
4937            WinEvent::InputUpdate(GameInput::ToggleCursor, true) if !self.typing() => {
4938                self.force_ungrab = !self.force_ungrab;
4939                true
4940            },
4941            WinEvent::InputUpdate(GameInput::AcceptGroupInvite, true) if !self.typing() => {
4942                if let Some(prompt_dialog) = &mut self.show.prompt_dialog {
4943                    prompt_dialog.set_outcome_via_keypress(true);
4944                    true
4945                } else {
4946                    false
4947                }
4948            },
4949            WinEvent::InputUpdate(GameInput::DeclineGroupInvite, true) if !self.typing() => {
4950                if let Some(prompt_dialog) = &mut self.show.prompt_dialog {
4951                    prompt_dialog.set_outcome_via_keypress(false);
4952                    true
4953                } else {
4954                    false
4955                }
4956            },
4957
4958            // If not showing the ui don't allow keys that change the ui state but do listen for
4959            // hotbar keys
4960            WinEvent::InputUpdate(key, state) if !self.show.ui => {
4961                if let Some(slot) = try_hotbar_slot_from_input(key) {
4962                    handle_slot(
4963                        slot,
4964                        state,
4965                        &mut self.events,
4966                        &mut self.slot_manager,
4967                        &mut self.hotbar,
4968                        client_inventory,
4969                    );
4970                    true
4971                } else {
4972                    false
4973                }
4974            },
4975
4976            WinEvent::Zoom(_) => !cursor_grabbed && !self.ui.no_widget_capturing_mouse(),
4977
4978            WinEvent::InputUpdate(GameInput::Chat, true) => {
4979                self.ui.focus_widget(if self.typing() {
4980                    None
4981                } else {
4982                    self.force_chat = true;
4983                    Some(self.ids.chat)
4984                });
4985                true
4986            },
4987            WinEvent::InputUpdate(GameInput::Escape, true) => {
4988                if self.typing() {
4989                    self.ui.focus_widget(None);
4990                    self.force_chat = false;
4991                } else if self.show.trade {
4992                    self.events.push(Event::TradeAction(TradeAction::Decline));
4993                } else {
4994                    // Close windows on esc
4995                    if self.show.bag {
4996                        self.slot_manager.idle();
4997                    }
4998                    self.show.toggle_windows(global_state);
4999                    self.force_ungrab = false;
5000                }
5001                true
5002            },
5003
5004            // Press key while not typing
5005            // MenuInput
5006            WinEvent::MenuInput(key, state) => {
5007                if self.typing() {
5008                    // Close an opened chat using MenuInputs
5009                    if key == MenuInput::Back {
5010                        self.ui.focus_widget(None);
5011                        self.force_chat = false;
5012                        true
5013                    } else {
5014                        false
5015                    }
5016                } else {
5017                    // Pass MenuInputs along to the UI
5018                    if state {
5019                        self.menu_events.push(key);
5020                        true
5021                    } else {
5022                        false
5023                    }
5024                }
5025            },
5026
5027            // GameInput
5028            WinEvent::InputUpdate(key, state) if !self.typing() => {
5029                let gs_audio = &global_state.settings.audio;
5030                let mut toggle_mute = |audio: Audio| {
5031                    self.events
5032                        .push(Event::SettingsChange(SettingsChange::Audio(audio)));
5033                    true
5034                };
5035
5036                match key {
5037                    GameInput::Command if state => {
5038                        self.force_chat_input = Some("/".to_owned());
5039                        self.force_chat_cursor = Some(Index { line: 0, char: 1 });
5040                        self.force_chat = true;
5041                        self.ui.focus_widget(Some(self.ids.chat));
5042                        true
5043                    },
5044                    GameInput::Map if state => {
5045                        global_state.profile.tutorial.event_open_map();
5046                        self.show.toggle_map();
5047                        true
5048                    },
5049                    GameInput::Inventory if state => {
5050                        global_state.profile.tutorial.event_open_inventory();
5051                        let state = !self.show.bag;
5052                        Self::show_bag(&mut self.slot_manager, &mut self.show, state);
5053                        true
5054                    },
5055                    GameInput::Social if state => {
5056                        self.show.toggle_social();
5057                        true
5058                    },
5059                    GameInput::Crafting if state => {
5060                        global_state.profile.tutorial.event_open_crafting();
5061                        self.show.toggle_crafting();
5062                        true
5063                    },
5064                    GameInput::Diary if state => {
5065                        global_state.profile.tutorial.event_open_diary();
5066                        self.show.toggle_diary();
5067                        true
5068                    },
5069                    GameInput::Settings if state => {
5070                        self.show.toggle_settings(global_state);
5071                        true
5072                    },
5073                    GameInput::Controls if state => {
5074                        self.show.toggle_settings(global_state);
5075                        self.show.settings_tab = SettingsTab::Controls;
5076                        true
5077                    },
5078                    GameInput::ToggleDebug if state => {
5079                        global_state.settings.interface.toggle_debug =
5080                            !global_state.settings.interface.toggle_debug;
5081                        true
5082                    },
5083                    #[cfg(feature = "egui-ui")]
5084                    GameInput::ToggleEguiDebug if state => {
5085                        global_state.settings.interface.toggle_egui_debug =
5086                            !global_state.settings.interface.toggle_egui_debug;
5087                        true
5088                    },
5089                    GameInput::ToggleChat if state => {
5090                        global_state.settings.interface.toggle_chat =
5091                            !global_state.settings.interface.toggle_chat;
5092                        true
5093                    },
5094                    GameInput::ToggleIngameUi if state => {
5095                        self.show.ingame = !self.show.ingame;
5096                        true
5097                    },
5098                    GameInput::MapZoomIn if state => {
5099                        handle_map_zoom(2.0, self.world_map.1, &self.show, global_state)
5100                    },
5101                    GameInput::MapZoomOut if state => {
5102                        handle_map_zoom(0.5, self.world_map.1, &self.show, global_state)
5103                    },
5104                    GameInput::MuteMaster if state => {
5105                        toggle_mute(Audio::MuteMasterVolume(!gs_audio.master_volume.muted))
5106                    },
5107                    GameInput::MuteInactiveMaster if state => {
5108                        toggle_mute(Audio::MuteInactiveMasterVolume(
5109                            !gs_audio.inactive_master_volume_perc.muted,
5110                        ))
5111                    },
5112                    GameInput::MuteMusic if state => {
5113                        toggle_mute(Audio::MuteMusicVolume(!gs_audio.music_volume.muted))
5114                    },
5115                    GameInput::MuteSfx if state => {
5116                        toggle_mute(Audio::MuteSfxVolume(!gs_audio.sfx_volume.muted))
5117                    },
5118                    GameInput::MuteAmbience if state => {
5119                        toggle_mute(Audio::MuteAmbienceVolume(!gs_audio.ambience_volume.muted))
5120                    },
5121                    GameInput::Interact if state => {
5122                        // Send ACKs during conversation
5123                        if let Some((sender, _, dialogue)) = &self.current_dialogue
5124                            && let rtsim::DialogueKind::Statement { tag, .. } = dialogue.kind
5125                        {
5126                            self.events.push(Event::Dialogue(*sender, rtsim::Dialogue {
5127                                id: dialogue.id,
5128                                kind: rtsim::DialogueKind::Ack { tag },
5129                            }));
5130                            true
5131                        } else {
5132                            false
5133                        }
5134                    },
5135                    GameInput::CurrentSlot => {
5136                        let current_slot = self.hotbar.currently_selected_slot;
5137                        handle_slot(
5138                            current_slot,
5139                            state,
5140                            &mut self.events,
5141                            &mut self.slot_manager,
5142                            &mut self.hotbar,
5143                            client_inventory,
5144                        );
5145                        true
5146                    },
5147                    GameInput::NextSlot if state => {
5148                        self.hotbar.currently_selected_slot.next_slot();
5149                        true
5150                    },
5151                    GameInput::PreviousSlot if state => {
5152                        self.hotbar.currently_selected_slot.previous_slot();
5153                        true
5154                    },
5155                    // Skillbar
5156                    input => {
5157                        if let Some(slot) = try_hotbar_slot_from_input(input) {
5158                            handle_slot(
5159                                slot,
5160                                state,
5161                                &mut self.events,
5162                                &mut self.slot_manager,
5163                                &mut self.hotbar,
5164                                client_inventory,
5165                            );
5166                            true
5167                        } else {
5168                            false
5169                        }
5170                    },
5171                }
5172            },
5173            // Else the player is typing in chat
5174            WinEvent::InputUpdate(_key, _) => self.typing(),
5175            WinEvent::Focused(state) => {
5176                self.force_ungrab = !state;
5177                true
5178            },
5179            WinEvent::Moved(_) => {
5180                // Prevent the cursor from being grabbed while the window is being moved as this
5181                // causes the window to move erratically
5182                // TODO: this creates an issue where if you move the window then you need to
5183                // close a menu to re-grab the mouse (and if one isn't already
5184                // open you need to open and close a menu)
5185                self.show.want_grab = false;
5186                true
5187            },
5188            _ => false,
5189        };
5190        // Handle cursor grab.
5191        global_state
5192            .window
5193            .grab_cursor(!self.force_ungrab && self.show.want_grab);
5194
5195        handled
5196    }
5197
5198    pub fn maintain(
5199        &mut self,
5200        client: &Client,
5201        global_state: &mut GlobalState,
5202        debug_info: &Option<DebugInfo>,
5203        camera: &Camera,
5204        dt: Duration,
5205        info: HudInfo,
5206        interactable_map: (
5207            HashMap<specs::Entity, Vec<interactable::EntityInteraction>>,
5208            HashMap<VolumePos, (Block, Vec<&interactable::BlockInteraction>)>,
5209        ),
5210    ) -> Vec<Event> {
5211        span!(_guard, "maintain", "Hud::maintain");
5212
5213        // Remove extra map markers that we've wandered a long distance away from
5214        if let Some(pos) = client.position() {
5215            self.extra_markers.retain(|em| {
5216                const EXTRA_DISTANCE: f32 = 100.0;
5217                em.marker.wpos.distance(pos.xy())
5218                    < em.recv_pos.distance(em.marker.wpos) + EXTRA_DISTANCE
5219            });
5220        }
5221
5222        // conrod eats tabs. Un-eat a tabstop so tab completion can work
5223        if self.ui.ui.global_input().events().any(|event| {
5224            use conrod_core::{event, input};
5225            matches!(
5226                event,
5227                /* event::Event::Raw(event::Input::Press(input::Button::Keyboard(input::Key::
5228                 * Tab))) | */
5229                event::Event::Ui(event::Ui::Press(_, event::Press {
5230                    button: event::Button::Keyboard(input::Key::Tab),
5231                    ..
5232                },))
5233            )
5234        }) {
5235            self.ui
5236                .ui
5237                .handle_event(conrod_core::event::Input::Text("\t".to_string()));
5238        }
5239
5240        // Stop selecting a sprite to perform crafting with when out of range or sprite
5241        // has been removed
5242        self.show.crafting_fields.craft_sprite =
5243            self.show
5244                .crafting_fields
5245                .craft_sprite
5246                .filter(|(pos, sprite)| {
5247                    self.show.crafting
5248                        && if let Some(player_pos) = client.position() {
5249                            pos.get_block_and_transform(
5250                                &client.state().terrain(),
5251                                &client.state().ecs().read_resource(),
5252                                |e| {
5253                                    client
5254                                        .state()
5255                                        .read_storage::<vcomp::Interpolated>()
5256                                        .get(e)
5257                                        .map(|interpolated| {
5258                                            (comp::Pos(interpolated.pos), interpolated.ori)
5259                                        })
5260                                },
5261                                &client.state().read_storage(),
5262                            )
5263                            .is_some_and(|(mat, block)| {
5264                                block.get_sprite() == Some(*sprite)
5265                                    && mat.mul_point(Vec3::broadcast(0.5)).distance(player_pos)
5266                                        < MAX_PICKUP_RANGE
5267                            })
5268                        } else {
5269                            false
5270                        }
5271                });
5272
5273        // Optimization: skip maintaining UI when it's off.
5274        if !self.show.ui {
5275            return std::mem::take(&mut self.events);
5276        }
5277
5278        if let Some(maybe_id) = self.to_focus.take() {
5279            self.ui.focus_widget(maybe_id);
5280        }
5281        let events = self.update_layout(
5282            client,
5283            global_state,
5284            debug_info,
5285            dt,
5286            info,
5287            camera,
5288            interactable_map,
5289        );
5290        let camera::Dependents {
5291            view_mat, proj_mat, ..
5292        } = camera.dependents();
5293        let focus_off = camera.get_focus_pos().map(f32::trunc);
5294
5295        // Check if item images need to be reloaded
5296        self.item_imgs.reload_if_changed(&mut self.ui);
5297        // TODO: using a thread pool in the obvious way for speeding up map zoom results
5298        // in flickering artifacts, figure out a better way to make use of the
5299        // thread pool
5300        let _pool = client.state().ecs().read_resource::<SlowJobPool>();
5301        self.ui.maintain(
5302            global_state.window.renderer_mut(),
5303            None,
5304            //Some(&pool),
5305            Some(proj_mat * view_mat * Mat4::translation_3d(-focus_off)),
5306        );
5307
5308        events
5309    }
5310
5311    #[inline]
5312    pub fn clear_cursor(&mut self) { self.slot_manager.idle(); }
5313
5314    pub fn render<'a>(&'a self, drawer: &mut UiDrawer<'_, 'a>) {
5315        span!(_guard, "render", "Hud::render");
5316        // Don't show anything if the UI is toggled off.
5317        if self.show.ui {
5318            self.ui.render(drawer);
5319        }
5320    }
5321
5322    pub fn free_look(&mut self, free_look: bool) { self.show.free_look = free_look; }
5323
5324    pub fn auto_walk(&mut self, auto_walk: bool) { self.show.auto_walk = auto_walk; }
5325
5326    pub fn camera_clamp(&mut self, camera_clamp: bool) { self.show.camera_clamp = camera_clamp; }
5327
5328    /// Remind the player camera zoom is currently locked, for example if they
5329    /// are trying to zoom.
5330    pub fn zoom_lock_reminder(&mut self) {
5331        if self.show.zoom_lock.reason.is_none() {
5332            self.show.zoom_lock = ChangeNotification::from_reason(NotificationReason::Remind);
5333        }
5334    }
5335
5336    /// Start showing a temporary notification ([ChangeNotification]) that zoom
5337    /// lock was toggled on/off.
5338    pub fn zoom_lock_toggle(&mut self, state: bool) {
5339        self.show.zoom_lock = ChangeNotification::from_state(state);
5340    }
5341
5342    pub fn show_content_bubble(&mut self, pos: Vec3<f32>, content: comp::Content) {
5343        self.content_bubbles.push((
5344            pos,
5345            comp::SpeechBubble::new(content, comp::SpeechBubbleType::None),
5346        ));
5347    }
5348
5349    pub fn handle_outcome(
5350        &mut self,
5351        outcome: &Outcome,
5352        scene_data: &SceneData,
5353        global_state: &GlobalState,
5354    ) {
5355        let client = scene_data.client;
5356        let interface = &global_state.settings.interface;
5357        match outcome {
5358            Outcome::ExpChange { uid, exp, xp_pools } => {
5359                let ecs = client.state().ecs();
5360                let uids = ecs.read_storage::<Uid>();
5361                let me = scene_data.viewpoint_entity;
5362
5363                if uids.get(me).is_some_and(|me| *me == *uid) {
5364                    match self.floaters.exp_floaters.last_mut() {
5365                        Some(floater)
5366                            if floater.timer
5367                                > (EXP_FLOATER_LIFETIME - EXP_ACCUMULATION_DURATION)
5368                                && global_state.settings.interface.accum_experience
5369                                && floater.owner == *uid =>
5370                        {
5371                            floater.jump_timer = 0.0;
5372                            floater.exp_change += *exp;
5373                        },
5374                        _ => self.floaters.exp_floaters.push(ExpFloater {
5375                            // Store the owner as to not accumulate old experience floaters
5376                            owner: *uid,
5377                            exp_change: *exp,
5378                            timer: EXP_FLOATER_LIFETIME,
5379                            jump_timer: 0.0,
5380                            rand_offset: rand::rng().random::<(f32, f32)>(),
5381                            xp_pools: xp_pools.clone(),
5382                        }),
5383                    }
5384                }
5385            },
5386            Outcome::SkillPointGain {
5387                uid,
5388                skill_tree,
5389                total_points,
5390                ..
5391            } => {
5392                let ecs = client.state().ecs();
5393                let uids = ecs.read_storage::<Uid>();
5394                let me = scene_data.viewpoint_entity;
5395
5396                if uids.get(me).is_some_and(|me| *me == *uid) {
5397                    self.floaters.skill_point_displays.push(SkillPointGain {
5398                        skill_tree: *skill_tree,
5399                        total_points: *total_points,
5400                        timer: 5.0,
5401                    });
5402                }
5403            },
5404            Outcome::ComboChange { uid, combo } => {
5405                let ecs = client.state().ecs();
5406                let uids = ecs.read_storage::<Uid>();
5407                let me = scene_data.viewpoint_entity;
5408
5409                if uids.get(me).is_some_and(|me| *me == *uid) {
5410                    self.floaters.combo_floater = Some(ComboFloater {
5411                        combo: *combo,
5412                        timer: comp::combo::COMBO_DECAY_START,
5413                    });
5414                }
5415            },
5416            Outcome::Block { uid, parry, .. } if *parry => {
5417                let ecs = client.state().ecs();
5418                let uids = ecs.read_storage::<Uid>();
5419                let me = scene_data.viewpoint_entity;
5420
5421                if uids.get(me).is_some_and(|me| *me == *uid) {
5422                    self.floaters
5423                        .block_floaters
5424                        .push(BlockFloater { timer: 1.0 });
5425                }
5426            },
5427            Outcome::HealthChange { info, .. } => {
5428                let ecs = client.state().ecs();
5429                let mut hp_floater_lists = ecs.write_storage::<HpFloaterList>();
5430                let uids = ecs.read_storage::<Uid>();
5431                let me = scene_data.viewpoint_entity;
5432                let my_uid = uids.get(me);
5433
5434                if let Some(entity) = ecs.entity_from_uid(info.target)
5435                    && let Some(floater_list) = hp_floater_lists.get_mut(entity)
5436                {
5437                    let hit_me = my_uid.is_some_and(|&uid| {
5438                        (info.target == uid) && global_state.settings.interface.sct_inc_dmg
5439                    });
5440                    if match info.by {
5441                        Some(by) => {
5442                            let by_me = my_uid.is_some_and(|&uid| by.uid() == uid);
5443                            // If the attack was by me also reset this timer
5444                            if by_me {
5445                                floater_list.time_since_last_dmg_by_me = Some(0.0);
5446                            }
5447                            hit_me || by_me
5448                        },
5449                        None => hit_me,
5450                    } {
5451                        // Group up damage from the same tick and instance number
5452                        for floater in floater_list.floaters.iter_mut().rev() {
5453                            if floater.timer > 0.0 {
5454                                break;
5455                            }
5456                            if floater.info.instance == info.instance
5457                                    // Group up precision hits and regular attacks for incoming damage
5458                                    && (hit_me
5459                                        || floater.info.precise
5460                                            == info.precise)
5461                            {
5462                                floater.info.amount += info.amount;
5463                                if info.precise {
5464                                    floater.info.precise = info.precise
5465                                }
5466                                return;
5467                            }
5468                        }
5469
5470                        // To separate healing and damage floaters alongside the precise and
5471                        // non-precise ones
5472                        let last_floater = if !info.precise || hit_me {
5473                            floater_list.floaters.iter_mut().rev().find(|f| {
5474                                (if info.amount < 0.0 {
5475                                        f.info.amount < 0.0
5476                                    } else {
5477                                        f.info.amount > 0.0
5478                                    }) && f.timer
5479                                        < if hit_me {
5480                                            interface.sct_inc_dmg_accum_duration
5481                                        } else {
5482                                            interface.sct_dmg_accum_duration
5483                                        }
5484                                    // Ignore precise floaters, unless the damage is incoming
5485                                    && (hit_me || !f.info.precise)
5486                            })
5487                        } else {
5488                            None
5489                        };
5490
5491                        match last_floater {
5492                            Some(f) => {
5493                                f.jump_timer = 0.0;
5494                                f.info.amount += info.amount;
5495                                f.info.precise = info.precise;
5496                            },
5497                            _ => {
5498                                floater_list.floaters.push(HpFloater {
5499                                    timer: 0.0,
5500                                    jump_timer: 0.0,
5501                                    info: *info,
5502                                    rand: rand::random(),
5503                                });
5504                            },
5505                        }
5506                    }
5507                }
5508            },
5509
5510            _ => {},
5511        }
5512    }
5513}
5514// Get item qualities of equipped items and assign a tooltip title/frame color
5515pub fn get_quality_col(quality: Quality) -> Color {
5516    match quality {
5517        Quality::Low => QUALITY_LOW,
5518        Quality::Common => QUALITY_COMMON,
5519        Quality::Moderate => QUALITY_MODERATE,
5520        Quality::High => QUALITY_HIGH,
5521        Quality::Epic => QUALITY_EPIC,
5522        Quality::Legendary => QUALITY_LEGENDARY,
5523        Quality::Artifact => QUALITY_ARTIFACT,
5524        Quality::Debug => QUALITY_DEBUG,
5525    }
5526}
5527
5528fn try_hotbar_slot_from_input(input: GameInput) -> Option<hotbar::Slot> {
5529    Some(match input {
5530        GameInput::Slot1 => hotbar::Slot::One,
5531        GameInput::Slot2 => hotbar::Slot::Two,
5532        GameInput::Slot3 => hotbar::Slot::Three,
5533        GameInput::Slot4 => hotbar::Slot::Four,
5534        GameInput::Slot5 => hotbar::Slot::Five,
5535        GameInput::Slot6 => hotbar::Slot::Six,
5536        GameInput::Slot7 => hotbar::Slot::Seven,
5537        GameInput::Slot8 => hotbar::Slot::Eight,
5538        GameInput::Slot9 => hotbar::Slot::Nine,
5539        GameInput::Slot10 => hotbar::Slot::Ten,
5540        _ => return None,
5541    })
5542}
5543
5544pub fn cr_color(combat_rating: f32) -> Color {
5545    let common = 2.0;
5546    let moderate = 3.5;
5547    let high = 6.5;
5548    let epic = 8.5;
5549    let legendary = 10.4;
5550    let artifact = 122.0;
5551    let debug = 200.0;
5552
5553    match combat_rating {
5554        x if (0.0..common).contains(&x) => QUALITY_LOW,
5555        x if (common..moderate).contains(&x) => QUALITY_COMMON,
5556        x if (moderate..high).contains(&x) => QUALITY_MODERATE,
5557        x if (high..epic).contains(&x) => QUALITY_HIGH,
5558        x if (epic..legendary).contains(&x) => QUALITY_EPIC,
5559        x if (legendary..artifact).contains(&x) => QUALITY_LEGENDARY,
5560        x if (artifact..debug).contains(&x) => QUALITY_ARTIFACT,
5561        x if x >= debug => QUALITY_DEBUG,
5562        _ => XP_COLOR,
5563    }
5564}
5565
5566pub fn get_buff_image(buff: BuffKind, imgs: &Imgs) -> conrod_core::image::Id {
5567    match buff {
5568        // Buffs
5569        BuffKind::Regeneration => imgs.buff_plus_0,
5570        BuffKind::Saturation => imgs.buff_saturation_0,
5571        BuffKind::Potion => imgs.buff_potion_0,
5572        // TODO: Need unique image for Agility (uses same as Hastened atm)
5573        BuffKind::Agility => imgs.buff_haste_0,
5574        BuffKind::RestingHeal => imgs.buff_resting_heal_0,
5575        BuffKind::EnergyRegen => imgs.buff_energyplus_0,
5576        BuffKind::ComboGeneration => imgs.buff_fury,
5577        BuffKind::IncreaseMaxEnergy => imgs.buff_energyplus_0,
5578        BuffKind::IncreaseMaxHealth => imgs.buff_healthplus_0,
5579        BuffKind::Invulnerability => imgs.buff_invincibility_0,
5580        BuffKind::ProtectingWard => imgs.buff_dmg_red_0,
5581        BuffKind::Frenzied => imgs.buff_frenzy_0,
5582        BuffKind::Hastened => imgs.buff_haste_0,
5583        BuffKind::Fortitude => imgs.buff_fortitude_0,
5584        BuffKind::Reckless => imgs.buff_reckless,
5585        BuffKind::Flame => imgs.buff_flame,
5586        BuffKind::Frigid => imgs.buff_frigid,
5587        BuffKind::Lifesteal => imgs.buff_lifesteal,
5588        BuffKind::Resilience => imgs.buff_resilience,
5589        // TODO: Get image
5590        // BuffKind::SalamanderAspect => imgs.debuff_burning_0,
5591        BuffKind::ImminentCritical => imgs.buff_imminentcritical,
5592        BuffKind::Fury => imgs.buff_fury,
5593        BuffKind::Sunderer => imgs.buff_sunderer,
5594        BuffKind::Defiance => imgs.buff_defiance,
5595        BuffKind::Bloodfeast => imgs.buff_plus_0,
5596        BuffKind::Berserk => imgs.buff_reckless,
5597        BuffKind::ScornfulTaunt => imgs.buff_scornfultaunt,
5598        BuffKind::Tenacity => imgs.buff_tenacity,
5599        BuffKind::StormChaser => imgs.buff_stormchaser,
5600        BuffKind::EagleEye => imgs.buff_eagleeye,
5601        BuffKind::ArdentHunt => imgs.buff_ardenthunt,
5602        BuffKind::IgniteArrow => imgs.bow_ignite_arrow,
5603        BuffKind::FreezeArrow => imgs.bow_freeze_arrow,
5604        BuffKind::DrenchArrow => imgs.bow_drench_arrow,
5605        BuffKind::JoltArrow => imgs.bow_jolt_arrow,
5606        //  Debuffs
5607        BuffKind::Bleeding => imgs.debuff_bleed_0,
5608        BuffKind::Cursed => imgs.debuff_cursed_0,
5609        BuffKind::Burning => imgs.debuff_burning_0,
5610        BuffKind::Crippled => imgs.debuff_crippled_0,
5611        BuffKind::Frozen => imgs.debuff_frozen_0,
5612        BuffKind::Wet => imgs.debuff_wet_0,
5613        BuffKind::Ensnared => imgs.debuff_ensnared_0,
5614        BuffKind::Poisoned => imgs.debuff_poisoned_0,
5615        BuffKind::Parried => imgs.debuff_parried_0,
5616        BuffKind::PotionSickness => imgs.debuff_potionsickness_0,
5617        BuffKind::Polymorphed => imgs.debuff_polymorphed_0,
5618        BuffKind::Heatstroke => imgs.debuff_heatstroke_0,
5619        BuffKind::Rooted => imgs.debuff_rooted_0,
5620        BuffKind::Winded => imgs.debuff_winded_0,
5621        BuffKind::Amnesia => imgs.debuff_amnesia_0,
5622        BuffKind::OffBalance => imgs.debuff_offbalance_0,
5623        BuffKind::Chilled => imgs.debuff_chilled,
5624    }
5625}
5626
5627pub fn get_sprite_desc(
5628    sprite: SpriteKind,
5629    localized_strings: &Localization,
5630) -> Option<Cow<'_, str>> {
5631    let i18n_key = match sprite {
5632        SpriteKind::Empty | SpriteKind::GlassBarrier => return None,
5633        SpriteKind::Anvil => "hud-crafting-anvil",
5634        SpriteKind::Cauldron => "hud-crafting-cauldron",
5635        SpriteKind::CookingPot => "hud-crafting-cooking_pot",
5636        SpriteKind::RepairBench => "hud-crafting-repair_bench",
5637        SpriteKind::CraftingBench => "hud-crafting-crafting_bench",
5638        SpriteKind::Forge => "hud-crafting-forge",
5639        SpriteKind::Loom => "hud-crafting-loom",
5640        SpriteKind::SpinningWheel => "hud-crafting-spinning_wheel",
5641        SpriteKind::TanningRack => "hud-crafting-tanning_rack",
5642        SpriteKind::DismantlingBench => "hud-crafting-salvaging_station",
5643        SpriteKind::ChestBuried
5644        | SpriteKind::Chest
5645        | SpriteKind::CommonLockedChest
5646        | SpriteKind::CoralChest
5647        | SpriteKind::DungeonChest0
5648        | SpriteKind::DungeonChest1
5649        | SpriteKind::DungeonChest2
5650        | SpriteKind::DungeonChest3
5651        | SpriteKind::DungeonChest4
5652        | SpriteKind::DungeonChest5
5653        | SpriteKind::SahaginChest
5654        | SpriteKind::TerracottaChest => "common-sprite-chest",
5655        SpriteKind::Mud => "common-sprite-mud",
5656        SpriteKind::Grave => "common-sprite-grave",
5657        SpriteKind::Crate => "common-sprite-crate",
5658        _ => return None,
5659    };
5660    Some(localized_strings.get_msg(i18n_key))
5661}
5662
5663pub fn angle_of_attack_text(
5664    fluid: Option<comp::Fluid>,
5665    velocity: Option<comp::Vel>,
5666    character_state: Option<&comp::CharacterState>,
5667) -> String {
5668    use comp::CharacterState;
5669
5670    let glider_ori = if let Some(CharacterState::Glide(data)) = character_state {
5671        data.ori
5672    } else {
5673        return "Angle of Attack: Not gliding".to_owned();
5674    };
5675
5676    let fluid = if let Some(fluid) = fluid {
5677        fluid
5678    } else {
5679        return "Angle of Attack: Not in fluid".to_owned();
5680    };
5681
5682    let velocity = if let Some(velocity) = velocity {
5683        velocity
5684    } else {
5685        return "Angle of Attack: Player has no vel component".to_owned();
5686    };
5687    let rel_flow = fluid.relative_flow(&velocity).0;
5688    let v_sq = rel_flow.magnitude_squared();
5689
5690    if v_sq.abs() > 0.0001 {
5691        let rel_flow_dir = Dir::new(rel_flow / v_sq.sqrt());
5692        let aoe = fluid_dynamics::angle_of_attack(&glider_ori, &rel_flow_dir);
5693        let (rel_x, rel_y, rel_z) = (rel_flow.x, rel_flow.y, rel_flow.z);
5694        format!(
5695            "Angle of Attack: {:.1} ({:.1},{:.1},{:.1})",
5696            aoe.to_degrees(),
5697            rel_x,
5698            rel_y,
5699            rel_z
5700        )
5701    } else {
5702        "Angle of Attack: Not moving".to_owned()
5703    }
5704}
5705
5706fn air_velocity(fluid: Option<comp::Fluid>) -> String {
5707    if let Some(comp::Fluid::Air { vel: air_vel, .. }) = fluid {
5708        format!(
5709            "Air Velocity: ({:.1}, {:.1}, {:.1})",
5710            air_vel.0.x, air_vel.0.y, air_vel.0.z
5711        )
5712    } else {
5713        "Air Velocity: Not in Air".to_owned()
5714    }
5715}
5716
5717/// Converts multiplier to percentage.
5718/// NOTE: floats are not the most precise type.
5719///
5720/// # Examples
5721/// ```
5722/// use veloren_voxygen::hud::multiplier_to_percentage;
5723///
5724/// let positive = multiplier_to_percentage(1.05);
5725/// assert!((positive - 5.0).abs() < 0.0001);
5726/// let negative = multiplier_to_percentage(0.85);
5727/// assert!((negative - (-15.0)).abs() < 0.0001);
5728/// ```
5729pub fn multiplier_to_percentage(value: f32) -> f32 { value * 100.0 - 100.0 }