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