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