veloren_voxygen/hud/
mod.rs

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