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