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