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