mod animation; mod bag; mod buffs; mod buttons; mod change_notification; mod chat; mod crafting; mod diary; mod esc_menu; mod group; mod hotbar; pub mod img_ids; pub mod item_imgs; mod loot_scroller; mod map; mod minimap; mod overhead; mod overitem; mod popup; mod prompt_dialog; mod quest; mod settings_window; mod skillbar; mod slots; mod social; mod trade; pub mod util; pub use crafting::CraftingTab; pub use hotbar::{SlotContents as HotbarSlotContents, State as HotbarState}; pub use item_imgs::animate_by_pulse; pub use loot_scroller::LootMessage; pub use settings_window::ScaleChange; use bag::Bag; use buffs::BuffsBar; use buttons::Buttons; use change_notification::{ChangeNotification, NotificationReason}; use chat::Chat; use chrono::NaiveTime; use crafting::Crafting; use diary::{Diary, SelectedSkillTree}; use esc_menu::EscMenu; use group::Group; use img_ids::Imgs; use item_imgs::ItemImgs; use loot_scroller::LootScroller; use map::Map; use minimap::{MiniMap, VoxelMinimap}; use popup::Popup; use prompt_dialog::PromptDialog; use quest::Quest; use serde::{Deserialize, Serialize}; use settings_window::{SettingsTab, SettingsWindow}; use skillbar::Skillbar; use social::Social; use trade::Trade; use crate::{ cmd::get_player_uuid, ecs::{ comp as vcomp, comp::{HpFloater, HpFloaterList}, }, game_input::GameInput, hud::{img_ids::ImgsRot, prompt_dialog::DialogOutcomeEvent}, render::UiDrawer, scene::camera::{self, Camera}, session::{ interactable::{BlockInteraction, Interactable}, settings_change::{ Audio, Chat as ChatChange, Interface as InterfaceChange, SettingsChange, }, }, settings::chat::ChatFilter, ui::{ self, fonts::Fonts, img_ids::Rotations, slot, slot::SlotKey, Graphic, Ingameable, ScaleMode, Ui, }, window::Event as WinEvent, GlobalState, }; use client::Client; use common::{ combat, comp::{ self, ability::AuxiliaryAbility, fluid_dynamics, inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason}, item::{ tool::{AbilityContext, ToolKind}, ItemDesc, MaterialStatManifest, Quality, }, loot_owner::LootOwnerKind, pet::is_mountable, skillset::{skills::Skill, SkillGroupKind, SkillsPersistenceError}, BuffData, BuffKind, Health, Item, MapMarkerChange, }, consts::MAX_PICKUP_RANGE, link::Is, mounting::Mount, outcome::Outcome, resources::{Secs, Time}, slowjob::SlowJobPool, terrain::{SpriteKind, TerrainChunk, UnlockKind}, trade::{ReducedInventory, TradeAction}, uid::Uid, util::{srgba_to_linear, Dir}, vol::RectRasterableVol, }; use common_base::{prof_span, span}; use common_net::{ msg::{world_msg::SiteId, Notification, PresenceKind}, sync::WorldSyncExt, }; use conrod_core::{ text::cursor::Index, widget::{self, Button, Image, Rectangle, Text}, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, }; use hashbrown::{HashMap, HashSet}; use i18n::Localization; use rand::Rng; use specs::{Entity as EcsEntity, Join, WorldExt}; use std::{ borrow::Cow, cmp::Ordering, collections::VecDeque, sync::Arc, time::{Duration, Instant}, }; use tracing::warn; use vek::*; const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); const TEXT_VELORITE: Color = Color::Rgba(0.0, 0.66, 0.66, 1.0); const TEXT_BLUE_COLOR: Color = Color::Rgba(0.8, 0.9, 1.0, 1.0); const TEXT_GRAY_COLOR: Color = Color::Rgba(0.5, 0.5, 0.5, 1.0); const TEXT_DULL_RED_COLOR: Color = Color::Rgba(0.56, 0.2, 0.2, 1.0); const TEXT_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5); //const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1); const TEXT_BIND_CONFLICT_COLOR: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0); const BLACK: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); //const BG_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 0.8); const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0); const LOW_HP_COLOR: Color = Color::Rgba(0.93, 0.59, 0.03, 1.0); const CRITICAL_HP_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0); const STAMINA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9); const ENEMY_HP_COLOR: Color = Color::Rgba(0.93, 0.1, 0.29, 1.0); const XP_COLOR: Color = Color::Rgba(0.59, 0.41, 0.67, 1.0); const POISE_COLOR: Color = Color::Rgba(0.70, 0.0, 0.60, 1.0); const POISEBAR_TICK_COLOR: Color = Color::Rgba(0.70, 0.90, 0.0, 1.0); //const TRANSPARENT: Color = Color::Rgba(0.0, 0.0, 0.0, 0.0); //const FOCUS_COLOR: Color = Color::Rgba(1.0, 0.56, 0.04, 1.0); //const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0); const BUFF_COLOR: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0); const DEBUFF_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0); // Item Quality Colors const QUALITY_LOW: Color = Color::Rgba(0.41, 0.41, 0.41, 1.0); // Grey - Trash, can be sold to vendors const QUALITY_COMMON: Color = Color::Rgba(0.79, 1.00, 1.00, 1.0); // Light blue - Crafting mats, food, starting equipment, quest items (like keys), rewards for easy quests const QUALITY_MODERATE: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0); // Green - Quest Rewards, commonly looted items from NPCs const QUALITY_HIGH: Color = Color::Rgba(0.18, 0.32, 0.9, 1.0); // Blue - Dungeon rewards, boss loot, rewards for hard quests const QUALITY_EPIC: Color = Color::Rgba(0.58, 0.29, 0.93, 1.0); // Purple - Rewards for epic quests and very hard bosses const QUALITY_LEGENDARY: Color = Color::Rgba(0.92, 0.76, 0.0, 1.0); // Gold - Legendary items that require a big effort to acquire const QUALITY_ARTIFACT: Color = Color::Rgba(0.74, 0.24, 0.11, 1.0); // Orange - Not obtainable by normal means, "artifacts" const QUALITY_DEBUG: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0); // Red - Admin and debug items // Chat Colors /// Color for chat command errors (yellow !) const ERROR_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0); /// Color for chat command info (blue i) const INFO_COLOR: Color = Color::Rgba(0.28, 0.83, 0.71, 1.0); /// Online color const ONLINE_COLOR: Color = Color::Rgba(0.3, 1.0, 0.3, 1.0); /// Offline color const OFFLINE_COLOR: Color = Color::Rgba(1.0, 0.3, 0.3, 1.0); /// Color for a private message from another player const TELL_COLOR: Color = Color::Rgba(0.98, 0.71, 1.0, 1.0); /// Color for local chat const SAY_COLOR: Color = Color::Rgba(1.0, 0.8, 0.8, 1.0); /// Color for group chat const GROUP_COLOR: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0); /// Color for factional chat const FACTION_COLOR: Color = Color::Rgba(0.24, 1.0, 0.48, 1.0); /// Color for regional chat const REGION_COLOR: Color = Color::Rgba(0.8, 1.0, 0.8, 1.0); /// Color for death messagesw const KILL_COLOR: Color = Color::Rgba(1.0, 0.17, 0.17, 1.0); /// Color for global messages const WORLD_COLOR: Color = Color::Rgba(0.95, 1.0, 0.95, 1.0); //Nametags const GROUP_MEMBER: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0); const DEFAULT_NPC: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); // UI Color-Theme const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue const UI_SUBTLE: Color = Color::Rgba(0.2, 0.24, 0.24, 1.0); // Dark Greenish Blue //const UI_MAIN: Color = Color::Rgba(0.1, 0.1, 0.1, 0.97); // Dark const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0); // Pull-Down menu BG color const MENU_BG: Color = Color::Rgba(0.1, 0.12, 0.12, 1.0); //const UI_DARK_0: Color = Color::Rgba(0.25, 0.37, 0.37, 1.0); /// Distance at which nametags are visible for group members const NAMETAG_GROUP_RANGE: f32 = 1000.0; /// Distance at which nametags are visible for merchants const NAMETAG_MERCHANT_RANGE: f32 = 50.0; /// Distance at which nametags are visible const NAMETAG_RANGE: f32 = 40.0; /// Time nametags stay visible after doing damage even if they are out of range /// in seconds const NAMETAG_DMG_TIME: f32 = 60.0; /// Range damaged triggered nametags can be seen const NAMETAG_DMG_RANGE: f32 = 120.0; /// Range to display speech-bubbles at const SPEECH_BUBBLE_RANGE: f32 = NAMETAG_RANGE; const EXP_FLOATER_LIFETIME: f32 = 2.0; const EXP_ACCUMULATION_DURATION: f32 = 0.5; widget_ids! { struct Ids { // Crosshair crosshair_inner, crosshair_outer, // SCT player_scts[], player_sct_bgs[], player_rank_up, player_rank_up_txt_number, player_rank_up_txt_0, player_rank_up_txt_0_bg, player_rank_up_txt_1, player_rank_up_txt_1_bg, player_rank_up_icon, sct_exp_bgs[], sct_exps[], sct_exp_icons[], sct_lvl_bg, sct_lvl, hurt_bg, death_bg, sct_bgs[], scts[], overheads[], overitems[], // Alpha Disclaimer alpha_text, // Debug debug_bg, fps_counter, ping, coordinates, velocity, glide_ratio, glide_aoe, orientation, look_direction, loaded_distance, time, entity_count, num_chunks, num_lights, num_figures, num_particles, current_biome, current_site, graphics_backend, gpu_timings[], weather, song_info, // Game Version version, // Help help, help_info, debug_info, lantern_info, // Window Frames window_frame_0, window_frame_1, window_frame_2, window_frame_3, window_frame_4, window_frame_5, button_help2, button_help3, // External chat, loot_scroller, map, world_map, character_window, popup, minimap, prompt_dialog, bag, trade, social, quest, diary, skillbar, buttons, buffs, esc_menu, small_window, social_window, quest_window, crafting_window, settings_window, group_window, item_info, // Free look indicator free_look_txt, free_look_bg, // Auto walk indicator auto_walk_txt, auto_walk_bg, // Temporal (fading) camera zoom lock indicator zoom_lock_txt, zoom_lock_bg, // Camera clamp indicator camera_clamp_txt, camera_clamp_bg, // Tutorial quest_bg, q_headline_bg, q_headline, q_text_bg, q_text, accept_button, intro_button, tut_arrow, tut_arrow_txt_bg, tut_arrow_txt, } } /// Specifier to use with `Position::position` /// Read its documentation for more // TODO: extend as you need it #[derive(Clone, Copy)] pub enum PositionSpecifier { // Place the widget near other widget with the given margins TopLeftWithMarginsOn(widget::Id, f64, f64), TopRightWithMarginsOn(widget::Id, f64, f64), MidBottomWithMarginOn(widget::Id, f64), BottomLeftWithMarginsOn(widget::Id, f64, f64), BottomRightWithMarginsOn(widget::Id, f64, f64), // Place the widget near other widget with given margin MidTopWithMarginOn(widget::Id, f64), // Place the widget near other widget at given distance MiddleOf(widget::Id), UpFrom(widget::Id, f64), DownFrom(widget::Id, f64), LeftFrom(widget::Id, f64), RightFrom(widget::Id, f64), } /// Trait which enables you to declare widget position /// to use later on widget creation. /// It is implemented for all widgets which are implement Positionable, /// so you can easily change your code to use this method. /// /// Consider this example: /// ```text /// let slot1 = slot_maker /// .fabricate(hotbar::Slot::One, [40.0; 2]) /// .filled_slot(self.imgs.skillbar_slot) /// .bottom_left_with_margins_on(state.ids.frame, 0.0, 0.0); /// if condition { /// call_slot1(slot1); /// } else { /// call_slot2(slot1); /// } /// let slot2 = slot_maker /// .fabricate(hotbar::Slot::Two, [40.0; 2]) /// .filled_slot(self.imgs.skillbar_slot) /// .right_from(state.ids.slot1, slot_offset); /// if condition { /// call_slot1(slot2); /// } else { /// call_slot2(slot2); /// } /// ``` /// Despite being identical, you can't easily deduplicate code /// which uses slot1 and slot2 as they are calling methods to position itself. /// This can be solved if you declare position and use it later like so /// ```text /// let slots = [ /// (hotbar::Slot::One, BottomLeftWithMarginsOn(state.ids.frame, 0.0, 0.0)), /// (hotbar::Slot::Two, RightFrom(state.ids.slot1, slot_offset)), /// ]; /// for (slot, pos) in slots { /// let slot = slot_maker /// .fabricate(slot, [40.0; 2]) /// .filled_slot(self.imgs.skillbar_slot) /// .position(pos); /// if condition { /// call_slot1(slot); /// } else { /// call_slot2(slot); /// } /// } /// ``` pub trait Position { #[must_use] fn position(self, request: PositionSpecifier) -> Self; } impl Position for W { fn position(self, request: PositionSpecifier) -> Self { match request { // Place the widget near other widget with the given margins PositionSpecifier::TopLeftWithMarginsOn(other, top, left) => { self.top_left_with_margins_on(other, top, left) }, PositionSpecifier::TopRightWithMarginsOn(other, top, right) => { self.top_right_with_margins_on(other, top, right) }, PositionSpecifier::MidBottomWithMarginOn(other, margin) => { self.mid_bottom_with_margin_on(other, margin) }, PositionSpecifier::BottomRightWithMarginsOn(other, bottom, right) => { self.bottom_right_with_margins_on(other, bottom, right) }, PositionSpecifier::BottomLeftWithMarginsOn(other, bottom, left) => { self.bottom_left_with_margins_on(other, bottom, left) }, // Place the widget near other widget with given margin PositionSpecifier::MidTopWithMarginOn(other, margin) => { self.mid_top_with_margin_on(other, margin) }, // Place the widget near other widget at given distance PositionSpecifier::MiddleOf(other) => self.middle_of(other), PositionSpecifier::UpFrom(other, offset) => self.up_from(other, offset), PositionSpecifier::DownFrom(other, offset) => self.down_from(other, offset), PositionSpecifier::LeftFrom(other, offset) => self.left_from(other, offset), PositionSpecifier::RightFrom(other, offset) => self.right_from(other, offset), } } } #[derive(Clone, Copy, Debug)] pub enum BuffIconKind<'a> { Buff { kind: BuffKind, data: BuffData, multiplicity: usize, }, Ability { ability_id: &'a str, }, } impl<'a> BuffIconKind<'a> { pub fn image(&self, imgs: &Imgs) -> conrod_core::image::Id { match self { Self::Buff { kind, .. } => get_buff_image(*kind, imgs), Self::Ability { ability_id, .. } => util::ability_image(imgs, ability_id), } } pub fn max_duration(&self) -> Option { match self { Self::Buff { data, .. } => data.duration, Self::Ability { .. } => None, } } pub fn title_description<'b>( &self, localized_strings: &'b Localization, ) -> (Cow<'b, str>, Cow<'b, str>) { match self { Self::Buff { kind, data, multiplicity: _, } => ( get_buff_title(*kind, localized_strings), get_buff_desc(*kind, *data, localized_strings), ), Self::Ability { ability_id } => { util::ability_description(ability_id, localized_strings) }, } } } impl<'a> PartialOrd for BuffIconKind<'a> { fn partial_cmp(&self, other: &Self) -> Option { match (self, other) { ( BuffIconKind::Buff { kind, .. }, BuffIconKind::Buff { kind: other_kind, .. }, ) => Some(kind.cmp(other_kind)), (BuffIconKind::Buff { .. }, BuffIconKind::Ability { .. }) => Some(Ordering::Greater), (BuffIconKind::Ability { .. }, BuffIconKind::Buff { .. }) => Some(Ordering::Less), ( BuffIconKind::Ability { ability_id }, BuffIconKind::Ability { ability_id: other_id, }, ) => Some(ability_id.cmp(other_id)), } } } impl<'a> Ord for BuffIconKind<'a> { fn cmp(&self, other: &Self) -> Ordering { // We know this is safe since we can look at the partialord implementation and // see that every variant is wrapped in Some self.partial_cmp(other).unwrap() } } impl<'a> PartialEq for BuffIconKind<'a> { fn eq(&self, other: &Self) -> bool { match (self, other) { ( BuffIconKind::Buff { kind, .. }, BuffIconKind::Buff { kind: other_kind, .. }, ) => kind == other_kind, ( BuffIconKind::Ability { ability_id }, BuffIconKind::Ability { ability_id: other_id, }, ) => ability_id == other_id, _ => false, } } } impl<'a> Eq for BuffIconKind<'a> {} #[derive(Clone, Copy, Debug)] pub struct BuffIcon<'a> { kind: BuffIconKind<'a>, is_buff: bool, end_time: Option, } impl<'a> BuffIcon<'a> { pub fn multiplicity(&self) -> usize { match self.kind { BuffIconKind::Buff { multiplicity, .. } => multiplicity, BuffIconKind::Ability { .. } => 1, } } pub fn get_buff_time(&self, time: Time) -> String { if let Some(end) = self.end_time { format!("{:.0}s", end - time.0) } else { "".to_string() } } pub fn icons_vec(buffs: &comp::Buffs, stance: Option<&comp::Stance>) -> Vec { buffs .iter_active() .filter_map(BuffIcon::from_buffs) .chain(stance.and_then(BuffIcon::from_stance).into_iter()) .collect::>() } fn from_stance(stance: &comp::Stance) -> Option { use comp::ability::{Stance, SwordStance}; let id = match stance { Stance::Sword(SwordStance::Offensive) => "common.abilities.sword.offensive_combo", Stance::Sword(SwordStance::Crippling) => "common.abilities.sword.crippling_combo", Stance::Sword(SwordStance::Cleaving) => "common.abilities.sword.cleaving_combo", Stance::Sword(SwordStance::Defensive) => "common.abilities.sword.defensive_combo", Stance::Sword(SwordStance::Parrying) => "common.abilities.sword.parrying_combo", Stance::Sword(SwordStance::Heavy) => "common.abilities.sword.heavy_combo", Stance::Sword(SwordStance::Mobility) => "common.abilities.sword.mobility_combo", Stance::Sword(SwordStance::Reaching) => "common.abilities.sword.reaching_combo", Stance::None => { return None; }, }; Some(BuffIcon { kind: BuffIconKind::Ability { ability_id: id }, is_buff: true, end_time: None, }) } fn from_buffs<'b, I: Iterator>(buffs: I) -> Option { let (buff, count) = buffs.fold((None, 0), |(strongest, count), buff| { (strongest.or(Some(buff)), count + 1) }); let buff = buff?; Some(Self { kind: BuffIconKind::Buff { kind: buff.kind, data: buff.data, multiplicity: count, }, is_buff: buff.kind.is_buff(), end_time: buff.end_time.map(|end| end.0), }) } } pub struct ExpFloater { pub owner: Uid, pub exp_change: u32, pub timer: f32, pub jump_timer: f32, pub rand_offset: (f32, f32), pub xp_pools: HashSet, } pub struct SkillPointGain { pub skill_tree: SkillGroupKind, pub total_points: u16, pub timer: f32, } #[derive(Debug, Clone, Copy)] pub struct ComboFloater { pub combo: u32, pub timer: f64, } pub struct BlockFloater { pub timer: f32, } pub struct DebugInfo { pub tps: f64, pub frame_time: Duration, pub ping_ms: f64, pub coordinates: Option, pub velocity: Option, pub ori: Option, pub character_state: Option, pub look_dir: Dir, pub in_fluid: Option, pub num_chunks: u32, pub num_lights: u32, pub num_visible_chunks: u32, pub num_shadow_chunks: u32, pub num_figures: u32, pub num_figures_visible: u32, pub num_particles: u32, pub num_particles_visible: u32, pub current_track: String, pub current_artist: String, } pub struct HudInfo { pub is_aiming: bool, pub is_mining: bool, pub is_first_person: bool, pub viewpoint_entity: specs::Entity, pub mutable_viewpoint: bool, pub target_entity: Option, pub selected_entity: Option<(specs::Entity, Instant)>, pub persistence_load_error: Option, } #[derive(Clone)] pub enum Event { SendMessage(String), SendCommand(String, Vec), CharacterSelection, UseSlot { slot: comp::slot::Slot, bypass_dialog: bool, }, SwapEquippedWeapons, SwapSlots { slot_a: comp::slot::Slot, slot_b: comp::slot::Slot, bypass_dialog: bool, }, SplitSwapSlots { slot_a: comp::slot::Slot, slot_b: comp::slot::Slot, bypass_dialog: bool, }, DropSlot(comp::slot::Slot), SplitDropSlot(comp::slot::Slot), SortInventory, ChangeHotbarState(Box), TradeAction(TradeAction), Ability(usize, bool), Logout, Quit, CraftRecipe { recipe_name: String, craft_sprite: Option<(Vec3, SpriteKind)>, amount: u32, }, SalvageItem { slot: InvSlotId, salvage_pos: Vec3, }, CraftModularWeapon { primary_slot: InvSlotId, secondary_slot: InvSlotId, craft_sprite: Option>, }, CraftModularWeaponComponent { toolkind: ToolKind, material: InvSlotId, modifier: Option, craft_sprite: Option>, }, InviteMember(Uid), AcceptInvite, DeclineInvite, KickMember(Uid), LeaveGroup, AssignLeader(Uid), RemoveBuff(BuffKind), UnlockSkill(Skill), SelectExpBar(Option), RequestSiteInfo(SiteId), ChangeAbility(usize, AuxiliaryAbility), SettingsChange(SettingsChange), AcknowledgePersistenceLoadError, MapMarkerEvent(MapMarkerChange), } // TODO: Are these the possible layouts we want? // TODO: Maybe replace this with bitflags. // `map` is not here because it currently is displayed over the top of other // open windows. #[derive(PartialEq, Eq)] pub enum Windows { Settings, // Display settings window. None, } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum CrosshairType { RoundEdges, Edges, #[serde(other)] Round, } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum Intro { Never, #[serde(other)] Show, } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum XpBar { OnGain, #[serde(other)] Always, } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum BarNumbers { Percent, Off, #[serde(other)] Values, } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum ShortcutNumbers { Off, #[serde(other)] On, } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum BuffPosition { Map, #[serde(other)] Bar, } #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum PressBehavior { Hold = 1, #[serde(other)] Toggle = 0, } /// Similar to [PressBehavior], with different semantics for settings that /// change state automatically. There is no [PressBehavior::update][update] /// implementation because it doesn't apply to the use case; this is just a /// sentinel. #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum AutoPressBehavior { Auto = 1, #[serde(other)] Toggle = 0, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ChatTab { pub label: String, pub filter: ChatFilter, } impl Default for ChatTab { fn default() -> Self { Self { label: String::from("Chat"), filter: ChatFilter::default(), } } } impl PressBehavior { pub fn update(&self, keystate: bool, setting: &mut bool, f: impl FnOnce(bool)) { match (self, keystate) { // flip the state on key press in toggle mode (PressBehavior::Toggle, true) => { *setting ^= true; f(*setting); }, // do nothing on key release in toggle mode (PressBehavior::Toggle, false) => {}, // set the setting to the key state in hold mode (PressBehavior::Hold, state) => { *setting = state; f(*setting); }, } } } #[derive(Default, Clone)] pub struct MapMarkers { owned: Option>, group: HashMap>, } /// (target slot, input value, inventory quantity, is our inventory, error, /// trade.offers index of trade slot) pub struct TradeAmountInput { slot: InvSlotId, input: String, inv: u32, ours: bool, err: Option, who: usize, input_painted: bool, submit_action: Option, } impl TradeAmountInput { pub fn new(slot: InvSlotId, input: String, inv: u32, ours: bool, who: usize) -> Self { Self { slot, input, inv, ours, who, err: None, input_painted: false, submit_action: None, } } } pub struct Show { ui: bool, intro: bool, help: bool, crafting: bool, bag: bool, bag_inv: bool, trade: bool, social: bool, diary: bool, group: bool, quest: bool, group_menu: bool, esc_menu: bool, open_windows: Windows, map: bool, ingame: bool, chat_tab_settings_index: Option, settings_tab: SettingsTab, diary_fields: diary::DiaryShow, crafting_fields: crafting::CraftingShow, social_search_key: Option, want_grab: bool, stats: bool, free_look: bool, auto_walk: bool, zoom_lock: ChangeNotification, camera_clamp: bool, prompt_dialog: Option, location_markers: MapMarkers, trade_amount_input_key: Option, } impl Show { fn bag(&mut self, open: bool) { if !self.esc_menu { self.bag = open; self.map = false; self.crafting_fields.salvage = false; if !open { self.crafting = false; } self.want_grab = !self.any_window_requires_cursor(); } } fn trade(&mut self, open: bool) { if !self.esc_menu { self.bag = open; self.trade = open; self.map = false; self.want_grab = !self.any_window_requires_cursor(); } } fn map(&mut self, open: bool) { if !self.esc_menu { self.map = open; self.bag = false; self.crafting = false; self.crafting_fields.salvage = false; self.social = false; self.quest = false; self.diary = false; self.want_grab = !self.any_window_requires_cursor(); } } fn social(&mut self, open: bool) { if !self.esc_menu { if !self.social && open { // rising edge detector self.search_social_players(None); } self.social = open; self.diary = false; self.want_grab = !self.any_window_requires_cursor(); } } fn quest(&mut self, open: bool) { if !self.esc_menu { self.quest = open; self.diary = false; self.map = false; self.want_grab = !self.any_window_requires_cursor(); } } fn crafting(&mut self, open: bool) { if !self.esc_menu { if !self.crafting && open { // rising edge detector self.search_crafting_recipe(None); } self.crafting = open; self.crafting_fields.salvage = false; self.crafting_fields.recipe_inputs = HashMap::new(); self.bag = open; self.map = false; self.want_grab = !self.any_window_requires_cursor(); } } pub fn open_crafting_tab( &mut self, tab: CraftingTab, craft_sprite: Option<(Vec3, SpriteKind)>, ) { self.selected_crafting_tab(tab); self.crafting(true); self.crafting_fields.craft_sprite = self.crafting_fields.craft_sprite.or(craft_sprite); self.crafting_fields.salvage = matches!( self.crafting_fields.craft_sprite, Some((_, SpriteKind::DismantlingBench)) ) && matches!(tab, CraftingTab::Dismantle); } fn diary(&mut self, open: bool) { if !self.esc_menu { self.social = false; self.quest = false; self.crafting = false; self.crafting_fields.salvage = false; self.bag = false; self.map = false; self.diary_fields = diary::DiaryShow::default(); self.diary = open; self.want_grab = !self.any_window_requires_cursor(); } } fn settings(&mut self, open: bool) { if !self.esc_menu { self.open_windows = if open { Windows::Settings } else { Windows::None }; self.bag = false; self.social = false; self.quest = false; self.crafting = false; self.crafting_fields.salvage = false; self.diary = false; self.want_grab = !self.any_window_requires_cursor(); } } fn toggle_trade(&mut self) { self.trade(!self.trade); } fn toggle_map(&mut self) { self.map(!self.map) } fn toggle_social(&mut self) { self.social(!self.social); } fn toggle_crafting(&mut self) { self.crafting(!self.crafting) } fn toggle_spell(&mut self) { self.diary(!self.diary) } fn toggle_ui(&mut self) { self.ui = !self.ui; } fn toggle_settings(&mut self, global_state: &GlobalState) { match self.open_windows { Windows::Settings => { #[cfg(feature = "singleplayer")] global_state.unpause(); self.settings(false); }, _ => { #[cfg(feature = "singleplayer")] global_state.pause(); self.settings(true) }, }; #[cfg(not(feature = "singleplayer"))] let _global_state = global_state; } // TODO: Add self updating key-bindings element //fn toggle_help(&mut self) { self.help = !self.help } fn any_window_requires_cursor(&self) -> bool { self.bag || self.trade || self.esc_menu || self.map || self.social || self.crafting || self.diary || self.help || self.intro || self.quest || !matches!(self.open_windows, Windows::None) } fn toggle_windows(&mut self, global_state: &mut GlobalState) { if self.any_window_requires_cursor() { self.bag = false; self.trade = false; self.esc_menu = false; self.help = false; self.intro = false; self.map = false; self.social = false; self.quest = false; self.diary = false; self.crafting = false; self.open_windows = Windows::None; self.want_grab = true; // Unpause the game if we are on singleplayer #[cfg(feature = "singleplayer")] global_state.unpause(); } else { self.esc_menu = true; self.want_grab = false; // Pause the game if we are on singleplayer #[cfg(feature = "singleplayer")] global_state.pause(); } #[cfg(not(feature = "singleplayer"))] let _global_state = global_state; } fn open_setting_tab(&mut self, tab: SettingsTab) { self.open_windows = Windows::Settings; self.esc_menu = false; self.settings_tab = tab; self.bag = false; self.want_grab = false; } fn open_skill_tree(&mut self, tree_sel: SelectedSkillTree) { self.diary_fields.skilltreetab = tree_sel; self.social = false; } fn selected_crafting_tab(&mut self, sel_cat: CraftingTab) { self.crafting_fields.crafting_tab = sel_cat; } fn search_crafting_recipe(&mut self, search_key: Option) { self.crafting_fields.crafting_search_key = search_key; } fn search_social_players(&mut self, search_key: Option) { self.social_search_key = search_key; } /// If all of the menus are closed, adjusts coordinates of cursor to center /// of screen fn toggle_cursor_on_menu_close(&self, global_state: &mut GlobalState, ui: &mut Ui) { if !self.bag && !self.trade && !self.esc_menu && !self.map && !self.social && !self.quest && !self.crafting && !self.diary && !self.help && !self.intro && global_state.window.is_cursor_grabbed() { ui.handle_event(ui::Event( conrod_core::input::Motion::MouseCursor { x: 0.0, y: 0.0 }.into(), )); global_state.window.center_cursor(); } } pub fn update_map_markers(&mut self, event: comp::MapMarkerUpdate) { match event { comp::MapMarkerUpdate::Owned(event) => match event { MapMarkerChange::Update(waypoint) => self.location_markers.owned = Some(waypoint), MapMarkerChange::Remove => self.location_markers.owned = None, }, comp::MapMarkerUpdate::GroupMember(user, event) => match event { MapMarkerChange::Update(waypoint) => { self.location_markers.group.insert(user, waypoint); }, MapMarkerChange::Remove => { self.location_markers.group.remove(&user); }, }, comp::MapMarkerUpdate::ClearGroup => { self.location_markers.group.clear(); }, } } } pub struct PromptDialogSettings { message: String, affirmative_event: Event, negative_option: bool, negative_event: Option, outcome_via_keypress: Option, } impl PromptDialogSettings { pub fn new(message: String, affirmative_event: Event, negative_event: Option) -> Self { Self { message, affirmative_event, negative_option: true, negative_event, outcome_via_keypress: None, } } pub fn set_outcome_via_keypress(&mut self, outcome: bool) { self.outcome_via_keypress = Some(outcome); } #[must_use] pub fn with_no_negative_option(mut self) -> Self { self.negative_option = false; self } } pub struct Floaters { pub exp_floaters: Vec, pub skill_point_displays: Vec, pub combo_floater: Option, pub block_floaters: Vec, } #[derive(Clone)] pub enum HudLootOwner { Name(String), Group, Unknown, } #[derive(Clone)] pub enum HudCollectFailedReason { InventoryFull, LootOwned { owner: HudLootOwner, expiry_secs: u64, }, } impl HudCollectFailedReason { pub fn from_server_reason(reason: &CollectFailedReason, ecs: &specs::World) -> Self { match reason { CollectFailedReason::InventoryFull => HudCollectFailedReason::InventoryFull, CollectFailedReason::LootOwned { owner, expiry_secs } => { let owner = match owner { LootOwnerKind::Player(owner_uid) => { let maybe_owner_name = ecs.entity_from_uid((*owner_uid).into()).and_then(|entity| { ecs.read_storage::() .get(entity) .map(|stats| stats.name.clone()) }); if let Some(name) = maybe_owner_name { HudLootOwner::Name(name) } else { HudLootOwner::Unknown } }, LootOwnerKind::Group(_) => HudLootOwner::Group, }; HudCollectFailedReason::LootOwned { owner, expiry_secs: *expiry_secs, } }, } } } #[derive(Clone)] pub struct CollectFailedData { pulse: f32, reason: HudCollectFailedReason, } impl CollectFailedData { pub fn new(pulse: f32, reason: HudCollectFailedReason) -> Self { Self { pulse, reason } } } pub struct Hud { ui: Ui, ids: Ids, world_map: (/* Id */ Vec, Vec2), imgs: Imgs, item_imgs: ItemImgs, fonts: Fonts, rot_imgs: ImgsRot, failed_block_pickups: HashMap, CollectFailedData>, failed_entity_pickups: HashMap, new_loot_messages: VecDeque, new_messages: VecDeque, new_notifications: VecDeque, speech_bubbles: HashMap, pub show: Show, //never_show: bool, //intro: bool, //intro_2: bool, to_focus: Option>, force_ungrab: bool, force_chat_input: Option, force_chat_cursor: Option, tab_complete: Option, pulse: f32, hp_pulse: f32, slot_manager: slots::SlotManager, hotbar: hotbar::State, events: Vec, crosshair_opacity: f32, floaters: Floaters, voxel_minimap: VoxelMinimap, map_drag: Vec2, } impl Hud { pub fn new(global_state: &mut GlobalState, client: &Client) -> Self { let window = &mut global_state.window; let settings = &global_state.settings; let mut ui = Ui::new(window).unwrap(); ui.set_scaling_mode(settings.interface.ui_scale); // Generate ids. let ids = Ids::new(ui.id_generator()); // NOTE: Use a border the same color as the LOD ocean color (but with a // translucent alpha since UI have transparency and LOD doesn't). let water_color = srgba_to_linear(Rgba::new(0.0, 0.18, 0.37, 1.0)); // Load world map let mut layers = Vec::new(); for layer in client.world_data().map_layers() { layers.push( ui.add_graphic_with_rotations(Graphic::Image(Arc::clone(layer), Some(water_color))), ); } let world_map = (layers, client.world_data().chunk_size().map(|e| e as u32)); // Load images. let imgs = Imgs::load(&mut ui).expect("Failed to load images!"); // Load rotation images. let rot_imgs = ImgsRot::load(&mut ui).expect("Failed to load rot images!"); // Load item images. let item_imgs = ItemImgs::new(&mut ui, imgs.not_found); // Load fonts. let fonts = Fonts::load(global_state.i18n.read().fonts(), &mut ui) .expect("Impossible to load fonts!"); // Get the server name. let server = &client.server_info().name; // Get the id, unwrap is safe because this CANNOT be None at this // point. let character_id = match client.presence().unwrap() { PresenceKind::Character(id) => Some(id), PresenceKind::Spectator => None, PresenceKind::Possessor => None, }; // Create a new HotbarState from the persisted slots. let hotbar_state = HotbarState::new(global_state.profile.get_hotbar_slots(server, character_id)); let slot_manager = slots::SlotManager::new( ui.id_generator(), Vec2::broadcast(40.0), // TODO(heyzoos) Will be useful for whoever works on rendering the number of items // "in hand". // fonts.cyri.conrod_id, // Vec2::new(1.0, 1.0), // fonts.cyri.scale(12), // TEXT_COLOR, ); Self { voxel_minimap: VoxelMinimap::new(&mut ui), ui, imgs, world_map, rot_imgs, item_imgs, fonts, ids, failed_block_pickups: HashMap::default(), failed_entity_pickups: HashMap::default(), new_loot_messages: VecDeque::new(), new_messages: VecDeque::new(), new_notifications: VecDeque::new(), speech_bubbles: HashMap::new(), //intro: false, //intro_2: false, show: Show { help: false, intro: false, bag: false, bag_inv: false, trade: false, esc_menu: false, open_windows: Windows::None, map: false, crafting: false, ui: true, social: false, diary: false, group: false, // Change this before implementation! quest: false, group_menu: false, chat_tab_settings_index: None, settings_tab: SettingsTab::Interface, diary_fields: diary::DiaryShow::default(), crafting_fields: crafting::CraftingShow::default(), social_search_key: None, want_grab: true, ingame: true, stats: false, free_look: false, auto_walk: false, zoom_lock: ChangeNotification::default(), camera_clamp: false, prompt_dialog: None, location_markers: MapMarkers::default(), trade_amount_input_key: None, }, to_focus: None, //never_show: false, force_ungrab: false, force_chat_input: None, force_chat_cursor: None, tab_complete: None, pulse: 0.0, hp_pulse: 0.0, slot_manager, hotbar: hotbar_state, events: Vec::new(), crosshair_opacity: 0.0, floaters: Floaters { exp_floaters: Vec::new(), skill_point_displays: Vec::new(), combo_floater: None, block_floaters: Vec::new(), }, map_drag: Vec2::zero(), } } pub fn set_prompt_dialog(&mut self, prompt_dialog: PromptDialogSettings) { self.show.prompt_dialog = Some(prompt_dialog); } pub fn update_fonts(&mut self, i18n: &Localization) { self.fonts = Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!"); } #[allow(clippy::single_match)] // TODO: Pending review in #587 fn update_layout( &mut self, client: &Client, global_state: &GlobalState, debug_info: &Option, dt: Duration, info: HudInfo, camera: &Camera, interactable: Option<&Interactable>, ) -> Vec { span!(_guard, "update_layout", "Hud::update_layout"); let mut events = core::mem::take(&mut self.events); if global_state.settings.interface.map_show_voxel_map { self.voxel_minimap.maintain(client, &mut self.ui); } let (ref mut ui_widgets, ref mut item_tooltip_manager, ref mut tooltip_manager) = &mut self.ui.set_widgets(); // self.ui.set_item_widgets(); pulse time for pulsating elements self.pulse += dt.as_secs_f32(); // FPS let fps = global_state.clock.stats().average_tps; let version = common::util::DISPLAY_VERSION_LONG.clone(); let i18n = &global_state.i18n.read(); let key_layout = &global_state.window.key_layout; if self.show.ingame { prof_span!("ingame elements"); let ecs = client.state().ecs(); let pos = ecs.read_storage::(); let stats = ecs.read_storage::(); let skill_sets = ecs.read_storage::(); let healths = ecs.read_storage::(); let buffs = ecs.read_storage::(); let energy = ecs.read_storage::(); let mut hp_floater_lists = ecs.write_storage::(); let uids = ecs.read_storage::(); let interpolated = ecs.read_storage::(); let scales = ecs.read_storage::(); let bodies = ecs.read_storage::(); let items = ecs.read_storage::(); let inventories = ecs.read_storage::(); let msm = ecs.read_resource::(); let entities = ecs.entities(); let me = info.viewpoint_entity; let poises = ecs.read_storage::(); let alignments = ecs.read_storage::(); let is_mount = ecs.read_storage::>(); let stances = ecs.read_storage::(); let time = ecs.read_resource::