veloren_voxygen/hud/
mod.rs

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