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