veloren_voxygen/hud/
mod.rs

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