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