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