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