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