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