veloren_voxygen/hud/
mod.rs

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