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