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