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