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