1#![expect(non_local_definitions)] mod 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);
157const 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);
161const 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);
170const 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
176const QUALITY_LOW: Color = Color::Rgba(0.60, 0.60, 0.60, 1.0); const QUALITY_COMMON: Color = Color::Rgba(0.79, 1.00, 1.00, 1.0); const QUALITY_MODERATE: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0); const QUALITY_HIGH: Color = Color::Rgba(0.18, 0.32, 0.9, 1.0); const QUALITY_EPIC: Color = Color::Rgba(0.58, 0.29, 0.93, 1.0); const QUALITY_LEGENDARY: Color = Color::Rgba(0.92, 0.76, 0.0, 1.0); const QUALITY_ARTIFACT: Color = Color::Rgba(0.74, 0.24, 0.11, 1.0); const QUALITY_DEBUG: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0); const ERROR_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0);
190const INFO_COLOR: Color = Color::Rgba(0.28, 0.83, 0.71, 1.0);
192const ONLINE_COLOR: Color = Color::Rgba(0.3, 1.0, 0.3, 1.0);
194const OFFLINE_COLOR: Color = Color::Rgba(1.0, 0.3, 0.3, 1.0);
196const TELL_COLOR: Color = Color::Rgba(0.98, 0.71, 1.0, 1.0);
198const SAY_COLOR: Color = Color::Rgba(1.0, 0.8, 0.8, 1.0);
200const GROUP_COLOR: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0);
202const FACTION_COLOR: Color = Color::Rgba(0.24, 1.0, 0.48, 1.0);
204const REGION_COLOR: Color = Color::Rgba(0.8, 1.0, 0.8, 1.0);
206const KILL_COLOR: Color = Color::Rgba(1.0, 0.17, 0.17, 1.0);
208const WORLD_COLOR: Color = Color::Rgba(0.95, 1.0, 0.95, 1.0);
210
211const 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
215const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); const UI_SUBTLE: Color = Color::Rgba(0.2, 0.24, 0.24, 1.0); const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0);
220const MENU_BG: Color = Color::Rgba(0.1, 0.12, 0.12, 1.0);
222const NAMETAG_GROUP_RANGE: f32 = 1000.0;
226const NAMETAG_RANGE: f32 = 40.0;
228const NAMETAG_DMG_TIME: f32 = 60.0;
231const NAMETAG_DMG_RANGE: f32 = 120.0;
233const SPEECH_BUBBLE_RANGE: f32 = NAMETAG_RANGE;
235const EXP_FLOATER_LIFETIME: f32 = 2.0;
236const EXP_ACCUMULATION_DURATION: f32 = 0.5;
237
238pub 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_inner,
245 crosshair_outer,
246
247 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_text,
272
273 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 version,
301
302 help,
304 help_info,
305 debug_info,
306 lantern_info,
307
308 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 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_txt,
348 free_look_bg,
349
350 auto_walk_txt,
352 auto_walk_bg,
353
354 walking_speed_txt,
356 walking_speed_bg,
357
358 zoom_lock_txt,
360 zoom_lock_bg,
361
362 camera_clamp_txt,
364 camera_clamp_bg,
365
366 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#[derive(Clone, Copy)]
384pub enum PositionSpecifier {
385 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 MidTopWithMarginOn(widget::Id, f64),
393 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
401pub 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 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 PositionSpecifier::MidTopWithMarginOn(other, margin) => {
473 self.mid_top_with_margin_on(other, margin)
474 },
475 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#[derive(PartialEq, Eq)]
772pub enum Windows {
773 Settings, 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#[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 (PressBehavior::Toggle, true) => {
853 *setting ^= true;
854 f(*setting);
855 },
856 (PressBehavior::Toggle, false) => {},
858 (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
873pub 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 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 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 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 #[cfg(feature = "singleplayer")]
1118 global_state.unpause();
1119 } else {
1120 self.esc_menu = true;
1121 self.want_grab = false;
1122
1123 #[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: (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 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 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 let ids = Ids::new(ui.id_generator());
1326 let mut layers = Vec::new();
1328 for layer in client.world_data().map_layers() {
1329 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 let imgs = Imgs::load(&mut ui).expect("Failed to load images!");
1339 let rot_imgs = ImgsRot::load(&mut ui).expect("Failed to load rot images!");
1341 let item_imgs = ItemImgs::new(&mut ui, imgs.not_found);
1343 let item_i18n = ItemI18n::new_expect();
1345 let fonts = Fonts::load(global_state.i18n.read().fonts(), &mut ui)
1347 .expect("Impossible to load fonts!");
1348 let server = &client.server_info().name;
1350 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 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 );
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 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 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 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)] 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.pulse += dt.as_secs_f32();
1499 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 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 let prompt_dialog = PromptDialogSettings::new(
1564 format!("{}\n", common_message),
1565 Event::AcknowledgePersistenceLoadError,
1566 None,
1567 )
1568 .with_no_negative_option();
1569 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 if let Some(health) = healths.get(me) {
1582 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; 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 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 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 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 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 const FLASH_MAX: u32 = 2;
1645
1646 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 if global_state.settings.interface.sct {
1656 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; 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 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 let hp_fade = calc_fade(floater);
1709
1710 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 let y = if floater.info.amount < 0.0 {
1729 floater.timer as f64
1730 * number_speed
1731 * floater.info.amount.signum() as f64
1732 + 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 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 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; 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 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; 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 "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 }
1841 }
1842
1843 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 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 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 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 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 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 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; 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 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 self.new_messages
1993 .retain(|msg| !chat::is_muted(client, &global_state.profile, msg));
1994
1995 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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; 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 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 let fade = calc_fade(floater);
2575 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 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 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 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 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 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 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 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 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 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 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 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 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 let time_in_seconds = client.state().get_time_of_day();
2812 let current_time = NaiveTime::from_num_seconds_from_midnight_opt(
2813 (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 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 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 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 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 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 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 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 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 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 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 if !gpu_timings.is_empty() {
2946 let num_timings = gpu_timings.len();
2947 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 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 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 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 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 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 let ecs = client.state().ecs();
3033 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 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::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 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 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 self.show.prompt_dialog = None;
3138 },
3139 None => {},
3140 }
3141 }
3142
3143 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 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 self.pulse,
3201 &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 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 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 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 if self.show.crafting {
3377 if let Some(inventory) = inventories.get(entity) {
3378 for event in Crafting::new(
3379 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 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 if global_state.settings.interface.toggle_chat || self.force_chat {
3530 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 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 #[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 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 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 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 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 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 #[cfg(feature = "singleplayer")]
3853 global_state.unpause();
3854 },
3855 Some(esc_menu::Event::Logout) => {
3856 #[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 #[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 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 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 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 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 '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 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 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 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 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 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 self.show.crafting_fields.recipe_inputs.remove(&c.index);
4136 }
4137 },
4138 slot::Event::SplitDropped(from) => {
4139 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 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 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 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 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 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 let arrow_ani = (self.pulse * 4.0).cos() * 0.5 + 0.8; let show_intro = self.show.intro; 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 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 pub fn add_backlog_messages(&mut self, messages: &mut chat::MessageBacklog) {
4633 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 fn typing(&self) -> bool { Hud::is_captured::<widget::TextEdit>(&self.ui.ui) }
4659
4660 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 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 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 let min_zoom = 1.0;
4750 let max_zoom = world_size
4751 .reduce_partial_max() as f64;
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 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 if self.show.bag {
4838 self.slot_manager.idle();
4839 }
4840 self.show.toggle_windows(global_state);
4841 }
4842 true
4843 },
4844
4845 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 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 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 self.show.want_grab = false;
4967 true
4968 },
4969 _ => false,
4970 };
4971 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 if self.ui.ui.global_input().events().any(|event| {
4995 use conrod_core::{event, input};
4996 matches!(
4997 event,
4998 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 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 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 self.item_imgs.reload_if_changed(&mut self.ui);
5068 let _pool = client.state().ecs().read_resource::<SlowJobPool>();
5072 self.ui.maintain(
5073 global_state.window.renderer_mut(),
5074 None,
5075 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 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 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 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 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 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 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 && (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 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 && (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}
5284pub 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 BuffKind::Regeneration => imgs.buff_plus_0,
5340 BuffKind::Saturation => imgs.buff_saturation_0,
5341 BuffKind::Potion => imgs.buff_potion_0,
5342 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 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 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
5474pub fn multiplier_to_percentage(value: f32) -> f32 { value * 100.0 - 100.0 }