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