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