veloren_voxygen/hud/
mod.rs

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