Skip to main content

veloren_voxygen/hud/
mod.rs

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