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