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