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