mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
5109 lines
206 KiB
Rust
5109 lines
206 KiB
Rust
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<W: Positionable> 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<Secs> {
|
|
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<Ordering> {
|
|
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<f64>,
|
|
}
|
|
|
|
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<Self> {
|
|
buffs
|
|
.iter_active()
|
|
.filter_map(BuffIcon::from_buffs)
|
|
.chain(stance.and_then(BuffIcon::from_stance).into_iter())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
fn from_stance(stance: &comp::Stance) -> Option<Self> {
|
|
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<Item = &'b comp::Buff>>(buffs: I) -> Option<Self> {
|
|
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<SkillGroupKind>,
|
|
}
|
|
|
|
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<comp::Pos>,
|
|
pub velocity: Option<comp::Vel>,
|
|
pub ori: Option<comp::Ori>,
|
|
pub character_state: Option<comp::CharacterState>,
|
|
pub look_dir: Dir,
|
|
pub in_fluid: Option<comp::Fluid>,
|
|
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<specs::Entity>,
|
|
pub selected_entity: Option<(specs::Entity, Instant)>,
|
|
pub persistence_load_error: Option<SkillsPersistenceError>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum Event {
|
|
SendMessage(String),
|
|
SendCommand(String, Vec<String>),
|
|
|
|
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<HotbarState>),
|
|
TradeAction(TradeAction),
|
|
Ability(usize, bool),
|
|
Logout,
|
|
Quit,
|
|
|
|
CraftRecipe {
|
|
recipe_name: String,
|
|
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
|
|
amount: u32,
|
|
},
|
|
SalvageItem {
|
|
slot: InvSlotId,
|
|
salvage_pos: Vec3<i32>,
|
|
},
|
|
CraftModularWeapon {
|
|
primary_slot: InvSlotId,
|
|
secondary_slot: InvSlotId,
|
|
craft_sprite: Option<Vec3<i32>>,
|
|
},
|
|
CraftModularWeaponComponent {
|
|
toolkind: ToolKind,
|
|
material: InvSlotId,
|
|
modifier: Option<InvSlotId>,
|
|
craft_sprite: Option<Vec3<i32>>,
|
|
},
|
|
InviteMember(Uid),
|
|
AcceptInvite,
|
|
DeclineInvite,
|
|
KickMember(Uid),
|
|
LeaveGroup,
|
|
AssignLeader(Uid),
|
|
RemoveBuff(BuffKind),
|
|
UnlockSkill(Skill),
|
|
SelectExpBar(Option<SkillGroupKind>),
|
|
|
|
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<Vec2<i32>>,
|
|
group: HashMap<Uid, Vec2<i32>>,
|
|
}
|
|
|
|
/// (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<String>,
|
|
who: usize,
|
|
input_painted: bool,
|
|
submit_action: Option<TradeAction>,
|
|
}
|
|
|
|
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<usize>,
|
|
settings_tab: SettingsTab,
|
|
diary_fields: diary::DiaryShow,
|
|
crafting_fields: crafting::CraftingShow,
|
|
social_search_key: Option<String>,
|
|
want_grab: bool,
|
|
stats: bool,
|
|
free_look: bool,
|
|
auto_walk: bool,
|
|
zoom_lock: ChangeNotification,
|
|
camera_clamp: bool,
|
|
prompt_dialog: Option<PromptDialogSettings>,
|
|
location_markers: MapMarkers,
|
|
trade_amount_input_key: Option<TradeAmountInput>,
|
|
}
|
|
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<i32>, 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<String>) {
|
|
self.crafting_fields.crafting_search_key = search_key;
|
|
}
|
|
|
|
fn search_social_players(&mut self, search_key: Option<String>) {
|
|
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<Event>,
|
|
outcome_via_keypress: Option<bool>,
|
|
}
|
|
|
|
impl PromptDialogSettings {
|
|
pub fn new(message: String, affirmative_event: Event, negative_event: Option<Event>) -> 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<ExpFloater>,
|
|
pub skill_point_displays: Vec<SkillPointGain>,
|
|
pub combo_floater: Option<ComboFloater>,
|
|
pub block_floaters: Vec<BlockFloater>,
|
|
}
|
|
|
|
#[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::<comp::Stats>()
|
|
.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<Rotations>, Vec2<u32>),
|
|
imgs: Imgs,
|
|
item_imgs: ItemImgs,
|
|
fonts: Fonts,
|
|
rot_imgs: ImgsRot,
|
|
failed_block_pickups: HashMap<Vec3<i32>, CollectFailedData>,
|
|
failed_entity_pickups: HashMap<EcsEntity, CollectFailedData>,
|
|
new_loot_messages: VecDeque<LootMessage>,
|
|
new_messages: VecDeque<comp::ChatMsg>,
|
|
new_notifications: VecDeque<Notification>,
|
|
speech_bubbles: HashMap<Uid, comp::SpeechBubble>,
|
|
pub show: Show,
|
|
//never_show: bool,
|
|
//intro: bool,
|
|
//intro_2: bool,
|
|
to_focus: Option<Option<widget::Id>>,
|
|
force_ungrab: bool,
|
|
force_chat_input: Option<String>,
|
|
force_chat_cursor: Option<Index>,
|
|
tab_complete: Option<String>,
|
|
pulse: f32,
|
|
hp_pulse: f32,
|
|
slot_manager: slots::SlotManager,
|
|
hotbar: hotbar::State,
|
|
events: Vec<Event>,
|
|
crosshair_opacity: f32,
|
|
floaters: Floaters,
|
|
voxel_minimap: VoxelMinimap,
|
|
map_drag: Vec2<f64>,
|
|
}
|
|
|
|
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<DebugInfo>,
|
|
dt: Duration,
|
|
info: HudInfo,
|
|
camera: &Camera,
|
|
interactable: Option<&Interactable>,
|
|
) -> Vec<Event> {
|
|
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::<comp::Pos>();
|
|
let stats = ecs.read_storage::<comp::Stats>();
|
|
let skill_sets = ecs.read_storage::<comp::SkillSet>();
|
|
let healths = ecs.read_storage::<Health>();
|
|
let buffs = ecs.read_storage::<comp::Buffs>();
|
|
let energy = ecs.read_storage::<comp::Energy>();
|
|
let mut hp_floater_lists = ecs.write_storage::<HpFloaterList>();
|
|
let uids = ecs.read_storage::<Uid>();
|
|
let interpolated = ecs.read_storage::<vcomp::Interpolated>();
|
|
let scales = ecs.read_storage::<comp::Scale>();
|
|
let bodies = ecs.read_storage::<comp::Body>();
|
|
let items = ecs.read_storage::<Item>();
|
|
let inventories = ecs.read_storage::<comp::Inventory>();
|
|
let msm = ecs.read_resource::<MaterialStatManifest>();
|
|
let entities = ecs.entities();
|
|
let me = info.viewpoint_entity;
|
|
let poises = ecs.read_storage::<comp::Poise>();
|
|
let alignments = ecs.read_storage::<comp::Alignment>();
|
|
let is_mount = ecs.read_storage::<Is<Mount>>();
|
|
let stances = ecs.read_storage::<comp::Stance>();
|
|
let time = ecs.read_resource::<Time>();
|
|
|
|
// Check if there was a persistence load error of the skillset, and if so
|
|
// display a dialog prompt
|
|
if self.show.prompt_dialog.is_none() {
|
|
if let Some(persistence_error) = info.persistence_load_error {
|
|
let persistence_error = match persistence_error {
|
|
SkillsPersistenceError::HashMismatch => {
|
|
"There was a difference detected in one of your skill groups since you \
|
|
last played."
|
|
},
|
|
SkillsPersistenceError::DeserializationFailure => {
|
|
"There was a error in loading some of your skills from the database."
|
|
},
|
|
SkillsPersistenceError::SpentExpMismatch => {
|
|
"The amount of free experience you had in one of your skill groups \
|
|
differed from when you last played."
|
|
},
|
|
SkillsPersistenceError::SkillsUnlockFailed => {
|
|
"Your skills were not able to be obtained in the same order you \
|
|
acquired them. Prerequisites or costs may have changed."
|
|
},
|
|
};
|
|
|
|
let common_message = "Some of your skill points have been reset. You will \
|
|
need to reassign them.";
|
|
|
|
warn!("{}\n{}", persistence_error, common_message);
|
|
let prompt_dialog = PromptDialogSettings::new(
|
|
format!("{}\n", common_message),
|
|
Event::AcknowledgePersistenceLoadError,
|
|
None,
|
|
)
|
|
.with_no_negative_option();
|
|
// self.set_prompt_dialog(prompt_dialog);
|
|
self.show.prompt_dialog = Some(prompt_dialog);
|
|
}
|
|
}
|
|
|
|
if (client.pending_trade().is_some() && !self.show.trade)
|
|
|| (client.pending_trade().is_none() && self.show.trade)
|
|
{
|
|
self.show.toggle_trade();
|
|
}
|
|
|
|
//self.input = client.read_storage::<comp::ControllerInputs>();
|
|
if let Some(health) = healths.get(me) {
|
|
// Hurt Frame
|
|
let hp_percentage = health.current() / health.maximum() * 100.0;
|
|
self.hp_pulse += dt.as_secs_f32() * 10.0 / hp_percentage.clamp(3.0, 7.0);
|
|
if hp_percentage < 10.0 && !health.is_dead {
|
|
let hurt_fade = (self.hp_pulse).sin() * 0.5 + 0.6; //Animation timer
|
|
Image::new(self.imgs.hurt_bg)
|
|
.wh_of(ui_widgets.window)
|
|
.middle_of(ui_widgets.window)
|
|
.graphics_for(ui_widgets.window)
|
|
.color(Some(Color::Rgba(1.0, 1.0, 1.0, hurt_fade)))
|
|
.set(self.ids.hurt_bg, ui_widgets);
|
|
}
|
|
// Alpha Disclaimer
|
|
Text::new(&format!("Veloren {}", &version))
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(10))
|
|
.color(TEXT_COLOR)
|
|
.mid_top_with_margin_on(ui_widgets.window, 2.0)
|
|
.set(self.ids.alpha_text, ui_widgets);
|
|
|
|
// Death Frame
|
|
if health.is_dead {
|
|
Image::new(self.imgs.death_bg)
|
|
.wh_of(ui_widgets.window)
|
|
.middle_of(ui_widgets.window)
|
|
.graphics_for(ui_widgets.window)
|
|
.color(Some(Color::Rgba(0.0, 0.0, 0.0, 1.0)))
|
|
.set(self.ids.death_bg, ui_widgets);
|
|
} // Crosshair
|
|
let show_crosshair = (info.is_aiming || info.is_first_person) && !health.is_dead;
|
|
self.crosshair_opacity = Lerp::lerp(
|
|
self.crosshair_opacity,
|
|
if show_crosshair { 1.0 } else { 0.0 },
|
|
5.0 * dt.as_secs_f32(),
|
|
);
|
|
|
|
if !self.show.help {
|
|
Image::new(
|
|
// TODO: Do we want to match on this every frame?
|
|
match global_state.settings.interface.crosshair_type {
|
|
CrosshairType::Round => self.imgs.crosshair_outer_round,
|
|
CrosshairType::RoundEdges => self.imgs.crosshair_outer_round_edges,
|
|
CrosshairType::Edges => self.imgs.crosshair_outer_edges,
|
|
},
|
|
)
|
|
.w_h(21.0 * 1.5, 21.0 * 1.5)
|
|
.middle_of(ui_widgets.window)
|
|
.color(Some(Color::Rgba(
|
|
1.0,
|
|
1.0,
|
|
1.0,
|
|
self.crosshair_opacity * global_state.settings.interface.crosshair_opacity,
|
|
)))
|
|
.set(self.ids.crosshair_outer, ui_widgets);
|
|
Image::new(self.imgs.crosshair_inner)
|
|
.w_h(21.0 * 2.0, 21.0 * 2.0)
|
|
.middle_of(self.ids.crosshair_outer)
|
|
.color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.6)))
|
|
.set(self.ids.crosshair_inner, ui_widgets);
|
|
}
|
|
}
|
|
|
|
// Max amount the sct font size increases when "flashing"
|
|
const FLASH_MAX: u32 = 2;
|
|
|
|
// Get player position.
|
|
let player_pos = client
|
|
.state()
|
|
.ecs()
|
|
.read_storage::<comp::Pos>()
|
|
.get(client.entity())
|
|
.map_or(Vec3::zero(), |pos| pos.0);
|
|
// SCT Output values are called hp_damage and floater.info.amount
|
|
// Numbers are currently divided by 10 and rounded
|
|
if global_state.settings.interface.sct {
|
|
// Render Player SCT numbers
|
|
let mut player_sct_bg_id_walker = self.ids.player_sct_bgs.walk();
|
|
let mut player_sct_id_walker = self.ids.player_scts.walk();
|
|
if let (Some(HpFloaterList { floaters, .. }), Some(health)) = (
|
|
hp_floater_lists
|
|
.get_mut(me)
|
|
.filter(|fl| !fl.floaters.is_empty()),
|
|
healths.get(me),
|
|
) {
|
|
let player_font_col = |crit: bool| {
|
|
if crit {
|
|
Rgb::new(1.0, 0.9, 0.0)
|
|
} else {
|
|
Rgb::new(1.0, 0.1, 0.0)
|
|
}
|
|
};
|
|
|
|
fn calc_fade(floater: &HpFloater) -> f32 {
|
|
((crate::ecs::sys::floater::MY_HP_SHOWTIME - floater.timer) * 0.25) + 0.2
|
|
}
|
|
|
|
floaters.retain(|fl| calc_fade(fl) > 0.0);
|
|
|
|
for floater in floaters {
|
|
let number_speed = 50.0; // Player number speed
|
|
let player_sct_bg_id = player_sct_bg_id_walker.next(
|
|
&mut self.ids.player_sct_bgs,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
let player_sct_id = player_sct_id_walker.next(
|
|
&mut self.ids.player_scts,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
// Clamp the amount so you don't have absurdly large damage numbers
|
|
let max_hp_frac = floater
|
|
.info
|
|
.amount
|
|
.abs()
|
|
.clamp(Health::HEALTH_EPSILON, health.maximum() * 1.25)
|
|
/ health.maximum();
|
|
let hp_dmg_text = if floater.info.amount.abs() < 0.1 {
|
|
String::new()
|
|
} else if global_state.settings.interface.sct_damage_rounding
|
|
&& floater.info.amount.abs() >= 1.0
|
|
{
|
|
format!("{:.0}", floater.info.amount.abs())
|
|
} else {
|
|
format!("{:.1}", floater.info.amount.abs())
|
|
};
|
|
let crit = floater.info.crit;
|
|
|
|
// Timer sets text transparency
|
|
let hp_fade = calc_fade(floater);
|
|
|
|
// Increase font size based on fraction of maximum health
|
|
// "flashes" by having a larger size in the first 100ms
|
|
let font_size =
|
|
30 + (if crit {
|
|
(max_hp_frac * 10.0) as u32 * 3 + 10
|
|
} else {
|
|
(max_hp_frac * 10.0) as u32 * 3
|
|
}) + if floater.jump_timer < 0.1 {
|
|
FLASH_MAX
|
|
* (((1.0 - floater.jump_timer * 10.0)
|
|
* 10.0
|
|
* if crit { 1.25 } else { 1.0 })
|
|
as u32)
|
|
} else {
|
|
0
|
|
};
|
|
let font_col = player_font_col(crit);
|
|
// Timer sets the widget offset
|
|
let y = if floater.info.amount < 0.0 {
|
|
floater.timer as f64
|
|
* number_speed
|
|
* floater.info.amount.signum() as f64
|
|
//* -1.0
|
|
+ 300.0
|
|
- ui_widgets.win_h * 0.5
|
|
} else {
|
|
floater.timer as f64
|
|
* number_speed
|
|
* floater.info.amount.signum() as f64
|
|
* -1.0
|
|
+ 300.0
|
|
- ui_widgets.win_h * 0.5
|
|
};
|
|
// Healing is offset randomly
|
|
let x = if floater.info.amount < 0.0 {
|
|
0.0
|
|
} else {
|
|
(floater.rand as f64 - 0.5) * 0.08 * ui_widgets.win_w
|
|
+ (0.03 * ui_widgets.win_w * (floater.rand as f64 - 0.5).signum())
|
|
};
|
|
Text::new(&hp_dmg_text)
|
|
.font_size(font_size)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(0.0, 0.0, 0.0, hp_fade))
|
|
.x_y(x, y - 3.0)
|
|
.set(player_sct_bg_id, ui_widgets);
|
|
Text::new(&hp_dmg_text)
|
|
.font_size(font_size)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(if floater.info.amount < 0.0 {
|
|
Color::Rgba(font_col.r, font_col.g, font_col.b, hp_fade)
|
|
} else {
|
|
Color::Rgba(0.1, 1.0, 0.1, hp_fade)
|
|
})
|
|
.x_y(x, y)
|
|
.set(player_sct_id, ui_widgets);
|
|
}
|
|
}
|
|
// EXP Numbers
|
|
self.floaters.exp_floaters.iter_mut().for_each(|f| {
|
|
f.timer -= dt.as_secs_f32();
|
|
f.jump_timer += dt.as_secs_f32();
|
|
});
|
|
self.floaters.exp_floaters.retain(|f| f.timer > 0.0);
|
|
for floater in self.floaters.exp_floaters.iter_mut() {
|
|
let number_speed = 50.0; // Number Speed for Single EXP
|
|
let player_sct_bg_id = player_sct_bg_id_walker.next(
|
|
&mut self.ids.player_sct_bgs,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
let player_sct_id = player_sct_id_walker.next(
|
|
&mut self.ids.player_scts,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
/*let player_sct_icon_id = player_sct_id_walker.next(
|
|
&mut self.ids.player_scts,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);*/
|
|
// Increase font size based on fraction of maximum Experience
|
|
// "flashes" by having a larger size in the first 100ms
|
|
let font_size_xp = 30
|
|
+ ((floater.exp_change as f32 / 300.0).min(1.0) * 50.0) as u32
|
|
+ if floater.jump_timer < 0.1 {
|
|
FLASH_MAX * (((1.0 - floater.jump_timer * 10.0) * 10.0) as u32)
|
|
} else {
|
|
0
|
|
};
|
|
let y = floater.timer as f64 * number_speed; // Timer sets the widget offset
|
|
//let fade = ((4.0 - floater.timer as f32) * 0.25) + 0.2; // Timer sets
|
|
// text transparency
|
|
let fade = floater.timer.min(1.0);
|
|
|
|
if floater.exp_change > 0 {
|
|
let xp_pool = &floater.xp_pools;
|
|
let exp_string =
|
|
&i18n.get_msg_ctx("hud-sct-experience", &i18n::fluent_args! {
|
|
// Don't show 0 Exp
|
|
"amount" => &floater.exp_change.max(1),
|
|
});
|
|
Text::new(exp_string)
|
|
.font_size(font_size_xp)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
|
|
.x_y(
|
|
ui_widgets.win_w * (0.5 * floater.rand_offset.0 as f64 - 0.25),
|
|
ui_widgets.win_h * (0.15 * floater.rand_offset.1 as f64) + y - 3.0,
|
|
)
|
|
.set(player_sct_bg_id, ui_widgets);
|
|
Text::new(exp_string)
|
|
.font_size(font_size_xp)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(
|
|
if xp_pool.contains(&SkillGroupKind::Weapon(ToolKind::Pick)) {
|
|
Color::Rgba(0.18, 0.32, 0.9, fade)
|
|
} else {
|
|
Color::Rgba(0.59, 0.41, 0.67, fade)
|
|
},
|
|
)
|
|
.x_y(
|
|
ui_widgets.win_w * (0.5 * floater.rand_offset.0 as f64 - 0.25),
|
|
ui_widgets.win_h * (0.15 * floater.rand_offset.1 as f64) + y,
|
|
)
|
|
.set(player_sct_id, ui_widgets);
|
|
// Exp Source Image (TODO: fix widget id crash)
|
|
/*if xp_pool.contains(&SkillGroupKind::Weapon(ToolKind::Pick)) {
|
|
Image::new(self.imgs.pickaxe_ico)
|
|
.w_h(font_size_xp as f64, font_size_xp as f64)
|
|
.left_from(player_sct_id, 5.0)
|
|
.set(player_sct_icon_id, ui_widgets);
|
|
}*/
|
|
}
|
|
}
|
|
|
|
// Skill points
|
|
self.floaters
|
|
.skill_point_displays
|
|
.iter_mut()
|
|
.for_each(|f| f.timer -= dt.as_secs_f32());
|
|
self.floaters
|
|
.skill_point_displays
|
|
.retain(|d| d.timer > 0_f32);
|
|
if let Some(display) = self.floaters.skill_point_displays.iter_mut().next() {
|
|
let fade = if display.timer < 3.0 {
|
|
display.timer * 0.33
|
|
} else if display.timer < 2.0 {
|
|
display.timer * 0.33 * 0.1
|
|
} else {
|
|
1.0
|
|
};
|
|
// Background image
|
|
let offset = if display.timer < 2.0 {
|
|
300.0 - (display.timer as f64 - 2.0) * -300.0
|
|
} else {
|
|
300.0
|
|
};
|
|
Image::new(self.imgs.level_up)
|
|
.w_h(328.0, 126.0)
|
|
.mid_top_with_margin_on(ui_widgets.window, offset)
|
|
.graphics_for(ui_widgets.window)
|
|
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
|
|
.set(self.ids.player_rank_up, ui_widgets);
|
|
// Rank Number
|
|
let rank = display.total_points;
|
|
let fontsize = match rank {
|
|
1..=99 => (20, 8.0),
|
|
100..=999 => (18, 9.0),
|
|
1000..=9999 => (17, 10.0),
|
|
_ => (14, 12.0),
|
|
};
|
|
Text::new(&format!("{}", rank))
|
|
.font_size(fontsize.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(1.0, 1.0, 1.0, fade))
|
|
.mid_top_with_margin_on(self.ids.player_rank_up, fontsize.1)
|
|
.set(self.ids.player_rank_up_txt_number, ui_widgets);
|
|
// Static "New Rank!" text
|
|
Text::new(&i18n.get_msg("hud-rank_up"))
|
|
.font_size(40)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
|
|
.mid_bottom_with_margin_on(self.ids.player_rank_up, 20.0)
|
|
.set(self.ids.player_rank_up_txt_0_bg, ui_widgets);
|
|
Text::new(&i18n.get_msg("hud-rank_up"))
|
|
.font_size(40)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(1.0, 1.0, 1.0, fade))
|
|
.bottom_left_with_margins_on(self.ids.player_rank_up_txt_0_bg, 2.0, 2.0)
|
|
.set(self.ids.player_rank_up_txt_0, ui_widgets);
|
|
// Variable skilltree text
|
|
let skill = match display.skill_tree {
|
|
General => i18n.get_msg("common-weapons-general"),
|
|
Weapon(ToolKind::Hammer) => i18n.get_msg("common-weapons-hammer"),
|
|
Weapon(ToolKind::Axe) => i18n.get_msg("common-weapons-axe"),
|
|
Weapon(ToolKind::Sword) => i18n.get_msg("common-weapons-sword"),
|
|
Weapon(ToolKind::Sceptre) => i18n.get_msg("common-weapons-sceptre"),
|
|
Weapon(ToolKind::Bow) => i18n.get_msg("common-weapons-bow"),
|
|
Weapon(ToolKind::Staff) => i18n.get_msg("common-weapons-staff"),
|
|
Weapon(ToolKind::Pick) => i18n.get_msg("common-tool-mining"),
|
|
_ => Cow::Borrowed("Unknown"),
|
|
};
|
|
Text::new(&skill)
|
|
.font_size(20)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
|
|
.mid_top_with_margin_on(self.ids.player_rank_up, 45.0)
|
|
.set(self.ids.player_rank_up_txt_1_bg, ui_widgets);
|
|
Text::new(&skill)
|
|
.font_size(20)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(1.0, 1.0, 1.0, fade))
|
|
.bottom_left_with_margins_on(self.ids.player_rank_up_txt_1_bg, 2.0, 2.0)
|
|
.set(self.ids.player_rank_up_txt_1, ui_widgets);
|
|
// Variable skilltree icon
|
|
use crate::hud::SkillGroupKind::{General, Weapon};
|
|
Image::new(match display.skill_tree {
|
|
General => self.imgs.swords_crossed,
|
|
Weapon(ToolKind::Hammer) => self.imgs.hammer,
|
|
Weapon(ToolKind::Axe) => self.imgs.axe,
|
|
Weapon(ToolKind::Sword) => self.imgs.sword,
|
|
Weapon(ToolKind::Sceptre) => self.imgs.sceptre,
|
|
Weapon(ToolKind::Bow) => self.imgs.bow,
|
|
Weapon(ToolKind::Staff) => self.imgs.staff,
|
|
Weapon(ToolKind::Pick) => self.imgs.mining,
|
|
_ => self.imgs.swords_crossed,
|
|
})
|
|
.w_h(20.0, 20.0)
|
|
.left_from(self.ids.player_rank_up_txt_1_bg, 5.0)
|
|
.color(Some(Color::Rgba(1.0, 1.0, 1.0, fade)))
|
|
.set(self.ids.player_rank_up_icon, ui_widgets);
|
|
}
|
|
|
|
// Scrolling Combat Text for Parrying an attack
|
|
self.floaters
|
|
.block_floaters
|
|
.iter_mut()
|
|
.for_each(|f| f.timer -= dt.as_secs_f32());
|
|
self.floaters.block_floaters.retain(|f| f.timer > 0_f32);
|
|
for floater in self.floaters.block_floaters.iter_mut() {
|
|
let number_speed = 50.0;
|
|
let player_sct_bg_id = player_sct_bg_id_walker.next(
|
|
&mut self.ids.player_sct_bgs,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
let player_sct_id = player_sct_id_walker.next(
|
|
&mut self.ids.player_scts,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
let font_size = 30;
|
|
let y = floater.timer as f64 * number_speed; // Timer sets the widget offset
|
|
// text transparency
|
|
let fade = if floater.timer < 0.25 {
|
|
floater.timer / 0.25
|
|
} else {
|
|
1.0
|
|
};
|
|
|
|
Text::new(&i18n.get_msg("hud-sct-block"))
|
|
.font_size(font_size)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(0.0, 0.0, 0.0, fade))
|
|
.x_y(
|
|
ui_widgets.win_w * (0.0),
|
|
ui_widgets.win_h * (-0.3) + y - 3.0,
|
|
)
|
|
.set(player_sct_bg_id, ui_widgets);
|
|
Text::new(&i18n.get_msg("hud-sct-block"))
|
|
.font_size(font_size)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(Color::Rgba(0.69, 0.82, 0.88, fade))
|
|
.x_y(ui_widgets.win_w * 0.0, ui_widgets.win_h * -0.3 + y)
|
|
.set(player_sct_id, ui_widgets);
|
|
}
|
|
}
|
|
|
|
// Pop speech bubbles
|
|
let now = Instant::now();
|
|
self.speech_bubbles
|
|
.retain(|_uid, bubble| bubble.timeout > now);
|
|
|
|
// Don't show messages from muted players
|
|
self.new_messages.retain(|msg| match msg.uid() {
|
|
Some(uid) => match client.player_list().get(&uid) {
|
|
Some(player_info) => {
|
|
if let Some(uuid) = get_player_uuid(client, &player_info.player_alias) {
|
|
!global_state.profile.mutelist.contains_key(&uuid)
|
|
} else {
|
|
true
|
|
}
|
|
},
|
|
None => true,
|
|
},
|
|
None => true,
|
|
});
|
|
|
|
// Push speech bubbles
|
|
for msg in self.new_messages.iter() {
|
|
if let Some((bubble, uid)) = msg.to_bubble() {
|
|
self.speech_bubbles.insert(uid, bubble);
|
|
}
|
|
}
|
|
|
|
let mut overhead_walker = self.ids.overheads.walk();
|
|
let mut overitem_walker = self.ids.overitems.walk();
|
|
let mut sct_walker = self.ids.scts.walk();
|
|
let mut sct_bg_walker = self.ids.sct_bgs.walk();
|
|
let pulse = self.pulse;
|
|
|
|
let make_overitem =
|
|
|item: &Item, pos, distance, properties, fonts, interaction_options| {
|
|
let quality = get_quality_col(item);
|
|
|
|
// Item
|
|
overitem::Overitem::new(
|
|
item.describe().into(),
|
|
quality,
|
|
distance,
|
|
fonts,
|
|
i18n,
|
|
&global_state.settings.controls,
|
|
properties,
|
|
pulse,
|
|
&global_state.window.key_layout,
|
|
interaction_options,
|
|
)
|
|
.x_y(0.0, 100.0)
|
|
.position_ingame(pos)
|
|
};
|
|
|
|
self.failed_block_pickups
|
|
.retain(|_, t| pulse - t.pulse < overitem::PICKUP_FAILED_FADE_OUT_TIME);
|
|
self.failed_entity_pickups
|
|
.retain(|_, t| pulse - t.pulse < overitem::PICKUP_FAILED_FADE_OUT_TIME);
|
|
|
|
// Render overitem: name, etc.
|
|
for (entity, pos, item, distance) in (&entities, &pos, &items)
|
|
.join()
|
|
.map(|(entity, pos, item)| (entity, pos, item, pos.0.distance_squared(player_pos)))
|
|
.filter(|(_, _, _, distance)| distance < &MAX_PICKUP_RANGE.powi(2))
|
|
{
|
|
let overitem_id = overitem_walker.next(
|
|
&mut self.ids.overitems,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
|
|
make_overitem(
|
|
item,
|
|
pos.0 + Vec3::unit_z() * 1.2,
|
|
distance,
|
|
overitem::OveritemProperties {
|
|
active: interactable.and_then(|i| i.entity()) == Some(entity),
|
|
pickup_failed_pulse: self.failed_entity_pickups.get(&entity).cloned(),
|
|
},
|
|
&self.fonts,
|
|
vec![(
|
|
Some(GameInput::Interact),
|
|
i18n.get_msg("hud-pick_up").to_string(),
|
|
)],
|
|
)
|
|
.set(overitem_id, ui_widgets);
|
|
}
|
|
|
|
// Render overtime for an interactable block
|
|
if let Some(Interactable::Block(block, pos, interaction)) = interactable {
|
|
let overitem_id = overitem_walker.next(
|
|
&mut self.ids.overitems,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
|
|
let overitem_properties = overitem::OveritemProperties {
|
|
active: true,
|
|
pickup_failed_pulse: self.failed_block_pickups.get(pos).cloned(),
|
|
};
|
|
let pos = pos.map(|e| e as f32 + 0.5);
|
|
let over_pos = pos + Vec3::unit_z() * 0.7;
|
|
|
|
let interaction_text = || match interaction {
|
|
BlockInteraction::Collect => {
|
|
vec![(
|
|
Some(GameInput::Interact),
|
|
i18n.get_msg("hud-collect").to_string(),
|
|
)]
|
|
},
|
|
BlockInteraction::Craft(_) => {
|
|
vec![(
|
|
Some(GameInput::Interact),
|
|
i18n.get_msg("hud-use").to_string(),
|
|
)]
|
|
},
|
|
BlockInteraction::Unlock(kind) => {
|
|
vec![(Some(GameInput::Interact), match kind {
|
|
UnlockKind::Free => i18n.get_msg("hud-open").to_string(),
|
|
UnlockKind::Requires(item) => i18n
|
|
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
|
|
"item" => item.as_ref().itemdef_id()
|
|
.map(|id| Item::new_from_asset_expect(id).describe())
|
|
.unwrap_or_else(|| "modular item".to_string()),
|
|
})
|
|
.to_string(),
|
|
UnlockKind::Consumes(item) => i18n
|
|
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
|
|
"item" => item.as_ref().itemdef_id()
|
|
.map(|id| Item::new_from_asset_expect(id).describe())
|
|
.unwrap_or_else(|| "modular item".to_string()),
|
|
})
|
|
.to_string(),
|
|
})]
|
|
},
|
|
BlockInteraction::Mine(mine_tool) => {
|
|
if info.is_mining {
|
|
vec![(
|
|
Some(GameInput::Primary),
|
|
i18n.get_msg("hud-mine").to_string(),
|
|
)]
|
|
} else {
|
|
match mine_tool {
|
|
ToolKind::Pick => {
|
|
vec![(None, i18n.get_msg("hud-mine-needs_pickaxe").to_string())]
|
|
},
|
|
// TODO: The required tool for mining something may not always be a
|
|
// pickaxe!
|
|
_ => {
|
|
vec![(
|
|
None,
|
|
i18n.get_msg("hud-mine-needs_unhandled_case").to_string(),
|
|
)]
|
|
},
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
// This is only done once per frame, so it's not a performance issue
|
|
if let Some(desc) = block
|
|
.get_sprite()
|
|
.filter(|s| s.is_container())
|
|
.and_then(|s| get_sprite_desc(s, i18n))
|
|
{
|
|
overitem::Overitem::new(
|
|
desc,
|
|
overitem::TEXT_COLOR,
|
|
pos.distance_squared(player_pos),
|
|
&self.fonts,
|
|
i18n,
|
|
&global_state.settings.controls,
|
|
overitem_properties,
|
|
self.pulse,
|
|
&global_state.window.key_layout,
|
|
vec![(
|
|
Some(GameInput::Interact),
|
|
i18n.get_msg("hud-open").to_string(),
|
|
)],
|
|
)
|
|
.x_y(0.0, 100.0)
|
|
.position_ingame(over_pos)
|
|
.set(overitem_id, ui_widgets);
|
|
} else if let Some(item) = Item::try_reclaim_from_block(*block) {
|
|
make_overitem(
|
|
&item,
|
|
over_pos,
|
|
pos.distance_squared(player_pos),
|
|
overitem_properties,
|
|
&self.fonts,
|
|
interaction_text(),
|
|
)
|
|
.set(overitem_id, ui_widgets);
|
|
} else if let Some(desc) = block.get_sprite().and_then(|s| get_sprite_desc(s, i18n))
|
|
{
|
|
overitem::Overitem::new(
|
|
desc,
|
|
overitem::TEXT_COLOR,
|
|
pos.distance_squared(player_pos),
|
|
&self.fonts,
|
|
i18n,
|
|
&global_state.settings.controls,
|
|
overitem_properties,
|
|
self.pulse,
|
|
&global_state.window.key_layout,
|
|
interaction_text(),
|
|
)
|
|
.x_y(0.0, 100.0)
|
|
.position_ingame(over_pos)
|
|
.set(overitem_id, ui_widgets);
|
|
}
|
|
} else if let Some(Interactable::Entity(entity)) = interactable {
|
|
// show hud for campfire
|
|
if client
|
|
.state()
|
|
.ecs()
|
|
.read_storage::<comp::Body>()
|
|
.get(*entity)
|
|
.map_or(false, |b| b.is_campfire())
|
|
{
|
|
let overitem_id = overitem_walker.next(
|
|
&mut self.ids.overitems,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
|
|
let overitem_properties = overitem::OveritemProperties {
|
|
active: true,
|
|
pickup_failed_pulse: None,
|
|
};
|
|
let pos = client
|
|
.state()
|
|
.ecs()
|
|
.read_storage::<comp::Pos>()
|
|
.get(*entity)
|
|
.map_or(Vec3::zero(), |e| e.0);
|
|
let over_pos = pos + Vec3::unit_z() * 1.5;
|
|
|
|
overitem::Overitem::new(
|
|
i18n.get_msg("hud-crafting-campfire"),
|
|
overitem::TEXT_COLOR,
|
|
pos.distance_squared(player_pos),
|
|
&self.fonts,
|
|
i18n,
|
|
&global_state.settings.controls,
|
|
overitem_properties,
|
|
self.pulse,
|
|
&global_state.window.key_layout,
|
|
vec![(
|
|
Some(GameInput::Interact),
|
|
i18n.get_msg("hud-sit").to_string(),
|
|
)],
|
|
)
|
|
.x_y(0.0, 100.0)
|
|
.position_ingame(over_pos)
|
|
.set(overitem_id, ui_widgets);
|
|
}
|
|
}
|
|
|
|
let speech_bubbles = &self.speech_bubbles;
|
|
|
|
// Render overhead name tags and health bars
|
|
for (
|
|
entity,
|
|
pos,
|
|
info,
|
|
bubble,
|
|
_,
|
|
_,
|
|
health,
|
|
_,
|
|
scale,
|
|
body,
|
|
hpfl,
|
|
in_group,
|
|
dist_sqr,
|
|
alignment,
|
|
is_mount,
|
|
) in (
|
|
&entities,
|
|
&pos,
|
|
interpolated.maybe(),
|
|
&stats,
|
|
&skill_sets,
|
|
healths.maybe(),
|
|
&buffs,
|
|
energy.maybe(),
|
|
scales.maybe(),
|
|
&bodies,
|
|
&mut hp_floater_lists,
|
|
&uids,
|
|
&inventories,
|
|
poises.maybe(),
|
|
(alignments.maybe(), is_mount.maybe(), stances.maybe()),
|
|
)
|
|
.join()
|
|
.filter(|t| {
|
|
let health = t.5;
|
|
!health.map_or(false, |h| h.is_dead)
|
|
})
|
|
.filter_map(
|
|
|(
|
|
entity,
|
|
pos,
|
|
interpolated,
|
|
stats,
|
|
skill_set,
|
|
health,
|
|
buffs,
|
|
energy,
|
|
scale,
|
|
body,
|
|
hpfl,
|
|
uid,
|
|
inventory,
|
|
poise,
|
|
(alignment, is_mount, stance),
|
|
)| {
|
|
// Use interpolated position if available
|
|
let pos = interpolated.map_or(pos.0, |i| i.pos);
|
|
let in_group = client.group_members().contains_key(uid);
|
|
let is_me = entity == me;
|
|
// TODO: once the site2 rework lands and merchants have dedicated stalls or
|
|
// buildings, they no longer need to be emphasized via the higher overhead
|
|
// text radius relative to other NPCs
|
|
let is_merchant =
|
|
stats.name == "Merchant" && client.player_list().get(uid).is_none();
|
|
let dist_sqr = pos.distance_squared(player_pos);
|
|
// Determine whether to display nametag and healthbar based on whether the
|
|
// entity has been damaged, is targeted/selected, or is in your group
|
|
// Note: even if this passes the healthbar can be hidden in some cases if it
|
|
// is at maximum
|
|
let display_overhead_info = !is_me
|
|
&& (info.target_entity.map_or(false, |e| e == entity)
|
|
|| info.selected_entity.map_or(false, |s| s.0 == entity)
|
|
|| health.map_or(true, overhead::should_show_healthbar)
|
|
|| in_group
|
|
|| is_merchant)
|
|
&& dist_sqr
|
|
< (if in_group {
|
|
NAMETAG_GROUP_RANGE
|
|
} else if is_merchant {
|
|
NAMETAG_MERCHANT_RANGE
|
|
} else if hpfl
|
|
.time_since_last_dmg_by_me
|
|
.map_or(false, |t| t < NAMETAG_DMG_TIME)
|
|
{
|
|
NAMETAG_DMG_RANGE
|
|
} else {
|
|
NAMETAG_RANGE
|
|
})
|
|
.powi(2);
|
|
|
|
let info = display_overhead_info.then(|| overhead::Info {
|
|
name: &stats.name,
|
|
health,
|
|
buffs,
|
|
energy,
|
|
combat_rating: if let (Some(health), Some(energy), Some(poise)) =
|
|
(health, energy, poise)
|
|
{
|
|
combat::combat_rating(
|
|
inventory, health, energy, poise, skill_set, *body, &msm,
|
|
)
|
|
} else {
|
|
0.0
|
|
},
|
|
stance,
|
|
});
|
|
// Only render bubble if nearby or if its me and setting is on
|
|
let bubble = if (dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) && !is_me)
|
|
|| (is_me && global_state.settings.interface.speech_bubble_self)
|
|
{
|
|
speech_bubbles.get(uid)
|
|
} else {
|
|
None
|
|
};
|
|
(info.is_some() || bubble.is_some()).then_some({
|
|
(
|
|
entity, pos, info, bubble, stats, skill_set, health, buffs, scale,
|
|
body, hpfl, in_group, dist_sqr, alignment, is_mount,
|
|
)
|
|
})
|
|
},
|
|
)
|
|
{
|
|
let overhead_id = overhead_walker.next(
|
|
&mut self.ids.overheads,
|
|
&mut ui_widgets.widget_id_generator(),
|
|
);
|
|
|
|
let height_offset = body.height() * scale.map_or(1.0, |s| s.0) + 0.5;
|
|
let ingame_pos = pos + Vec3::unit_z() * height_offset;
|
|
|
|
// Speech bubble, name, level, and hp bars
|
|
overhead::Overhead::new(
|
|
info,
|
|
bubble,
|
|
in_group,
|
|
&global_state.settings.interface,
|
|
self.pulse,
|
|
i18n,
|
|
&global_state.settings.controls,
|
|
&self.imgs,
|
|
&self.fonts,
|
|
&global_state.window.key_layout,
|
|
match alignment {
|
|
// TODO: Don't use `MAX_MOUNT_RANGE` here, add dedicated interaction range
|
|
Some(comp::Alignment::Npc)
|
|
if dist_sqr < common::consts::MAX_MOUNT_RANGE.powi(2)
|
|
&& interactable.and_then(|i| i.entity()) == Some(entity) =>
|
|
{
|
|
vec![
|
|
(GameInput::Interact, i18n.get_msg("hud-talk").to_string()),
|
|
(GameInput::Trade, i18n.get_msg("hud-trade").to_string()),
|
|
]
|
|
},
|
|
Some(comp::Alignment::Owned(owner))
|
|
if Some(*owner) == client.uid()
|
|
&& dist_sqr < common::consts::MAX_MOUNT_RANGE.powi(2) =>
|
|
{
|
|
let mut options =
|
|
vec![(GameInput::Trade, i18n.get_msg("hud-trade").to_string())];
|
|
if !client.is_riding()
|
|
&& is_mount.is_none()
|
|
&& is_mountable(body, bodies.get(client.entity()))
|
|
{
|
|
options
|
|
.push((GameInput::Mount, i18n.get_msg("hud-mount").to_string()))
|
|
}
|
|
options
|
|
},
|
|
_ => Vec::new(),
|
|
},
|
|
&time,
|
|
)
|
|
.x_y(0.0, 100.0)
|
|
.position_ingame(ingame_pos)
|
|
.set(overhead_id, ui_widgets);
|
|
|
|
// Enemy SCT
|
|
if global_state.settings.interface.sct && !hpfl.floaters.is_empty() {
|
|
fn calc_fade(floater: &HpFloater) -> f32 {
|
|
if floater.info.crit {
|
|
((crate::ecs::sys::floater::CRIT_SHOWTIME - floater.timer) * 0.75) + 0.5
|
|
} else {
|
|
((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer) * 0.25) + 0.2
|
|
}
|
|
}
|
|
|
|
hpfl.floaters.retain(|fl| calc_fade(fl) > 0.0);
|
|
let floaters = &hpfl.floaters;
|
|
|
|
// Colors
|
|
const WHITE: Rgb<f32> = Rgb::new(1.0, 0.9, 0.8);
|
|
const LIGHT_OR: Rgb<f32> = Rgb::new(1.0, 0.925, 0.749);
|
|
const LIGHT_MED_OR: Rgb<f32> = Rgb::new(1.0, 0.85, 0.498);
|
|
const MED_OR: Rgb<f32> = Rgb::new(1.0, 0.776, 0.247);
|
|
const DARK_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.7, 0.0);
|
|
const RED_ORANGE: Rgb<f32> = Rgb::new(1.0, 0.349, 0.0);
|
|
const DAMAGE_COLORS: [Rgb<f32>; 6] = [
|
|
WHITE,
|
|
LIGHT_OR,
|
|
LIGHT_MED_OR,
|
|
MED_OR,
|
|
DARK_ORANGE,
|
|
RED_ORANGE,
|
|
];
|
|
// Largest value that select the first color is 40, then it shifts colors
|
|
// every 5
|
|
let font_col = |font_size: u32, crit: bool| {
|
|
if crit {
|
|
Rgb::new(1.0, 0.9, 0.0)
|
|
} else {
|
|
DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize]
|
|
}
|
|
};
|
|
|
|
for floater in floaters {
|
|
let number_speed = 250.0; // Enemy number speed
|
|
let sct_id = sct_walker
|
|
.next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator());
|
|
let sct_bg_id = sct_bg_walker
|
|
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
|
|
// Clamp the amount so you don't have absurdly large damage numbers
|
|
let max_hp_frac = floater
|
|
.info
|
|
.amount
|
|
.abs()
|
|
.clamp(Health::HEALTH_EPSILON, health.map_or(1.0, |h| h.maximum()))
|
|
/ health.map_or(1.0, |h| h.maximum());
|
|
let hp_dmg_text = if floater.info.amount.abs() < 0.1 {
|
|
String::new()
|
|
} else if global_state.settings.interface.sct_damage_rounding
|
|
&& floater.info.amount.abs() >= 1.0
|
|
{
|
|
format!("{:.0}", floater.info.amount.abs())
|
|
} else {
|
|
format!("{:.1}", floater.info.amount.abs())
|
|
};
|
|
let crit = floater.info.crit;
|
|
// Timer sets text transparency
|
|
let fade = calc_fade(floater);
|
|
// Increase font size based on fraction of maximum health
|
|
// "flashes" by having a larger size in the first 100ms
|
|
let font_size =
|
|
30 + (if crit {
|
|
(max_hp_frac * 10.0) as u32 * 3 + 10
|
|
} else {
|
|
(max_hp_frac * 10.0) as u32 * 3
|
|
}) + if floater.jump_timer < 0.1 {
|
|
FLASH_MAX
|
|
* (((1.0 - floater.jump_timer * 10.0)
|
|
* 10.0
|
|
* if crit { 1.25 } else { 1.0 })
|
|
as u32)
|
|
} else {
|
|
0
|
|
};
|
|
let font_col = font_col(font_size, crit);
|
|
// Timer sets the widget offset
|
|
let y = if crit {
|
|
ui_widgets.win_h * (floater.rand as f64 % 0.075)
|
|
+ ui_widgets.win_h * 0.05
|
|
} else {
|
|
(floater.timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64
|
|
* number_speed)
|
|
+ 100.0
|
|
};
|
|
|
|
let x = if !crit {
|
|
0.0
|
|
} else {
|
|
(floater.rand as f64 - 0.5) * 0.075 * ui_widgets.win_w
|
|
+ (0.03 * ui_widgets.win_w * (floater.rand as f64 - 0.5).signum())
|
|
};
|
|
|
|
Text::new(&hp_dmg_text)
|
|
.font_size(font_size)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(if floater.info.amount < 0.0 {
|
|
Color::Rgba(0.0, 0.0, 0.0, fade)
|
|
} else {
|
|
Color::Rgba(0.0, 0.0, 0.0, 1.0)
|
|
})
|
|
.x_y(x, y - 3.0)
|
|
.position_ingame(ingame_pos)
|
|
.set(sct_bg_id, ui_widgets);
|
|
Text::new(&hp_dmg_text)
|
|
.font_size(font_size)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.x_y(x, y)
|
|
.color(if floater.info.amount < 0.0 {
|
|
Color::Rgba(font_col.r, font_col.g, font_col.b, fade)
|
|
} else {
|
|
Color::Rgba(0.1, 1.0, 0.1, 1.0)
|
|
})
|
|
.position_ingame(ingame_pos)
|
|
.set(sct_id, ui_widgets);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display debug window.
|
|
// TODO:
|
|
// Make it use i18n keys.
|
|
if let Some(debug_info) = debug_info {
|
|
prof_span!("debug info");
|
|
|
|
const V_PAD: f64 = 5.0;
|
|
const H_PAD: f64 = 5.0;
|
|
|
|
// Alpha Version
|
|
Text::new(&version)
|
|
.top_left_with_margins_on(self.ids.debug_bg, V_PAD, H_PAD)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(TEXT_COLOR)
|
|
.set(self.ids.version, ui_widgets);
|
|
// Ticks per second
|
|
Text::new(&format!(
|
|
"FPS: {:.0} ({}ms)",
|
|
debug_info.tps,
|
|
debug_info.frame_time.as_millis()
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.version, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.fps_counter, ui_widgets);
|
|
// Ping
|
|
Text::new(&format!("Ping: {:.0}ms", debug_info.ping_ms))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.fps_counter, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.ping, ui_widgets);
|
|
// Player's position
|
|
let coordinates_text = match debug_info.coordinates {
|
|
Some(coordinates) => format!(
|
|
"Coordinates: ({:.0}, {:.0}, {:.0})",
|
|
coordinates.0.x, coordinates.0.y, coordinates.0.z,
|
|
),
|
|
None => "Player has no Pos component".to_owned(),
|
|
};
|
|
Text::new(&coordinates_text)
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.ping, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.coordinates, ui_widgets);
|
|
// Player's velocity
|
|
let (velocity_text, glide_ratio_text) = match debug_info.velocity {
|
|
Some(velocity) => {
|
|
let velocity = velocity.0;
|
|
let velocity_text = format!(
|
|
"Velocity: ({:.1}, {:.1}, {:.1}) [{:.1} u/s]",
|
|
velocity.x,
|
|
velocity.y,
|
|
velocity.z,
|
|
velocity.magnitude()
|
|
);
|
|
let horizontal_velocity = velocity.xy().magnitude();
|
|
let dz = velocity.z;
|
|
// don't divide by zero
|
|
let glide_ratio_text = if dz.abs() > 0.0001 {
|
|
format!("Glide Ratio: {:.1}", (-1.0) * (horizontal_velocity / dz))
|
|
} else {
|
|
"Glide Ratio: Altitude is constant".to_owned()
|
|
};
|
|
|
|
(velocity_text, glide_ratio_text)
|
|
},
|
|
None => {
|
|
let err = "Player has no Vel component";
|
|
(err.to_owned(), err.to_owned())
|
|
},
|
|
};
|
|
Text::new(&velocity_text)
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.coordinates, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.velocity, ui_widgets);
|
|
Text::new(&glide_ratio_text)
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.velocity, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.glide_ratio, ui_widgets);
|
|
let glide_angle_text = angle_of_attack_text(
|
|
debug_info.in_fluid,
|
|
debug_info.velocity,
|
|
debug_info.character_state.as_ref(),
|
|
);
|
|
Text::new(&glide_angle_text)
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.glide_ratio, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.glide_aoe, ui_widgets);
|
|
// Player's orientation vector
|
|
let orientation_text = match debug_info.ori {
|
|
Some(ori) => {
|
|
let orientation = ori.look_dir();
|
|
format!(
|
|
"Orientation: ({:.2}, {:.2}, {:.2})",
|
|
orientation.x, orientation.y, orientation.z,
|
|
)
|
|
},
|
|
None => "Player has no Ori component".to_owned(),
|
|
};
|
|
Text::new(&orientation_text)
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.glide_aoe, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.orientation, ui_widgets);
|
|
let look_dir_text = {
|
|
let look_vec = debug_info.look_dir.to_vec();
|
|
|
|
format!(
|
|
"Look Direction: ({:.2}, {:.2}, {:.2})",
|
|
look_vec.x, look_vec.y, look_vec.z,
|
|
)
|
|
};
|
|
Text::new(&look_dir_text)
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.orientation, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.look_direction, ui_widgets);
|
|
// Loaded distance
|
|
Text::new(&format!(
|
|
"View distance: {:.2} blocks ({:.2} chunks)",
|
|
client.loaded_distance(),
|
|
client.loaded_distance() / TerrainChunk::RECT_SIZE.x as f32,
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.look_direction, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.loaded_distance, ui_widgets);
|
|
// Time
|
|
let time_in_seconds = client.state().get_time_of_day();
|
|
let current_time = NaiveTime::from_num_seconds_from_midnight(
|
|
// Wraps around back to 0s if it exceeds 24 hours (24 hours = 86400s)
|
|
(time_in_seconds as u64 % 86400) as u32,
|
|
0,
|
|
);
|
|
Text::new(&format!("Time: {}", current_time.format("%H:%M")))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.loaded_distance, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.time, ui_widgets);
|
|
// Weather
|
|
let weather = client.weather_at_player();
|
|
Text::new(&format!(
|
|
"Weather({kind}): {{cloud: {cloud:.2}, rain: {rain:.2}, wind: <{wind_x:.0}, \
|
|
{wind_y:.0}>}}",
|
|
kind = weather.get_kind(),
|
|
cloud = weather.cloud,
|
|
rain = weather.rain,
|
|
wind_x = weather.wind.x,
|
|
wind_y = weather.wind.y
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.time, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.weather, ui_widgets);
|
|
|
|
// Number of entities
|
|
let entity_count = client.state().ecs().entities().join().count();
|
|
Text::new(&format!("Entity count: {}", entity_count))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.weather, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.entity_count, ui_widgets);
|
|
|
|
// Number of chunks
|
|
Text::new(&format!(
|
|
"Chunks: {} ({} visible) & {} (shadow)",
|
|
debug_info.num_chunks, debug_info.num_visible_chunks, debug_info.num_shadow_chunks,
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.entity_count, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.num_chunks, ui_widgets);
|
|
|
|
// Type of biome
|
|
Text::new(&format!("Biome: {:?}", client.current_biome()))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.num_chunks, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.current_biome, ui_widgets);
|
|
|
|
// Type of site
|
|
Text::new(&format!("Site: {:?}", client.current_site()))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.current_biome, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.current_site, ui_widgets);
|
|
|
|
// Current song info
|
|
Text::new(&format!(
|
|
"Now playing: {} [{}]",
|
|
debug_info.current_track, debug_info.current_artist,
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.current_site, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.song_info, ui_widgets);
|
|
|
|
// Number of lights
|
|
Text::new(&format!("Lights: {}", debug_info.num_lights,))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.song_info, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.num_lights, ui_widgets);
|
|
|
|
// Number of figures
|
|
Text::new(&format!(
|
|
"Figures: {} ({} visible)",
|
|
debug_info.num_figures, debug_info.num_figures_visible,
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.num_lights, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.num_figures, ui_widgets);
|
|
|
|
// Number of particles
|
|
Text::new(&format!(
|
|
"Particles: {} ({} visible)",
|
|
debug_info.num_particles, debug_info.num_particles_visible,
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.num_figures, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.num_particles, ui_widgets);
|
|
|
|
// Graphics backend
|
|
Text::new(&format!(
|
|
"Graphics backend: {}",
|
|
global_state.window.renderer().graphics_backend(),
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.down_from(self.ids.num_particles, V_PAD)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.set(self.ids.graphics_backend, ui_widgets);
|
|
|
|
let gpu_timings = global_state.window.renderer().timings();
|
|
let mut timings_height = 0.0;
|
|
|
|
// GPU timing for different pipelines
|
|
if !gpu_timings.is_empty() {
|
|
let num_timings = gpu_timings.len();
|
|
// Make sure we have enough ids
|
|
if self.ids.gpu_timings.len() < num_timings {
|
|
self.ids
|
|
.gpu_timings
|
|
.resize(num_timings, &mut ui_widgets.widget_id_generator());
|
|
}
|
|
|
|
for (i, timing) in gpu_timings.iter().enumerate() {
|
|
let timings_text = &format!(
|
|
"{:16}{:.3} ms",
|
|
&format!("{}:", timing.1),
|
|
timing.2 * 1000.0,
|
|
);
|
|
let timings_widget = Text::new(timings_text)
|
|
.color(TEXT_COLOR)
|
|
.down(V_PAD)
|
|
.x_place_on(
|
|
self.ids.debug_bg,
|
|
conrod_core::position::Place::Start(Some(
|
|
H_PAD + 10.0 * timing.0 as f64,
|
|
)),
|
|
)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14));
|
|
|
|
// Calculate timings height
|
|
timings_height += timings_widget.get_h(ui_widgets).unwrap_or(0.0) + V_PAD;
|
|
|
|
timings_widget.set(self.ids.gpu_timings[i], ui_widgets);
|
|
}
|
|
}
|
|
|
|
// Set debug box dimensions, only timings height is dynamic
|
|
// TODO: Make the background box size fully dynamic
|
|
|
|
let debug_bg_size = [375.0, 405.0 + timings_height];
|
|
|
|
Rectangle::fill(debug_bg_size)
|
|
.rgba(0.0, 0.0, 0.0, global_state.settings.chat.chat_opacity)
|
|
.top_left_with_margins_on(ui_widgets.window, 10.0, 10.0)
|
|
.set(self.ids.debug_bg, ui_widgets);
|
|
}
|
|
|
|
if global_state.settings.interface.toggle_hotkey_hints {
|
|
// Help Window
|
|
if let Some(help_key) = global_state.settings.controls.get_binding(GameInput::Help) {
|
|
Text::new(&i18n.get_msg_ctx(
|
|
"hud-press_key_to_show_keybindings_fmt",
|
|
&i18n::fluent_args! {
|
|
"key" => help_key.display_string(key_layout),
|
|
},
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.bottom_left_with_margins_on(ui_widgets.window, 210.0, 10.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(12))
|
|
.set(self.ids.help_info, ui_widgets);
|
|
}
|
|
// Lantern Key
|
|
if let Some(toggle_lantern_key) = global_state
|
|
.settings
|
|
.controls
|
|
.get_binding(GameInput::ToggleLantern)
|
|
{
|
|
Text::new(&i18n.get_msg_ctx(
|
|
"hud-press_key_to_toggle_lantern_fmt",
|
|
&i18n::fluent_args! {
|
|
"key" => toggle_lantern_key.display_string(key_layout),
|
|
},
|
|
))
|
|
.color(TEXT_COLOR)
|
|
.up_from(self.ids.help_info, 2.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(12))
|
|
.set(self.ids.lantern_info, ui_widgets);
|
|
}
|
|
}
|
|
|
|
// Bag button and nearby icons
|
|
let ecs = client.state().ecs();
|
|
// let entity = info.viewpoint_entity;
|
|
let stats = ecs.read_storage::<comp::Stats>();
|
|
let skill_sets = ecs.read_storage::<comp::SkillSet>();
|
|
let buffs = ecs.read_storage::<comp::Buffs>();
|
|
let msm = ecs.read_resource::<MaterialStatManifest>();
|
|
let time = ecs.read_resource::<Time>();
|
|
|
|
match Buttons::new(
|
|
&self.imgs,
|
|
&self.fonts,
|
|
global_state,
|
|
&self.rot_imgs,
|
|
tooltip_manager,
|
|
i18n,
|
|
)
|
|
.set(self.ids.buttons, ui_widgets)
|
|
{
|
|
Some(buttons::Event::ToggleSettings) => self.show.toggle_settings(global_state),
|
|
Some(buttons::Event::ToggleSocial) => self.show.toggle_social(),
|
|
Some(buttons::Event::ToggleMap) => self.show.toggle_map(),
|
|
Some(buttons::Event::ToggleCrafting) => self.show.toggle_crafting(),
|
|
None => {},
|
|
}
|
|
|
|
// Group Window
|
|
for event in Group::new(
|
|
&mut self.show,
|
|
client,
|
|
&global_state.settings,
|
|
&self.imgs,
|
|
&self.rot_imgs,
|
|
&self.fonts,
|
|
i18n,
|
|
self.pulse,
|
|
global_state,
|
|
tooltip_manager,
|
|
&msm,
|
|
&time,
|
|
)
|
|
.set(self.ids.group_window, ui_widgets)
|
|
{
|
|
match event {
|
|
group::Event::Accept => events.push(Event::AcceptInvite),
|
|
group::Event::Decline => events.push(Event::DeclineInvite),
|
|
group::Event::Kick(uid) => events.push(Event::KickMember(uid)),
|
|
group::Event::LeaveGroup => events.push(Event::LeaveGroup),
|
|
group::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)),
|
|
}
|
|
}
|
|
// Popup (waypoint saved and similar notifications)
|
|
Popup::new(
|
|
i18n,
|
|
client,
|
|
&self.new_notifications,
|
|
&self.fonts,
|
|
&self.show,
|
|
)
|
|
.set(self.ids.popup, ui_widgets);
|
|
|
|
// MiniMap
|
|
for event in MiniMap::new(
|
|
client,
|
|
&self.imgs,
|
|
&self.rot_imgs,
|
|
&self.world_map,
|
|
&self.fonts,
|
|
camera.get_orientation(),
|
|
global_state,
|
|
&self.show.location_markers,
|
|
&self.voxel_minimap,
|
|
)
|
|
.set(self.ids.minimap, ui_widgets)
|
|
{
|
|
match event {
|
|
minimap::Event::SettingsChange(interface_change) => {
|
|
events.push(Event::SettingsChange(interface_change.into()));
|
|
},
|
|
}
|
|
}
|
|
|
|
if let Some(prompt_dialog_settings) = &self.show.prompt_dialog {
|
|
// Prompt Dialog
|
|
match PromptDialog::new(
|
|
&self.imgs,
|
|
&self.fonts,
|
|
&global_state.i18n,
|
|
&global_state.settings,
|
|
prompt_dialog_settings,
|
|
&global_state.window.key_layout,
|
|
)
|
|
.set(self.ids.prompt_dialog, ui_widgets)
|
|
{
|
|
Some(dialog_outcome_event) => {
|
|
match dialog_outcome_event {
|
|
DialogOutcomeEvent::Affirmative(event) => events.push(event),
|
|
DialogOutcomeEvent::Negative(event) => {
|
|
if let Some(event) = event {
|
|
events.push(event);
|
|
};
|
|
},
|
|
};
|
|
|
|
// Close the prompt dialog once an option has been chosen
|
|
self.show.prompt_dialog = None;
|
|
},
|
|
None => {},
|
|
}
|
|
}
|
|
|
|
// Skillbar
|
|
// Get player stats
|
|
let ecs = client.state().ecs();
|
|
let entity = info.viewpoint_entity;
|
|
let healths = ecs.read_storage::<Health>();
|
|
let inventories = ecs.read_storage::<comp::Inventory>();
|
|
let energies = ecs.read_storage::<comp::Energy>();
|
|
let skillsets = ecs.read_storage::<comp::SkillSet>();
|
|
let active_abilities = ecs.read_storage::<comp::ActiveAbilities>();
|
|
let bodies = ecs.read_storage::<comp::Body>();
|
|
let poises = ecs.read_storage::<comp::Poise>();
|
|
let combos = ecs.read_storage::<comp::Combo>();
|
|
let time = ecs.read_resource::<Time>();
|
|
let stances = ecs.read_storage::<comp::Stance>();
|
|
// Combo floater stuffs
|
|
self.floaters.combo_floater = self.floaters.combo_floater.map(|mut f| {
|
|
f.timer -= dt.as_secs_f64();
|
|
f
|
|
});
|
|
self.floaters.combo_floater = self.floaters.combo_floater.filter(|f| f.timer > 0_f64);
|
|
|
|
if let (
|
|
Some(health),
|
|
Some(inventory),
|
|
Some(energy),
|
|
Some(poise),
|
|
Some(skillset),
|
|
Some(body),
|
|
) = (
|
|
healths.get(entity),
|
|
inventories.get(entity),
|
|
energies.get(entity),
|
|
poises.get(entity),
|
|
skillsets.get(entity),
|
|
bodies.get(entity),
|
|
) {
|
|
let context = AbilityContext::from(stances.get(entity));
|
|
match Skillbar::new(
|
|
client,
|
|
&info,
|
|
global_state,
|
|
&self.imgs,
|
|
&self.item_imgs,
|
|
&self.fonts,
|
|
&self.rot_imgs,
|
|
health,
|
|
inventory,
|
|
energy,
|
|
poise,
|
|
skillset,
|
|
active_abilities.get(entity),
|
|
body,
|
|
//&character_state,
|
|
self.pulse,
|
|
//&controller,
|
|
&self.hotbar,
|
|
tooltip_manager,
|
|
item_tooltip_manager,
|
|
&mut self.slot_manager,
|
|
i18n,
|
|
&msm,
|
|
self.floaters.combo_floater,
|
|
context,
|
|
combos.get(entity),
|
|
)
|
|
.set(self.ids.skillbar, ui_widgets)
|
|
{
|
|
Some(skillbar::Event::OpenDiary(skillgroup)) => {
|
|
self.show.diary(true);
|
|
self.show.open_skill_tree(skillgroup);
|
|
},
|
|
Some(skillbar::Event::OpenBag) => self.show.bag = !self.show.bag,
|
|
None => {},
|
|
}
|
|
}
|
|
// Bag contents
|
|
if self.show.bag {
|
|
if let (
|
|
Some(player_stats),
|
|
Some(skill_set),
|
|
Some(health),
|
|
Some(energy),
|
|
Some(body),
|
|
Some(poise),
|
|
) = (
|
|
stats.get(info.viewpoint_entity),
|
|
skill_sets.get(info.viewpoint_entity),
|
|
healths.get(entity),
|
|
energies.get(entity),
|
|
bodies.get(entity),
|
|
poises.get(entity),
|
|
) {
|
|
match Bag::new(
|
|
client,
|
|
&info,
|
|
global_state,
|
|
&self.imgs,
|
|
&self.item_imgs,
|
|
&self.fonts,
|
|
&self.rot_imgs,
|
|
tooltip_manager,
|
|
item_tooltip_manager,
|
|
&mut self.slot_manager,
|
|
self.pulse,
|
|
i18n,
|
|
player_stats,
|
|
skill_set,
|
|
health,
|
|
energy,
|
|
&self.show,
|
|
body,
|
|
&msm,
|
|
poise,
|
|
)
|
|
.set(self.ids.bag, ui_widgets)
|
|
{
|
|
Some(bag::Event::BagExpand) => self.show.bag_inv = !self.show.bag_inv,
|
|
Some(bag::Event::Close) => {
|
|
self.show.stats = false;
|
|
Self::show_bag(&mut self.slot_manager, &mut self.show, false);
|
|
if !self.show.social {
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
} else {
|
|
self.force_ungrab = true
|
|
};
|
|
},
|
|
Some(bag::Event::SortInventory) => self.events.push(Event::SortInventory),
|
|
Some(bag::Event::SwapEquippedWeapons) => {
|
|
self.events.push(Event::SwapEquippedWeapons)
|
|
},
|
|
None => {},
|
|
}
|
|
}
|
|
}
|
|
// Trade window
|
|
if self.show.trade {
|
|
if let Some(action) = Trade::new(
|
|
client,
|
|
&info,
|
|
&self.imgs,
|
|
&self.item_imgs,
|
|
&self.fonts,
|
|
&self.rot_imgs,
|
|
item_tooltip_manager,
|
|
&mut self.slot_manager,
|
|
i18n,
|
|
&msm,
|
|
self.pulse,
|
|
&mut self.show,
|
|
)
|
|
.set(self.ids.trade, ui_widgets)
|
|
{
|
|
match action {
|
|
Err(update) => match update {
|
|
trade::HudUpdate::Focus(idx) => self.to_focus = Some(Some(idx)),
|
|
trade::HudUpdate::Submit => {
|
|
let key = self.show.trade_amount_input_key.take();
|
|
key.map(|k| {
|
|
k.submit_action.map(|action| {
|
|
self.events.push(Event::TradeAction(action));
|
|
});
|
|
});
|
|
},
|
|
},
|
|
Ok(action) => {
|
|
if let TradeAction::Decline = action {
|
|
self.show.stats = false;
|
|
self.show.trade(false);
|
|
if !self.show.social {
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
} else {
|
|
self.force_ungrab = true
|
|
};
|
|
}
|
|
events.push(Event::TradeAction(action));
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Buffs
|
|
if let (Some(player_buffs), Some(health), Some(energy)) = (
|
|
buffs.get(info.viewpoint_entity),
|
|
healths.get(entity),
|
|
energies.get(entity),
|
|
) {
|
|
for event in BuffsBar::new(
|
|
&self.imgs,
|
|
&self.fonts,
|
|
&self.rot_imgs,
|
|
tooltip_manager,
|
|
i18n,
|
|
player_buffs,
|
|
stances.get(entity),
|
|
self.pulse,
|
|
global_state,
|
|
health,
|
|
energy,
|
|
&time,
|
|
)
|
|
.set(self.ids.buffs, ui_widgets)
|
|
{
|
|
match event {
|
|
buffs::Event::RemoveBuff(buff_id) => events.push(Event::RemoveBuff(buff_id)),
|
|
}
|
|
}
|
|
}
|
|
// Crafting
|
|
if self.show.crafting {
|
|
if let Some(inventory) = inventories.get(entity) {
|
|
for event in Crafting::new(
|
|
//&self.show,
|
|
client,
|
|
&info,
|
|
&self.imgs,
|
|
&self.fonts,
|
|
i18n,
|
|
self.pulse,
|
|
&self.rot_imgs,
|
|
item_tooltip_manager,
|
|
&mut self.slot_manager,
|
|
&self.item_imgs,
|
|
inventory,
|
|
&msm,
|
|
tooltip_manager,
|
|
&mut self.show,
|
|
)
|
|
.set(self.ids.crafting_window, ui_widgets)
|
|
{
|
|
match event {
|
|
crafting::Event::CraftRecipe {
|
|
recipe_name,
|
|
amount,
|
|
} => {
|
|
events.push(Event::CraftRecipe {
|
|
recipe_name,
|
|
craft_sprite: self.show.crafting_fields.craft_sprite,
|
|
amount,
|
|
});
|
|
},
|
|
crafting::Event::CraftModularWeapon {
|
|
primary_slot,
|
|
secondary_slot,
|
|
} => {
|
|
events.push(Event::CraftModularWeapon {
|
|
primary_slot,
|
|
secondary_slot,
|
|
craft_sprite: self
|
|
.show
|
|
.crafting_fields
|
|
.craft_sprite
|
|
.map(|(pos, _sprite)| pos),
|
|
});
|
|
},
|
|
crafting::Event::CraftModularWeaponComponent {
|
|
toolkind,
|
|
material,
|
|
modifier,
|
|
} => {
|
|
events.push(Event::CraftModularWeaponComponent {
|
|
toolkind,
|
|
material,
|
|
modifier,
|
|
craft_sprite: self
|
|
.show
|
|
.crafting_fields
|
|
.craft_sprite
|
|
.map(|(pos, _sprite)| pos),
|
|
});
|
|
},
|
|
crafting::Event::Close => {
|
|
self.show.stats = false;
|
|
self.show.crafting(false);
|
|
if !self.show.social {
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
} else {
|
|
self.force_ungrab = true
|
|
};
|
|
},
|
|
crafting::Event::ChangeCraftingTab(sel_cat) => {
|
|
self.show.open_crafting_tab(sel_cat, None);
|
|
},
|
|
crafting::Event::Focus(widget_id) => {
|
|
self.to_focus = Some(Some(widget_id));
|
|
},
|
|
crafting::Event::SearchRecipe(search_key) => {
|
|
self.show.search_crafting_recipe(search_key);
|
|
},
|
|
crafting::Event::ClearRecipeInputs => {
|
|
self.show.crafting_fields.recipe_inputs.clear();
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't put NPC messages in chat box.
|
|
self.new_messages
|
|
.retain(|m| !matches!(m.chat_type, comp::ChatType::Npc(_, _)));
|
|
|
|
// Chat box
|
|
if global_state.settings.interface.toggle_chat {
|
|
for event in Chat::new(
|
|
&mut self.new_messages,
|
|
client,
|
|
global_state,
|
|
self.pulse,
|
|
&self.imgs,
|
|
&self.fonts,
|
|
i18n,
|
|
)
|
|
.and_then(self.force_chat_input.take(), |c, input| c.input(input))
|
|
.and_then(self.tab_complete.take(), |c, input| {
|
|
c.prepare_tab_completion(input)
|
|
})
|
|
.and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos))
|
|
.set(self.ids.chat, ui_widgets)
|
|
{
|
|
match event {
|
|
chat::Event::TabCompletionStart(input) => {
|
|
self.tab_complete = Some(input);
|
|
},
|
|
chat::Event::SendMessage(message) => {
|
|
events.push(Event::SendMessage(message));
|
|
},
|
|
chat::Event::SendCommand(name, args) => {
|
|
events.push(Event::SendCommand(name, args));
|
|
},
|
|
chat::Event::Focus(focus_id) => {
|
|
self.to_focus = Some(Some(focus_id));
|
|
},
|
|
chat::Event::ChangeChatTab(tab) => {
|
|
events.push(Event::SettingsChange(ChatChange::ChangeChatTab(tab).into()));
|
|
},
|
|
chat::Event::ShowChatTabSettings(tab) => {
|
|
self.show.chat_tab_settings_index = Some(tab);
|
|
self.show.settings_tab = SettingsTab::Chat;
|
|
self.show.settings(true);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
self.new_messages = VecDeque::new();
|
|
self.new_notifications = VecDeque::new();
|
|
|
|
//Loot
|
|
LootScroller::new(
|
|
&mut self.new_loot_messages,
|
|
client,
|
|
&info,
|
|
&self.show,
|
|
&self.imgs,
|
|
&self.item_imgs,
|
|
&self.rot_imgs,
|
|
&self.fonts,
|
|
i18n,
|
|
&msm,
|
|
item_tooltip_manager,
|
|
self.pulse,
|
|
)
|
|
.set(self.ids.loot_scroller, ui_widgets);
|
|
|
|
self.new_loot_messages = VecDeque::new();
|
|
|
|
// Windows
|
|
|
|
// Char Window will always appear at the left side. Other Windows default to the
|
|
// left side, but when the Char Window is opened they will appear to the right
|
|
// of it.
|
|
|
|
// Settings
|
|
if let Windows::Settings = self.show.open_windows {
|
|
for event in SettingsWindow::new(
|
|
global_state,
|
|
&self.show,
|
|
&self.imgs,
|
|
&self.fonts,
|
|
i18n,
|
|
client.server_view_distance_limit(),
|
|
fps as f32,
|
|
)
|
|
.set(self.ids.settings_window, ui_widgets)
|
|
{
|
|
match event {
|
|
settings_window::Event::ChangeTab(tab) => self.show.open_setting_tab(tab),
|
|
settings_window::Event::Close => {
|
|
// Unpause the game if we are on singleplayer so that we can logout
|
|
#[cfg(feature = "singleplayer")]
|
|
global_state.unpause();
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
|
|
self.show.settings(false)
|
|
},
|
|
settings_window::Event::ChangeChatSettingsTab(tab) => {
|
|
self.show.chat_tab_settings_index = tab;
|
|
},
|
|
settings_window::Event::SettingsChange(settings_change) => {
|
|
match &settings_change {
|
|
SettingsChange::Interface(interface_change) => match interface_change {
|
|
InterfaceChange::ToggleHelp(toggle_help) => {
|
|
self.show.help = *toggle_help;
|
|
},
|
|
InterfaceChange::ResetInterfaceSettings => {
|
|
self.show.help = false;
|
|
},
|
|
_ => {},
|
|
},
|
|
_ => {},
|
|
}
|
|
events.push(Event::SettingsChange(settings_change));
|
|
},
|
|
}
|
|
}
|
|
}
|
|
// Quest Window
|
|
let stats = client.state().ecs().read_storage::<comp::Stats>();
|
|
if self.show.quest {
|
|
if let Some(stats) = stats.get(entity) {
|
|
match Quest::new(
|
|
&self.show,
|
|
client,
|
|
&self.imgs,
|
|
&self.fonts,
|
|
i18n,
|
|
&self.rot_imgs,
|
|
tooltip_manager,
|
|
stats,
|
|
&self.item_imgs,
|
|
self.pulse,
|
|
)
|
|
.set(self.ids.quest_window, ui_widgets)
|
|
{
|
|
Some(quest::Event::Close) => {
|
|
self.show.quest(false);
|
|
if !self.show.bag {
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
} else {
|
|
self.force_ungrab = true
|
|
};
|
|
},
|
|
None => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Social Window
|
|
if self.show.social {
|
|
let ecs = client.state().ecs();
|
|
let _stats = ecs.read_storage::<comp::Stats>();
|
|
for event in Social::new(
|
|
&self.show,
|
|
client,
|
|
&self.imgs,
|
|
&self.fonts,
|
|
i18n,
|
|
info.selected_entity,
|
|
&self.rot_imgs,
|
|
tooltip_manager,
|
|
)
|
|
.set(self.ids.social_window, ui_widgets)
|
|
{
|
|
match event {
|
|
social::Event::Close => {
|
|
self.show.social(false);
|
|
if !self.show.bag {
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
} else {
|
|
self.force_ungrab = true
|
|
};
|
|
},
|
|
social::Event::Focus(widget_id) => {
|
|
self.to_focus = Some(Some(widget_id));
|
|
},
|
|
social::Event::Invite(uid) => events.push(Event::InviteMember(uid)),
|
|
social::Event::SearchPlayers(search_key) => {
|
|
self.show.search_social_players(search_key)
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Diary
|
|
if self.show.diary {
|
|
let entity = info.viewpoint_entity;
|
|
let skill_sets = ecs.read_storage::<comp::SkillSet>();
|
|
if let (
|
|
Some(skill_set),
|
|
Some(inventory),
|
|
Some(health),
|
|
Some(energy),
|
|
Some(body),
|
|
Some(poise),
|
|
) = (
|
|
skill_sets.get(entity),
|
|
inventories.get(entity),
|
|
healths.get(entity),
|
|
energies.get(entity),
|
|
bodies.get(entity),
|
|
poises.get(entity),
|
|
) {
|
|
let context = AbilityContext::from(stances.get(entity));
|
|
for event in Diary::new(
|
|
&self.show,
|
|
client,
|
|
global_state,
|
|
skill_set,
|
|
active_abilities.get(entity).unwrap_or(&Default::default()),
|
|
inventory,
|
|
health,
|
|
energy,
|
|
poise,
|
|
body,
|
|
&msm,
|
|
&self.imgs,
|
|
&self.item_imgs,
|
|
&self.fonts,
|
|
i18n,
|
|
&self.rot_imgs,
|
|
tooltip_manager,
|
|
&mut self.slot_manager,
|
|
self.pulse,
|
|
context,
|
|
)
|
|
.set(self.ids.diary, ui_widgets)
|
|
{
|
|
match event {
|
|
diary::Event::Close => {
|
|
self.show.diary(false);
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
},
|
|
diary::Event::ChangeSkillTree(tree_sel) => {
|
|
self.show.open_skill_tree(tree_sel)
|
|
},
|
|
diary::Event::UnlockSkill(skill) => events.push(Event::UnlockSkill(skill)),
|
|
diary::Event::ChangeSection(section) => {
|
|
self.show.diary_fields.section = section;
|
|
},
|
|
diary::Event::SelectExpBar(xp_bar) => {
|
|
events.push(Event::SelectExpBar(xp_bar))
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Map
|
|
if self.show.map {
|
|
for event in Map::new(
|
|
client,
|
|
&self.imgs,
|
|
&self.rot_imgs,
|
|
&self.world_map,
|
|
&self.fonts,
|
|
self.pulse,
|
|
i18n,
|
|
global_state,
|
|
tooltip_manager,
|
|
&self.show.location_markers,
|
|
self.map_drag,
|
|
)
|
|
.set(self.ids.map, ui_widgets)
|
|
{
|
|
match event {
|
|
map::Event::Close => {
|
|
self.show.map(false);
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
},
|
|
map::Event::SettingsChange(settings_change) => {
|
|
events.push(Event::SettingsChange(settings_change.into()));
|
|
},
|
|
map::Event::RequestSiteInfo(id) => {
|
|
events.push(Event::RequestSiteInfo(id));
|
|
},
|
|
map::Event::SetLocationMarker(pos) => {
|
|
events.push(Event::MapMarkerEvent(MapMarkerChange::Update(pos)));
|
|
self.show.location_markers.owned = Some(pos);
|
|
},
|
|
map::Event::MapDrag(new_drag) => {
|
|
self.map_drag = new_drag;
|
|
},
|
|
map::Event::RemoveMarker => {
|
|
self.show.location_markers.owned = None;
|
|
events.push(Event::MapMarkerEvent(MapMarkerChange::Remove));
|
|
},
|
|
}
|
|
}
|
|
} else {
|
|
// Reset the map position when it's not showing
|
|
self.map_drag = Vec2::zero();
|
|
}
|
|
|
|
if self.show.esc_menu {
|
|
match EscMenu::new(&self.imgs, &self.fonts, i18n).set(self.ids.esc_menu, ui_widgets) {
|
|
Some(esc_menu::Event::OpenSettings(tab)) => {
|
|
self.show.open_setting_tab(tab);
|
|
},
|
|
Some(esc_menu::Event::Close) => {
|
|
self.show.esc_menu = false;
|
|
self.show.want_grab = true;
|
|
self.force_ungrab = false;
|
|
|
|
// Unpause the game if we are on singleplayer
|
|
#[cfg(feature = "singleplayer")]
|
|
global_state.unpause();
|
|
},
|
|
Some(esc_menu::Event::Logout) => {
|
|
// Unpause the game if we are on singleplayer so that we can logout
|
|
#[cfg(feature = "singleplayer")]
|
|
global_state.unpause();
|
|
|
|
events.push(Event::Logout);
|
|
},
|
|
Some(esc_menu::Event::Quit) => events.push(Event::Quit),
|
|
Some(esc_menu::Event::CharacterSelection) => {
|
|
// Unpause the game if we are on singleplayer so that we can logout
|
|
#[cfg(feature = "singleplayer")]
|
|
global_state.unpause();
|
|
|
|
events.push(Event::CharacterSelection)
|
|
},
|
|
None => {},
|
|
}
|
|
}
|
|
|
|
let mut indicator_offset = 40.0;
|
|
|
|
// Free look indicator
|
|
if let Some(freelook_key) = global_state
|
|
.settings
|
|
.controls
|
|
.get_binding(GameInput::FreeLook)
|
|
{
|
|
if self.show.free_look {
|
|
let msg = i18n.get_msg_ctx("hud-free_look_indicator", &i18n::fluent_args! {
|
|
"key" => freelook_key.display_string(key_layout),
|
|
});
|
|
Text::new(&msg)
|
|
.color(TEXT_BG)
|
|
.mid_top_with_margin_on(ui_widgets.window, indicator_offset)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(20))
|
|
.set(self.ids.free_look_bg, ui_widgets);
|
|
indicator_offset += 30.0;
|
|
Text::new(&msg)
|
|
.color(KILL_COLOR)
|
|
.top_left_with_margins_on(self.ids.free_look_bg, -1.0, -1.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(20))
|
|
.set(self.ids.free_look_txt, ui_widgets);
|
|
}
|
|
};
|
|
|
|
// Auto walk indicator
|
|
if self.show.auto_walk {
|
|
Text::new(&i18n.get_msg("hud-auto_walk_indicator"))
|
|
.color(TEXT_BG)
|
|
.mid_top_with_margin_on(ui_widgets.window, indicator_offset)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(20))
|
|
.set(self.ids.auto_walk_bg, ui_widgets);
|
|
indicator_offset += 30.0;
|
|
Text::new(&i18n.get_msg("hud-auto_walk_indicator"))
|
|
.color(KILL_COLOR)
|
|
.top_left_with_margins_on(self.ids.auto_walk_bg, -1.0, -1.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(20))
|
|
.set(self.ids.auto_walk_txt, ui_widgets);
|
|
}
|
|
|
|
// Camera zoom lock
|
|
self.show.zoom_lock.update(dt);
|
|
|
|
if let Some(zoom_lock) = self.show.zoom_lock.reason {
|
|
let zoom_lock_message = match zoom_lock {
|
|
NotificationReason::Remind => "hud-zoom_lock_indicator-remind",
|
|
NotificationReason::Enable => "hud-zoom_lock_indicator-enable",
|
|
NotificationReason::Disable => "hud-zoom_lock_indicator-disable",
|
|
};
|
|
|
|
Text::new(&i18n.get_msg(zoom_lock_message))
|
|
.color(TEXT_BG.alpha(self.show.zoom_lock.alpha))
|
|
.mid_top_with_margin_on(ui_widgets.window, indicator_offset)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(20))
|
|
.set(self.ids.zoom_lock_bg, ui_widgets);
|
|
indicator_offset += 30.0;
|
|
Text::new(&i18n.get_msg(zoom_lock_message))
|
|
.color(TEXT_COLOR.alpha(self.show.zoom_lock.alpha))
|
|
.top_left_with_margins_on(self.ids.zoom_lock_bg, -1.0, -1.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(20))
|
|
.set(self.ids.zoom_lock_txt, ui_widgets);
|
|
}
|
|
|
|
// Camera clamp indicator
|
|
if let Some(cameraclamp_key) = global_state
|
|
.settings
|
|
.controls
|
|
.get_binding(GameInput::CameraClamp)
|
|
{
|
|
if self.show.camera_clamp {
|
|
let msg = i18n.get_msg_ctx("hud-camera_clamp_indicator", &i18n::fluent_args! {
|
|
"key" => cameraclamp_key.display_string(key_layout),
|
|
});
|
|
Text::new(&msg)
|
|
.color(TEXT_BG)
|
|
.mid_top_with_margin_on(ui_widgets.window, indicator_offset)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(20))
|
|
.set(self.ids.camera_clamp_bg, ui_widgets);
|
|
Text::new(&msg)
|
|
.color(KILL_COLOR)
|
|
.top_left_with_margins_on(self.ids.camera_clamp_bg, -1.0, -1.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(20))
|
|
.set(self.ids.camera_clamp_txt, ui_widgets);
|
|
}
|
|
}
|
|
|
|
// Maintain slot manager
|
|
'slot_events: for event in self.slot_manager.maintain(ui_widgets) {
|
|
use comp::slot::Slot;
|
|
use slots::{AbilitySlot, InventorySlot, SlotKind::*};
|
|
let to_slot = |slot_kind| match slot_kind {
|
|
Inventory(InventorySlot {
|
|
slot, ours: true, ..
|
|
}) => Some(Slot::Inventory(slot)),
|
|
Inventory(InventorySlot { ours: false, .. }) => None,
|
|
Equip(e) => Some(Slot::Equip(e)),
|
|
Hotbar(_) => None,
|
|
Trade(_) => None,
|
|
Ability(_) => None,
|
|
Crafting(_) => None,
|
|
};
|
|
match event {
|
|
slot::Event::Dragged(a, b) => {
|
|
// Swap between slots
|
|
if let (Some(a), Some(b)) = (to_slot(a), to_slot(b)) {
|
|
events.push(Event::SwapSlots {
|
|
slot_a: a,
|
|
slot_b: b,
|
|
bypass_dialog: false,
|
|
});
|
|
} else if let (
|
|
Inventory(InventorySlot {
|
|
slot, ours: true, ..
|
|
}),
|
|
Hotbar(h),
|
|
) = (a, b)
|
|
{
|
|
if let Some(item) = inventories
|
|
.get(info.viewpoint_entity)
|
|
.and_then(|inv| inv.get(slot))
|
|
{
|
|
self.hotbar.add_inventory_link(h, item);
|
|
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
|
|
}
|
|
} else if let (Hotbar(a), Hotbar(b)) = (a, b) {
|
|
self.hotbar.swap(a, b);
|
|
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
|
|
} else if let (Inventory(i), Trade(t)) = (a, b) {
|
|
if i.ours == t.ours {
|
|
if let Some(inventory) = inventories.get(t.entity) {
|
|
events.push(Event::TradeAction(TradeAction::AddItem {
|
|
item: i.slot,
|
|
quantity: i.amount(inventory).unwrap_or(1),
|
|
ours: i.ours,
|
|
}));
|
|
}
|
|
}
|
|
} else if let (Trade(t), Inventory(i)) = (a, b) {
|
|
if i.ours == t.ours {
|
|
if let Some(inventory) = inventories.get(t.entity) {
|
|
if let Some(invslot) = t.invslot {
|
|
events.push(Event::TradeAction(TradeAction::RemoveItem {
|
|
item: invslot,
|
|
quantity: t.amount(inventory).unwrap_or(1),
|
|
ours: t.ours,
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
} else if let (Ability(a), Ability(b)) = (a, b) {
|
|
match (a, b) {
|
|
(AbilitySlot::Ability(ability), AbilitySlot::Slot(index)) => {
|
|
events.push(Event::ChangeAbility(index, ability));
|
|
},
|
|
(AbilitySlot::Slot(a), AbilitySlot::Slot(b)) => {
|
|
let me = info.viewpoint_entity;
|
|
if let Some(active_abilities) = active_abilities.get(me) {
|
|
let ability_a = active_abilities
|
|
.auxiliary_set(inventories.get(me), skill_sets.get(me))
|
|
.get(a)
|
|
.copied()
|
|
.unwrap_or(AuxiliaryAbility::Empty);
|
|
let ability_b = active_abilities
|
|
.auxiliary_set(inventories.get(me), skill_sets.get(me))
|
|
.get(b)
|
|
.copied()
|
|
.unwrap_or(AuxiliaryAbility::Empty);
|
|
events.push(Event::ChangeAbility(a, ability_b));
|
|
events.push(Event::ChangeAbility(b, ability_a));
|
|
}
|
|
},
|
|
(AbilitySlot::Slot(index), _) => {
|
|
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
|
|
},
|
|
(AbilitySlot::Ability(_), AbilitySlot::Ability(_)) => {},
|
|
}
|
|
} else if let (Inventory(i), Crafting(c)) = (a, b) {
|
|
// Add item to crafting input
|
|
if inventories
|
|
.get(info.viewpoint_entity)
|
|
.and_then(|inv| inv.get(i.slot))
|
|
.map_or(false, |item| {
|
|
(c.requirement)(item, client.component_recipe_book(), c.info)
|
|
})
|
|
{
|
|
self.show
|
|
.crafting_fields
|
|
.recipe_inputs
|
|
.insert(c.index, i.slot);
|
|
}
|
|
} else if let (Crafting(c), Inventory(_)) = (a, b) {
|
|
// Remove item from crafting input
|
|
self.show.crafting_fields.recipe_inputs.remove(&c.index);
|
|
} else if let (Ability(AbilitySlot::Ability(ability)), Hotbar(slot)) = (a, b) {
|
|
if let Some(Some(HotbarSlotContents::Ability(index))) =
|
|
self.hotbar.slots.get(slot as usize)
|
|
{
|
|
events.push(Event::ChangeAbility(*index, ability));
|
|
}
|
|
}
|
|
},
|
|
slot::Event::Dropped(from) => {
|
|
// Drop item
|
|
if let Some(from) = to_slot(from) {
|
|
events.push(Event::DropSlot(from));
|
|
} else if let Hotbar(h) = from {
|
|
self.hotbar.clear_slot(h);
|
|
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
|
|
} else if let Trade(t) = from {
|
|
if let Some(inventory) = inventories.get(t.entity) {
|
|
if let Some(invslot) = t.invslot {
|
|
events.push(Event::TradeAction(TradeAction::RemoveItem {
|
|
item: invslot,
|
|
quantity: t.amount(inventory).unwrap_or(1),
|
|
ours: t.ours,
|
|
}));
|
|
}
|
|
}
|
|
} else if let Ability(AbilitySlot::Slot(index)) = from {
|
|
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
|
|
} else if let Crafting(c) = from {
|
|
// Remove item from crafting input
|
|
self.show.crafting_fields.recipe_inputs.remove(&c.index);
|
|
}
|
|
},
|
|
slot::Event::SplitDropped(from) => {
|
|
// Drop item
|
|
if let Some(from) = to_slot(from) {
|
|
events.push(Event::SplitDropSlot(from));
|
|
} else if let Hotbar(h) = from {
|
|
self.hotbar.clear_slot(h);
|
|
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
|
|
} else if let Ability(AbilitySlot::Slot(index)) = from {
|
|
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
|
|
}
|
|
},
|
|
slot::Event::SplitDragged(a, b) => {
|
|
// Swap between slots
|
|
if let (Some(a), Some(b)) = (to_slot(a), to_slot(b)) {
|
|
events.push(Event::SplitSwapSlots {
|
|
slot_a: a,
|
|
slot_b: b,
|
|
bypass_dialog: false,
|
|
});
|
|
} else if let (Inventory(i), Hotbar(h)) = (a, b) {
|
|
if let Some(item) = inventories
|
|
.get(info.viewpoint_entity)
|
|
.and_then(|inv| inv.get(i.slot))
|
|
{
|
|
self.hotbar.add_inventory_link(h, item);
|
|
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
|
|
}
|
|
} else if let (Hotbar(a), Hotbar(b)) = (a, b) {
|
|
self.hotbar.swap(a, b);
|
|
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
|
|
} else if let (Inventory(i), Trade(t)) = (a, b) {
|
|
if i.ours == t.ours {
|
|
if let Some(inventory) = inventories.get(t.entity) {
|
|
events.push(Event::TradeAction(TradeAction::AddItem {
|
|
item: i.slot,
|
|
quantity: i.amount(inventory).unwrap_or(1) / 2,
|
|
ours: i.ours,
|
|
}));
|
|
}
|
|
}
|
|
} else if let (Trade(t), Inventory(i)) = (a, b) {
|
|
if i.ours == t.ours {
|
|
if let Some(inventory) = inventories.get(t.entity) {
|
|
if let Some(invslot) = t.invslot {
|
|
events.push(Event::TradeAction(TradeAction::RemoveItem {
|
|
item: invslot,
|
|
quantity: t.amount(inventory).unwrap_or(1) / 2,
|
|
ours: t.ours,
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
} else if let (Ability(a), Ability(b)) = (a, b) {
|
|
match (a, b) {
|
|
(AbilitySlot::Ability(ability), AbilitySlot::Slot(index)) => {
|
|
events.push(Event::ChangeAbility(index, ability));
|
|
},
|
|
(AbilitySlot::Slot(a), AbilitySlot::Slot(b)) => {
|
|
let me = info.viewpoint_entity;
|
|
if let Some(active_abilities) = active_abilities.get(me) {
|
|
let ability_a = active_abilities
|
|
.auxiliary_set(inventories.get(me), skill_sets.get(me))
|
|
.get(a)
|
|
.copied()
|
|
.unwrap_or(AuxiliaryAbility::Empty);
|
|
let ability_b = active_abilities
|
|
.auxiliary_set(inventories.get(me), skill_sets.get(me))
|
|
.get(b)
|
|
.copied()
|
|
.unwrap_or(AuxiliaryAbility::Empty);
|
|
events.push(Event::ChangeAbility(a, ability_b));
|
|
events.push(Event::ChangeAbility(b, ability_a));
|
|
}
|
|
},
|
|
(AbilitySlot::Slot(index), _) => {
|
|
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
|
|
},
|
|
(AbilitySlot::Ability(_), AbilitySlot::Ability(_)) => {},
|
|
}
|
|
}
|
|
},
|
|
slot::Event::Used(from) => {
|
|
// Item used (selected and then clicked again)
|
|
if let Some(from) = to_slot(from) {
|
|
if self.show.crafting_fields.salvage
|
|
&& matches!(
|
|
self.show.crafting_fields.crafting_tab,
|
|
CraftingTab::Dismantle
|
|
)
|
|
{
|
|
if let (Slot::Inventory(slot), Some((salvage_pos, _sprite_kind))) =
|
|
(from, self.show.crafting_fields.craft_sprite)
|
|
{
|
|
events.push(Event::SalvageItem { slot, salvage_pos })
|
|
}
|
|
} else {
|
|
events.push(Event::UseSlot {
|
|
slot: from,
|
|
bypass_dialog: false,
|
|
});
|
|
}
|
|
} else if let Hotbar(h) = from {
|
|
// Used from hotbar
|
|
self.hotbar.get(h).map(|s| match s {
|
|
hotbar::SlotContents::Inventory(i, _) => {
|
|
if let Some(inv) = inventories.get(info.viewpoint_entity) {
|
|
// If the item in the inactive main hand is the same as the item
|
|
// pressed in the hotbar, then swap active and inactive hands
|
|
// instead of looking for
|
|
// the item in the inventory
|
|
if inv
|
|
.equipped(comp::slot::EquipSlot::InactiveMainhand)
|
|
.map_or(false, |item| item.item_hash() == i)
|
|
{
|
|
events.push(Event::SwapEquippedWeapons);
|
|
} else if let Some(slot) = inv.get_slot_from_hash(i) {
|
|
events.push(Event::UseSlot {
|
|
slot: Slot::Inventory(slot),
|
|
bypass_dialog: false,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
hotbar::SlotContents::Ability(_) => {},
|
|
});
|
|
} else if let Ability(AbilitySlot::Slot(index)) = from {
|
|
events.push(Event::ChangeAbility(index, AuxiliaryAbility::Empty));
|
|
} else if let Crafting(c) = from {
|
|
// Remove item from crafting input
|
|
self.show.crafting_fields.recipe_inputs.remove(&c.index);
|
|
}
|
|
},
|
|
slot::Event::Request {
|
|
slot,
|
|
auto_quantity,
|
|
} => {
|
|
if let Some((_, trade, prices)) = client.pending_trade() {
|
|
let ecs = client.state().ecs();
|
|
let inventories = ecs.read_component::<comp::Inventory>();
|
|
let get_inventory = |uid: Uid| {
|
|
if let Some(entity) = ecs.entity_from_uid(uid.0) {
|
|
inventories.get(entity)
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
let mut r_inventories = [None, None];
|
|
for (i, party) in trade.parties.iter().enumerate() {
|
|
match get_inventory(*party) {
|
|
Some(inventory) => {
|
|
r_inventories[i] = Some(ReducedInventory::from(inventory))
|
|
},
|
|
None => continue 'slot_events,
|
|
};
|
|
}
|
|
let who = match ecs
|
|
.uid_from_entity(info.viewpoint_entity)
|
|
.and_then(|uid| trade.which_party(uid))
|
|
{
|
|
Some(who) => who,
|
|
None => continue 'slot_events,
|
|
};
|
|
let do_auto_quantity =
|
|
|inventory: &comp::Inventory,
|
|
slot,
|
|
ours,
|
|
remove,
|
|
quantity: &mut u32| {
|
|
if let Some(prices) = prices {
|
|
let balance0 =
|
|
prices.balance(&trade.offers, &r_inventories, who, true);
|
|
let balance1 = prices.balance(
|
|
&trade.offers,
|
|
&r_inventories,
|
|
1 - who,
|
|
false,
|
|
);
|
|
if let Some(item) = inventory.get(slot) {
|
|
if let Some(materials) =
|
|
TradePricing::get_materials(&item.item_definition_id())
|
|
{
|
|
let unit_price: f32 = materials
|
|
.iter()
|
|
.map(|e| {
|
|
prices
|
|
.values
|
|
.get(&e.1)
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
* e.0
|
|
* (if ours {
|
|
e.1.trade_margin()
|
|
} else {
|
|
1.0
|
|
})
|
|
})
|
|
.sum();
|
|
|
|
let mut float_delta = if ours ^ remove {
|
|
(balance1 - balance0) / unit_price
|
|
} else {
|
|
(balance0 - balance1) / unit_price
|
|
};
|
|
if ours ^ remove {
|
|
float_delta = float_delta.ceil();
|
|
} else {
|
|
float_delta = float_delta.floor();
|
|
}
|
|
*quantity = float_delta.max(0.0) as u32;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
match slot {
|
|
Inventory(i) => {
|
|
if let Some(inventory) = inventories.get(i.entity) {
|
|
let mut quantity = 1;
|
|
if auto_quantity {
|
|
do_auto_quantity(
|
|
inventory,
|
|
i.slot,
|
|
i.ours,
|
|
false,
|
|
&mut quantity,
|
|
);
|
|
let inv_quantity = i.amount(inventory).unwrap_or(1);
|
|
quantity = quantity.min(inv_quantity);
|
|
}
|
|
|
|
events.push(Event::TradeAction(TradeAction::AddItem {
|
|
item: i.slot,
|
|
quantity,
|
|
ours: i.ours,
|
|
}));
|
|
}
|
|
},
|
|
Trade(t) => {
|
|
if let Some(inventory) = inventories.get(t.entity) {
|
|
if let Some(invslot) = t.invslot {
|
|
let mut quantity = 1;
|
|
if auto_quantity {
|
|
do_auto_quantity(
|
|
inventory,
|
|
invslot,
|
|
t.ours,
|
|
true,
|
|
&mut quantity,
|
|
);
|
|
let inv_quantity = t.amount(inventory).unwrap_or(1);
|
|
quantity = quantity.min(inv_quantity);
|
|
}
|
|
events.push(Event::TradeAction(TradeAction::RemoveItem {
|
|
item: invslot,
|
|
quantity,
|
|
ours: t.ours,
|
|
}));
|
|
}
|
|
}
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
self.hotbar.maintain_abilities(client, &info);
|
|
|
|
// Temporary Example Quest
|
|
let arrow_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer
|
|
let show_intro = self.show.intro; // borrow check doesn't understand closures
|
|
if let Some(toggle_cursor_key) = global_state
|
|
.settings
|
|
.controls
|
|
.get_binding(GameInput::ToggleCursor)
|
|
.filter(|_| !show_intro)
|
|
{
|
|
prof_span!("temporary example quest");
|
|
match global_state.settings.interface.intro_show {
|
|
Intro::Show => {
|
|
if Button::image(self.imgs.button)
|
|
.w_h(200.0, 60.0)
|
|
.hover_image(self.imgs.button_hover)
|
|
.press_image(self.imgs.button_press)
|
|
.bottom_left_with_margins_on(ui_widgets.window, 350.0, 150.0)
|
|
.label(&i18n.get_msg("hud-tutorial_btn"))
|
|
.label_font_id(self.fonts.cyri.conrod_id)
|
|
.label_font_size(self.fonts.cyri.scale(18))
|
|
.label_color(TEXT_COLOR)
|
|
.label_y(conrod_core::position::Relative::Scalar(2.0))
|
|
.image_color(ENEMY_HP_COLOR)
|
|
.set(self.ids.intro_button, ui_widgets)
|
|
.was_clicked()
|
|
{
|
|
self.show.intro = true;
|
|
self.show.want_grab = true;
|
|
}
|
|
let tutorial_click_msg =
|
|
i18n.get_msg_ctx("hud-tutorial_click_here", &i18n::fluent_args! {
|
|
"key" => toggle_cursor_key.display_string(key_layout),
|
|
});
|
|
Image::new(self.imgs.sp_indicator_arrow)
|
|
.w_h(20.0, 11.0)
|
|
.mid_top_with_margin_on(self.ids.intro_button, -20.0 + arrow_ani as f64)
|
|
.color(Some(QUALITY_LEGENDARY))
|
|
.set(self.ids.tut_arrow, ui_widgets);
|
|
Text::new(&tutorial_click_msg)
|
|
.mid_top_with_margin_on(self.ids.tut_arrow, -40.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.center_justify()
|
|
.color(BLACK)
|
|
.set(self.ids.tut_arrow_txt_bg, ui_widgets);
|
|
Text::new(&tutorial_click_msg)
|
|
.bottom_right_with_margins_on(self.ids.tut_arrow_txt_bg, 1.0, 1.0)
|
|
.center_justify()
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(14))
|
|
.color(QUALITY_LEGENDARY)
|
|
.set(self.ids.tut_arrow_txt, ui_widgets);
|
|
},
|
|
Intro::Never => {
|
|
self.show.intro = false;
|
|
},
|
|
}
|
|
}
|
|
// TODO: Add event/stat based tutorial system
|
|
if self.show.intro && !self.show.esc_menu {
|
|
prof_span!("intro show");
|
|
match global_state.settings.interface.intro_show {
|
|
Intro::Show => {
|
|
if self.show.intro {
|
|
self.show.want_grab = false;
|
|
let quest_headline = i18n.get_msg("hud-temp_quest_headline");
|
|
let quest_text = i18n.get_msg("hud-temp_quest_text");
|
|
Image::new(self.imgs.quest_bg0)
|
|
.w_h(404.0, 858.0)
|
|
.middle_of(ui_widgets.window)
|
|
.set(self.ids.quest_bg, ui_widgets);
|
|
|
|
Text::new(&quest_headline)
|
|
.mid_top_with_margin_on(self.ids.quest_bg, 310.0)
|
|
.font_size(self.fonts.cyri.scale(30))
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(TEXT_BG)
|
|
.set(self.ids.q_headline_bg, ui_widgets);
|
|
Text::new(&quest_headline)
|
|
.bottom_left_with_margins_on(self.ids.q_headline_bg, 1.0, 1.0)
|
|
.font_size(self.fonts.cyri.scale(30))
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(TEXT_COLOR)
|
|
.set(self.ids.q_headline, ui_widgets);
|
|
|
|
Text::new(&quest_text)
|
|
.mid_top_with_margin_on(self.ids.quest_bg, 360.0)
|
|
.w(350.0)
|
|
.font_size(self.fonts.cyri.scale(17))
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(TEXT_BG)
|
|
.set(self.ids.q_text_bg, ui_widgets);
|
|
Text::new(&quest_text)
|
|
.bottom_left_with_margins_on(self.ids.q_text_bg, 1.0, 1.0)
|
|
.w(350.0)
|
|
.font_size(self.fonts.cyri.scale(17))
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.color(TEXT_COLOR)
|
|
.set(self.ids.q_text, ui_widgets);
|
|
|
|
if Button::image(self.imgs.button)
|
|
.w_h(212.0, 52.0)
|
|
.hover_image(self.imgs.button_hover)
|
|
.press_image(self.imgs.button_press)
|
|
.mid_bottom_with_margin_on(self.ids.q_text_bg, -80.0)
|
|
.label(&i18n.get_msg("common-close"))
|
|
.label_font_id(self.fonts.cyri.conrod_id)
|
|
.label_font_size(self.fonts.cyri.scale(22))
|
|
.label_color(TEXT_COLOR)
|
|
.label_y(conrod_core::position::Relative::Scalar(2.0))
|
|
.set(self.ids.accept_button, ui_widgets)
|
|
.was_clicked()
|
|
{
|
|
self.show.intro = false;
|
|
events.push(Event::SettingsChange(
|
|
InterfaceChange::Intro(Intro::Never).into(),
|
|
));
|
|
self.show.want_grab = true;
|
|
}
|
|
if !self.show.crafting && !self.show.bag {
|
|
Image::new(self.imgs.sp_indicator_arrow)
|
|
.w_h(20.0, 11.0)
|
|
.bottom_right_with_margins_on(
|
|
ui_widgets.window,
|
|
40.0 + arrow_ani as f64,
|
|
205.0,
|
|
)
|
|
.color(Some(QUALITY_LEGENDARY))
|
|
.set(self.ids.tut_arrow, ui_widgets);
|
|
Text::new(&i18n.get_msg("hud-tutorial_elements"))
|
|
.mid_top_with_margin_on(self.ids.tut_arrow, -50.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(40))
|
|
.color(BLACK)
|
|
.floating(true)
|
|
.set(self.ids.tut_arrow_txt_bg, ui_widgets);
|
|
Text::new(&i18n.get_msg("hud-tutorial_elements"))
|
|
.bottom_right_with_margins_on(self.ids.tut_arrow_txt_bg, 1.0, 1.0)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(40))
|
|
.color(QUALITY_LEGENDARY)
|
|
.floating(true)
|
|
.set(self.ids.tut_arrow_txt, ui_widgets);
|
|
}
|
|
}
|
|
},
|
|
Intro::Never => {
|
|
self.show.intro = false;
|
|
},
|
|
}
|
|
}
|
|
|
|
events
|
|
}
|
|
|
|
fn show_bag(slot_manager: &mut slots::SlotManager, show: &mut Show, state: bool) {
|
|
show.bag(state);
|
|
if !state {
|
|
slot_manager.idle();
|
|
}
|
|
}
|
|
|
|
pub fn add_failed_block_pickup(&mut self, pos: Vec3<i32>, reason: HudCollectFailedReason) {
|
|
self.failed_block_pickups
|
|
.insert(pos, CollectFailedData::new(self.pulse, reason));
|
|
}
|
|
|
|
pub fn add_failed_entity_pickup(&mut self, entity: EcsEntity, reason: HudCollectFailedReason) {
|
|
self.failed_entity_pickups
|
|
.insert(entity, CollectFailedData::new(self.pulse, reason));
|
|
}
|
|
|
|
pub fn new_loot_message(&mut self, item: LootMessage) {
|
|
self.new_loot_messages.push_back(item);
|
|
}
|
|
|
|
pub fn new_message(&mut self, msg: comp::ChatMsg) { self.new_messages.push_back(msg); }
|
|
|
|
pub fn new_notification(&mut self, msg: Notification) { self.new_notifications.push_back(msg); }
|
|
|
|
pub fn set_scaling_mode(&mut self, scale_mode: ScaleMode) {
|
|
self.ui.set_scaling_mode(scale_mode);
|
|
}
|
|
|
|
pub fn scale_change(&mut self, scale_change: ScaleChange) -> ScaleMode {
|
|
let scale_mode = match scale_change {
|
|
ScaleChange::Adjust(scale) => ScaleMode::Absolute(scale),
|
|
ScaleChange::ToAbsolute => self.ui.scale().scaling_mode_as_absolute(),
|
|
ScaleChange::ToRelative => self.ui.scale().scaling_mode_as_relative(),
|
|
};
|
|
self.ui.set_scaling_mode(scale_mode);
|
|
scale_mode
|
|
}
|
|
|
|
/// Checks if a TextEdit widget has the keyboard captured.
|
|
fn typing(&self) -> bool { Hud::is_captured::<widget::TextEdit>(&self.ui.ui) }
|
|
|
|
/// Checks if a widget of type `W` has captured the keyboard
|
|
fn is_captured<W: Widget>(ui: &conrod_core::Ui) -> bool {
|
|
if let Some(id) = ui.global_input().current.widget_capturing_keyboard {
|
|
ui.widget_graph()
|
|
.widget(id)
|
|
.filter(|c| c.type_id == std::any::TypeId::of::<<W as Widget>::State>())
|
|
.is_some()
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn handle_event(
|
|
&mut self,
|
|
event: WinEvent,
|
|
global_state: &mut GlobalState,
|
|
client_inventory: Option<&comp::Inventory>,
|
|
) -> bool {
|
|
// Helper
|
|
fn handle_slot(
|
|
slot: hotbar::Slot,
|
|
state: bool,
|
|
events: &mut Vec<Event>,
|
|
slot_manager: &mut slots::SlotManager,
|
|
hotbar: &mut hotbar::State,
|
|
client_inventory: Option<&comp::Inventory>,
|
|
) {
|
|
use slots::InventorySlot;
|
|
if let Some(slots::SlotKind::Inventory(InventorySlot {
|
|
slot: i,
|
|
ours: true,
|
|
..
|
|
})) = slot_manager.selected()
|
|
{
|
|
if let Some(item) = client_inventory.and_then(|inv| inv.get(i)) {
|
|
hotbar.add_inventory_link(slot, item);
|
|
events.push(Event::ChangeHotbarState(Box::new(hotbar.to_owned())));
|
|
slot_manager.idle();
|
|
}
|
|
} else {
|
|
let just_pressed = hotbar.process_input(slot, state);
|
|
hotbar.get(slot).map(|s| match s {
|
|
hotbar::SlotContents::Inventory(i, _) => {
|
|
if just_pressed {
|
|
if let Some(inv) = client_inventory {
|
|
// If the item in the inactive main hand is the same as the item
|
|
// pressed in the hotbar, then swap active and inactive hands
|
|
// instead of looking for the item
|
|
// in the inventory
|
|
if inv
|
|
.equipped(comp::slot::EquipSlot::InactiveMainhand)
|
|
.map_or(false, |item| item.item_hash() == i)
|
|
{
|
|
events.push(Event::SwapEquippedWeapons);
|
|
} else if let Some(slot) = inv.get_slot_from_hash(i) {
|
|
events.push(Event::UseSlot {
|
|
slot: comp::slot::Slot::Inventory(slot),
|
|
bypass_dialog: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
hotbar::SlotContents::Ability(i) => events.push(Event::Ability(i, state)),
|
|
});
|
|
}
|
|
}
|
|
|
|
fn handle_map_zoom(
|
|
factor: f64,
|
|
world_size: Vec2<u32>,
|
|
show: &Show,
|
|
global_state: &mut GlobalState,
|
|
) -> bool {
|
|
let max_zoom = world_size.reduce_partial_max() as f64;
|
|
|
|
if show.map {
|
|
let new_zoom_lvl = (global_state.settings.interface.map_zoom * factor)
|
|
.clamped(1.25, max_zoom / 64.0);
|
|
|
|
global_state.settings.interface.map_zoom = new_zoom_lvl;
|
|
global_state
|
|
.settings
|
|
.save_to_file_warn(&global_state.config_dir);
|
|
} else if global_state.settings.interface.minimap_show {
|
|
let new_zoom_lvl = global_state.settings.interface.minimap_zoom * factor;
|
|
|
|
global_state.settings.interface.minimap_zoom = new_zoom_lvl;
|
|
global_state
|
|
.settings
|
|
.save_to_file_warn(&global_state.config_dir);
|
|
}
|
|
|
|
show.map && global_state.settings.interface.minimap_show
|
|
}
|
|
|
|
let cursor_grabbed = global_state.window.is_cursor_grabbed();
|
|
let handled = match event {
|
|
WinEvent::Ui(event) => {
|
|
if (self.typing() && event.is_keyboard() && self.show.ui)
|
|
|| !(cursor_grabbed && event.is_keyboard_or_mouse())
|
|
{
|
|
self.ui.handle_event(event);
|
|
}
|
|
true
|
|
},
|
|
WinEvent::ScaleFactorChanged(scale_factor) => {
|
|
self.ui.scale_factor_changed(scale_factor);
|
|
false
|
|
},
|
|
WinEvent::InputUpdate(GameInput::ToggleInterface, true) if !self.typing() => {
|
|
self.show.toggle_ui();
|
|
true
|
|
},
|
|
WinEvent::InputUpdate(GameInput::ToggleCursor, true) if !self.typing() => {
|
|
self.force_ungrab = !self.force_ungrab;
|
|
true
|
|
},
|
|
WinEvent::InputUpdate(GameInput::AcceptGroupInvite, true) if !self.typing() => {
|
|
if let Some(prompt_dialog) = &mut self.show.prompt_dialog {
|
|
prompt_dialog.set_outcome_via_keypress(true);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
},
|
|
WinEvent::InputUpdate(GameInput::DeclineGroupInvite, true) if !self.typing() => {
|
|
if let Some(prompt_dialog) = &mut self.show.prompt_dialog {
|
|
prompt_dialog.set_outcome_via_keypress(false);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
},
|
|
|
|
// If not showing the ui don't allow keys that change the ui state but do listen for
|
|
// hotbar keys
|
|
WinEvent::InputUpdate(key, state) if !self.show.ui => {
|
|
if let Some(slot) = try_hotbar_slot_from_input(key) {
|
|
handle_slot(
|
|
slot,
|
|
state,
|
|
&mut self.events,
|
|
&mut self.slot_manager,
|
|
&mut self.hotbar,
|
|
client_inventory,
|
|
);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
},
|
|
|
|
WinEvent::Zoom(_) => !cursor_grabbed && !self.ui.no_widget_capturing_mouse(),
|
|
|
|
WinEvent::InputUpdate(GameInput::Chat, true) => {
|
|
self.ui.focus_widget(if self.typing() {
|
|
None
|
|
} else {
|
|
Some(self.ids.chat)
|
|
});
|
|
true
|
|
},
|
|
WinEvent::InputUpdate(GameInput::Escape, true) => {
|
|
if self.typing() {
|
|
self.ui.focus_widget(None);
|
|
} else if self.show.trade {
|
|
self.events.push(Event::TradeAction(TradeAction::Decline));
|
|
} else {
|
|
// Close windows on esc
|
|
if self.show.bag {
|
|
self.slot_manager.idle();
|
|
}
|
|
self.show.toggle_windows(global_state);
|
|
}
|
|
true
|
|
},
|
|
|
|
// Press key while not typing
|
|
WinEvent::InputUpdate(key, state) if !self.typing() => {
|
|
let gs_audio = &global_state.settings.audio;
|
|
let mut toggle_mute = |audio: Audio| {
|
|
self.events
|
|
.push(Event::SettingsChange(SettingsChange::Audio(audio)));
|
|
true
|
|
};
|
|
|
|
let matching_key = match key {
|
|
GameInput::Command if state => {
|
|
self.force_chat_input = Some("/".to_owned());
|
|
self.force_chat_cursor = Some(Index { line: 0, char: 1 });
|
|
self.ui.focus_widget(Some(self.ids.chat));
|
|
true
|
|
},
|
|
GameInput::Map if state => {
|
|
self.show.toggle_map();
|
|
true
|
|
},
|
|
GameInput::Bag if state => {
|
|
let state = !self.show.bag;
|
|
Self::show_bag(&mut self.slot_manager, &mut self.show, state);
|
|
true
|
|
},
|
|
GameInput::Social if state => {
|
|
self.show.toggle_social();
|
|
true
|
|
},
|
|
GameInput::Crafting if state => {
|
|
self.show.toggle_crafting();
|
|
true
|
|
},
|
|
GameInput::Spellbook if state => {
|
|
self.show.toggle_spell();
|
|
true
|
|
},
|
|
GameInput::Settings if state => {
|
|
self.show.toggle_settings(global_state);
|
|
true
|
|
},
|
|
GameInput::Help if state => {
|
|
self.show.toggle_settings(global_state);
|
|
self.show.settings_tab = SettingsTab::Controls;
|
|
true
|
|
},
|
|
GameInput::ToggleDebug if state => {
|
|
global_state.settings.interface.toggle_debug =
|
|
!global_state.settings.interface.toggle_debug;
|
|
true
|
|
},
|
|
#[cfg(feature = "egui-ui")]
|
|
GameInput::ToggleEguiDebug if state => {
|
|
global_state.settings.interface.toggle_egui_debug =
|
|
!global_state.settings.interface.toggle_egui_debug;
|
|
true
|
|
},
|
|
GameInput::ToggleChat if state => {
|
|
global_state.settings.interface.toggle_chat =
|
|
!global_state.settings.interface.toggle_chat;
|
|
true
|
|
},
|
|
GameInput::ToggleIngameUi if state => {
|
|
self.show.ingame = !self.show.ingame;
|
|
true
|
|
},
|
|
GameInput::MapZoomIn if state => {
|
|
handle_map_zoom(2.0, self.world_map.1, &self.show, global_state)
|
|
},
|
|
GameInput::MapZoomOut if state => {
|
|
handle_map_zoom(0.5, self.world_map.1, &self.show, global_state)
|
|
},
|
|
GameInput::MuteMaster if state => {
|
|
toggle_mute(Audio::MuteMasterVolume(!gs_audio.master_volume.muted))
|
|
},
|
|
GameInput::MuteInactiveMaster if state => {
|
|
toggle_mute(Audio::MuteInactiveMasterVolume(
|
|
!gs_audio.inactive_master_volume_perc.muted,
|
|
))
|
|
},
|
|
GameInput::MuteMusic if state => {
|
|
toggle_mute(Audio::MuteMusicVolume(!gs_audio.music_volume.muted))
|
|
},
|
|
GameInput::MuteSfx if state => {
|
|
toggle_mute(Audio::MuteSfxVolume(!gs_audio.sfx_volume.muted))
|
|
},
|
|
GameInput::MuteAmbience if state => {
|
|
toggle_mute(Audio::MuteAmbienceVolume(!gs_audio.ambience_volume.muted))
|
|
},
|
|
// Skillbar
|
|
input => {
|
|
if let Some(slot) = try_hotbar_slot_from_input(input) {
|
|
handle_slot(
|
|
slot,
|
|
state,
|
|
&mut self.events,
|
|
&mut self.slot_manager,
|
|
&mut self.hotbar,
|
|
client_inventory,
|
|
);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
},
|
|
};
|
|
|
|
// When a player closes all menus, resets the cursor
|
|
// to the center of the screen
|
|
self.show
|
|
.toggle_cursor_on_menu_close(global_state, &mut self.ui);
|
|
matching_key
|
|
},
|
|
// Else the player is typing in chat
|
|
WinEvent::InputUpdate(_key, _) => self.typing(),
|
|
WinEvent::Char(_) => self.typing(),
|
|
WinEvent::Focused(state) => {
|
|
self.force_ungrab = !state;
|
|
true
|
|
},
|
|
WinEvent::Moved(_) => {
|
|
// Prevent the cursor from being grabbed while the window is being moved as this
|
|
// causes the window to move erratically
|
|
// TODO: this creates an issue where if you move the window then you need to
|
|
// close a menu to re-grab the mouse (and if one isn't already
|
|
// open you need to open and close a menu)
|
|
self.show.want_grab = false;
|
|
true
|
|
},
|
|
_ => false,
|
|
};
|
|
// Handle cursor grab.
|
|
global_state
|
|
.window
|
|
.grab_cursor(!self.force_ungrab && self.show.want_grab);
|
|
|
|
handled
|
|
}
|
|
|
|
pub fn maintain(
|
|
&mut self,
|
|
client: &Client,
|
|
global_state: &mut GlobalState,
|
|
debug_info: &Option<DebugInfo>,
|
|
camera: &Camera,
|
|
dt: Duration,
|
|
info: HudInfo,
|
|
interactable: Option<&Interactable>,
|
|
) -> Vec<Event> {
|
|
span!(_guard, "maintain", "Hud::maintain");
|
|
// conrod eats tabs. Un-eat a tabstop so tab completion can work
|
|
if self.ui.ui.global_input().events().any(|event| {
|
|
use conrod_core::{event, input};
|
|
matches!(
|
|
event,
|
|
/* event::Event::Raw(event::Input::Press(input::Button::Keyboard(input::Key::
|
|
* Tab))) | */
|
|
event::Event::Ui(event::Ui::Press(_, event::Press {
|
|
button: event::Button::Keyboard(input::Key::Tab),
|
|
..
|
|
},))
|
|
)
|
|
}) {
|
|
self.ui
|
|
.ui
|
|
.handle_event(conrod_core::event::Input::Text("\t".to_string()));
|
|
}
|
|
|
|
// Stop selecting a sprite to perform crafting with when out of range
|
|
self.show.crafting_fields.craft_sprite =
|
|
self.show.crafting_fields.craft_sprite.filter(|(pos, _)| {
|
|
self.show.crafting
|
|
&& if let Some(player_pos) = client.position() {
|
|
pos.map(|e| e as f32 + 0.5).distance(player_pos) < MAX_PICKUP_RANGE
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
|
|
// Optimization: skip maintaining UI when it's off.
|
|
if !self.show.ui {
|
|
return std::mem::take(&mut self.events);
|
|
}
|
|
|
|
if let Some(maybe_id) = self.to_focus.take() {
|
|
self.ui.focus_widget(maybe_id);
|
|
}
|
|
let events = self.update_layout(
|
|
client,
|
|
global_state,
|
|
debug_info,
|
|
dt,
|
|
info,
|
|
camera,
|
|
interactable,
|
|
);
|
|
let camera::Dependents {
|
|
view_mat, proj_mat, ..
|
|
} = camera.dependents();
|
|
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
|
|
|
// Check if item images need to be reloaded
|
|
self.item_imgs.reload_if_changed(&mut self.ui);
|
|
// TODO: using a thread pool in the obvious way for speeding up map zoom results
|
|
// in flickering artifacts, figure out a better way to make use of the
|
|
// thread pool
|
|
let _pool = client.state().ecs().read_resource::<SlowJobPool>();
|
|
self.ui.maintain(
|
|
global_state.window.renderer_mut(),
|
|
None,
|
|
//Some(&pool),
|
|
Some(proj_mat * view_mat * Mat4::translation_3d(-focus_off)),
|
|
);
|
|
|
|
events
|
|
}
|
|
|
|
#[inline]
|
|
pub fn clear_cursor(&mut self) { self.slot_manager.idle(); }
|
|
|
|
pub fn render<'a>(&'a self, drawer: &mut UiDrawer<'_, 'a>) {
|
|
span!(_guard, "render", "Hud::render");
|
|
// Don't show anything if the UI is toggled off.
|
|
if self.show.ui {
|
|
self.ui.render(drawer);
|
|
}
|
|
}
|
|
|
|
pub fn free_look(&mut self, free_look: bool) { self.show.free_look = free_look; }
|
|
|
|
pub fn auto_walk(&mut self, auto_walk: bool) { self.show.auto_walk = auto_walk; }
|
|
|
|
pub fn camera_clamp(&mut self, camera_clamp: bool) { self.show.camera_clamp = camera_clamp; }
|
|
|
|
/// Remind the player camera zoom is currently locked, for example if they
|
|
/// are trying to zoom.
|
|
pub fn zoom_lock_reminder(&mut self) {
|
|
if self.show.zoom_lock.reason.is_none() {
|
|
self.show.zoom_lock = ChangeNotification::from_reason(NotificationReason::Remind);
|
|
}
|
|
}
|
|
|
|
/// Start showing a temporary notification ([ChangeNotification]) that zoom
|
|
/// lock was toggled on/off.
|
|
pub fn zoom_lock_toggle(&mut self, state: bool) {
|
|
self.show.zoom_lock = ChangeNotification::from_state(state);
|
|
}
|
|
|
|
pub fn handle_outcome(
|
|
&mut self,
|
|
outcome: &Outcome,
|
|
client: &Client,
|
|
global_state: &GlobalState,
|
|
) {
|
|
let interface = &global_state.settings.interface;
|
|
match outcome {
|
|
Outcome::ExpChange { uid, exp, xp_pools } => {
|
|
let ecs = client.state().ecs();
|
|
let uids = ecs.read_storage::<Uid>();
|
|
let me = client.entity();
|
|
|
|
if uids.get(me).map_or(false, |me| *me == *uid) {
|
|
match self.floaters.exp_floaters.last_mut() {
|
|
Some(floater)
|
|
if floater.timer
|
|
> (EXP_FLOATER_LIFETIME - EXP_ACCUMULATION_DURATION)
|
|
&& global_state.settings.interface.accum_experience
|
|
&& floater.owner == *uid =>
|
|
{
|
|
floater.jump_timer = 0.0;
|
|
floater.exp_change += *exp;
|
|
},
|
|
_ => self.floaters.exp_floaters.push(ExpFloater {
|
|
// Store the owner as to not accumulate old experience floaters
|
|
owner: *uid,
|
|
exp_change: *exp,
|
|
timer: EXP_FLOATER_LIFETIME,
|
|
jump_timer: 0.0,
|
|
rand_offset: rand::thread_rng().gen::<(f32, f32)>(),
|
|
xp_pools: xp_pools.clone(),
|
|
}),
|
|
}
|
|
}
|
|
},
|
|
Outcome::SkillPointGain {
|
|
uid,
|
|
skill_tree,
|
|
total_points,
|
|
..
|
|
} => {
|
|
let ecs = client.state().ecs();
|
|
let uids = ecs.read_storage::<Uid>();
|
|
let me = client.entity();
|
|
|
|
if uids.get(me).map_or(false, |me| *me == *uid) {
|
|
self.floaters.skill_point_displays.push(SkillPointGain {
|
|
skill_tree: *skill_tree,
|
|
total_points: *total_points,
|
|
timer: 5.0,
|
|
});
|
|
}
|
|
},
|
|
Outcome::ComboChange { uid, combo } => {
|
|
let ecs = client.state().ecs();
|
|
let uids = ecs.read_storage::<Uid>();
|
|
let me = client.entity();
|
|
|
|
if uids.get(me).map_or(false, |me| *me == *uid) {
|
|
self.floaters.combo_floater = Some(ComboFloater {
|
|
combo: *combo,
|
|
timer: comp::combo::COMBO_DECAY_START,
|
|
});
|
|
}
|
|
},
|
|
Outcome::Block { uid, parry, .. } if *parry => {
|
|
let ecs = client.state().ecs();
|
|
let uids = ecs.read_storage::<Uid>();
|
|
let me = client.entity();
|
|
|
|
if uids.get(me).map_or(false, |me| *me == *uid) {
|
|
self.floaters
|
|
.block_floaters
|
|
.push(BlockFloater { timer: 1.0 });
|
|
}
|
|
},
|
|
Outcome::HealthChange { info, .. } => {
|
|
let ecs = client.state().ecs();
|
|
let mut hp_floater_lists = ecs.write_storage::<HpFloaterList>();
|
|
let uids = ecs.read_storage::<Uid>();
|
|
let me = client.entity();
|
|
let my_uid = uids.get(me);
|
|
|
|
if let Some(entity) = ecs.entity_from_uid(info.target.0) {
|
|
if let Some(floater_list) = hp_floater_lists.get_mut(entity) {
|
|
let hit_me = my_uid.map_or(false, |&uid| {
|
|
(info.target == uid) && global_state.settings.interface.sct_inc_dmg
|
|
});
|
|
if match info.by {
|
|
Some(by) => {
|
|
let by_me = my_uid.map_or(false, |&uid| by.uid() == uid);
|
|
// If the attack was by me also reset this timer
|
|
if by_me {
|
|
floater_list.time_since_last_dmg_by_me = Some(0.0);
|
|
}
|
|
hit_me || by_me
|
|
},
|
|
None => hit_me,
|
|
} {
|
|
// Group up damage from the same tick and instance number
|
|
for floater in floater_list.floaters.iter_mut().rev() {
|
|
if floater.timer > 0.0 {
|
|
break;
|
|
}
|
|
if floater.info.instance == info.instance
|
|
// Group up crits and regular attacks for incoming damage
|
|
&& (hit_me
|
|
|| floater.info.crit
|
|
== info.crit)
|
|
{
|
|
floater.info.amount += info.amount;
|
|
if info.crit {
|
|
floater.info.crit = info.crit
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// To separate healing and damage floaters alongside the crit and
|
|
// non-crit ones
|
|
let last_floater = if !info.crit || hit_me {
|
|
floater_list.floaters.iter_mut().rev().find(|f| {
|
|
(if info.amount < 0.0 {
|
|
f.info.amount < 0.0
|
|
} else {
|
|
f.info.amount > 0.0
|
|
}) && f.timer
|
|
< if hit_me {
|
|
interface.sct_inc_dmg_accum_duration
|
|
} else {
|
|
interface.sct_dmg_accum_duration
|
|
}
|
|
// Ignore crit floaters, unless the damage is incoming
|
|
&& (hit_me || !f.info.crit)
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
match last_floater {
|
|
Some(f) => {
|
|
f.jump_timer = 0.0;
|
|
f.info.amount += info.amount;
|
|
f.info.crit = info.crit;
|
|
},
|
|
_ => {
|
|
floater_list.floaters.push(HpFloater {
|
|
timer: 0.0,
|
|
jump_timer: 0.0,
|
|
info: *info,
|
|
rand: rand::random(),
|
|
});
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_ => {},
|
|
}
|
|
}
|
|
}
|
|
// Get item qualities of equipped items and assign a tooltip title/frame color
|
|
pub fn get_quality_col<I: ItemDesc + ?Sized>(item: &I) -> Color {
|
|
match item.quality() {
|
|
Quality::Low => QUALITY_LOW,
|
|
Quality::Common => QUALITY_COMMON,
|
|
Quality::Moderate => QUALITY_MODERATE,
|
|
Quality::High => QUALITY_HIGH,
|
|
Quality::Epic => QUALITY_EPIC,
|
|
Quality::Legendary => QUALITY_LEGENDARY,
|
|
Quality::Artifact => QUALITY_ARTIFACT,
|
|
Quality::Debug => QUALITY_DEBUG,
|
|
}
|
|
}
|
|
|
|
fn try_hotbar_slot_from_input(input: GameInput) -> Option<hotbar::Slot> {
|
|
Some(match input {
|
|
GameInput::Slot1 => hotbar::Slot::One,
|
|
GameInput::Slot2 => hotbar::Slot::Two,
|
|
GameInput::Slot3 => hotbar::Slot::Three,
|
|
GameInput::Slot4 => hotbar::Slot::Four,
|
|
GameInput::Slot5 => hotbar::Slot::Five,
|
|
GameInput::Slot6 => hotbar::Slot::Six,
|
|
GameInput::Slot7 => hotbar::Slot::Seven,
|
|
GameInput::Slot8 => hotbar::Slot::Eight,
|
|
GameInput::Slot9 => hotbar::Slot::Nine,
|
|
GameInput::Slot10 => hotbar::Slot::Ten,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
pub fn cr_color(combat_rating: f32) -> Color {
|
|
let common = 2.0;
|
|
let moderate = 3.5;
|
|
let high = 6.5;
|
|
let epic = 8.5;
|
|
let legendary = 10.4;
|
|
let artifact = 122.0;
|
|
let debug = 200.0;
|
|
|
|
match combat_rating {
|
|
x if (0.0..common).contains(&x) => QUALITY_LOW,
|
|
x if (common..moderate).contains(&x) => QUALITY_COMMON,
|
|
x if (moderate..high).contains(&x) => QUALITY_MODERATE,
|
|
x if (high..epic).contains(&x) => QUALITY_HIGH,
|
|
x if (epic..legendary).contains(&x) => QUALITY_EPIC,
|
|
x if (legendary..artifact).contains(&x) => QUALITY_LEGENDARY,
|
|
x if (artifact..debug).contains(&x) => QUALITY_ARTIFACT,
|
|
x if x >= debug => QUALITY_DEBUG,
|
|
_ => XP_COLOR,
|
|
}
|
|
}
|
|
|
|
pub fn get_buff_image(buff: BuffKind, imgs: &Imgs) -> conrod_core::image::Id {
|
|
match buff {
|
|
// Buffs
|
|
BuffKind::Regeneration { .. } => imgs.buff_plus_0,
|
|
BuffKind::Saturation { .. } => imgs.buff_saturation_0,
|
|
BuffKind::Potion { .. } => imgs.buff_potion_0,
|
|
BuffKind::CampfireHeal { .. } => imgs.buff_campfire_heal_0,
|
|
BuffKind::EnergyRegen { .. } => imgs.buff_energyplus_0,
|
|
BuffKind::IncreaseMaxEnergy { .. } => imgs.buff_energyplus_0,
|
|
BuffKind::IncreaseMaxHealth { .. } => imgs.buff_healthplus_0,
|
|
BuffKind::Invulnerability => imgs.buff_invincibility_0,
|
|
BuffKind::ProtectingWard => imgs.buff_dmg_red_0,
|
|
BuffKind::Frenzied => imgs.buff_frenzy_0,
|
|
BuffKind::Hastened => imgs.buff_haste_0,
|
|
BuffKind::Fortitude => imgs.buff_fortitude_0,
|
|
// Debuffs
|
|
BuffKind::Bleeding => imgs.debuff_bleed_0,
|
|
BuffKind::Cursed => imgs.debuff_skull_0,
|
|
BuffKind::Burning => imgs.debuff_burning_0,
|
|
BuffKind::Crippled => imgs.debuff_crippled_0,
|
|
BuffKind::Frozen => imgs.debuff_frozen_0,
|
|
BuffKind::Wet => imgs.debuff_wet_0,
|
|
BuffKind::Ensnared => imgs.debuff_ensnared_0,
|
|
BuffKind::Poisoned => imgs.debuff_poisoned_0,
|
|
BuffKind::Parried => imgs.debuff_parried_0,
|
|
BuffKind::PotionSickness => imgs.debuff_potionsickness_0,
|
|
}
|
|
}
|
|
|
|
pub fn get_buff_title(buff: BuffKind, localized_strings: &Localization) -> Cow<str> {
|
|
match buff {
|
|
// Buffs
|
|
BuffKind::Regeneration { .. } => localized_strings.get_msg("buff-title-heal"),
|
|
BuffKind::Saturation { .. } => localized_strings.get_msg("buff-title-saturation"),
|
|
BuffKind::Potion { .. } => localized_strings.get_msg("buff-title-potion"),
|
|
BuffKind::CampfireHeal { .. } => localized_strings.get_msg("buff-title-campfire_heal"),
|
|
BuffKind::EnergyRegen { .. } => localized_strings.get_msg("buff-title-energy_regen"),
|
|
BuffKind::IncreaseMaxHealth { .. } => {
|
|
localized_strings.get_msg("buff-title-increase_max_health")
|
|
},
|
|
BuffKind::IncreaseMaxEnergy { .. } => {
|
|
localized_strings.get_msg("buff-title-increase_max_energy")
|
|
},
|
|
BuffKind::Invulnerability => localized_strings.get_msg("buff-title-invulnerability"),
|
|
BuffKind::ProtectingWard => localized_strings.get_msg("buff-title-protectingward"),
|
|
BuffKind::Frenzied => localized_strings.get_msg("buff-title-frenzied"),
|
|
BuffKind::Hastened => localized_strings.get_msg("buff-title-hastened"),
|
|
BuffKind::Fortitude => localized_strings.get_msg("buff-title-fortitude"),
|
|
// Debuffs
|
|
BuffKind::Bleeding { .. } => localized_strings.get_msg("buff-title-bleed"),
|
|
BuffKind::Cursed { .. } => localized_strings.get_msg("buff-title-cursed"),
|
|
BuffKind::Burning { .. } => localized_strings.get_msg("buff-title-burn"),
|
|
BuffKind::Crippled { .. } => localized_strings.get_msg("buff-title-crippled"),
|
|
BuffKind::Frozen { .. } => localized_strings.get_msg("buff-title-frozen"),
|
|
BuffKind::Wet { .. } => localized_strings.get_msg("buff-title-wet"),
|
|
BuffKind::Ensnared { .. } => localized_strings.get_msg("buff-title-ensnared"),
|
|
BuffKind::Poisoned { .. } => localized_strings.get_msg("buff-title-poisoned"),
|
|
BuffKind::Parried { .. } => localized_strings.get_msg("buff-title-parried"),
|
|
BuffKind::PotionSickness { .. } => localized_strings.get_msg("buff-title-potionsickness"),
|
|
}
|
|
}
|
|
|
|
pub fn get_buff_desc(buff: BuffKind, data: BuffData, localized_strings: &Localization) -> Cow<str> {
|
|
match buff {
|
|
// Buffs
|
|
BuffKind::Regeneration { .. } => localized_strings.get_msg("buff-desc-heal"),
|
|
BuffKind::Saturation { .. } => localized_strings.get_msg("buff-desc-saturation"),
|
|
BuffKind::Potion { .. } => localized_strings.get_msg("buff-desc-potion"),
|
|
BuffKind::CampfireHeal { .. } => {
|
|
localized_strings.get_msg_ctx("buff-desc-campfire_heal", &i18n::fluent_args! {
|
|
"rate" => data.strength * 100.0
|
|
})
|
|
},
|
|
BuffKind::EnergyRegen { .. } => localized_strings.get_msg("buff-desc-energy_regen"),
|
|
BuffKind::IncreaseMaxHealth { .. } => {
|
|
localized_strings.get_msg("buff-desc-increase_max_health")
|
|
},
|
|
BuffKind::IncreaseMaxEnergy { .. } => {
|
|
localized_strings.get_msg("buff-desc-increase_max_energy")
|
|
},
|
|
BuffKind::Invulnerability => localized_strings.get_msg("buff-desc-invulnerability"),
|
|
BuffKind::ProtectingWard => localized_strings.get_msg("buff-desc-protectingward"),
|
|
BuffKind::Frenzied => localized_strings.get_msg("buff-desc-frenzied"),
|
|
BuffKind::Hastened => localized_strings.get_msg("buff-desc-hastened"),
|
|
BuffKind::Fortitude => localized_strings.get_msg("buff-desc-fortitude"),
|
|
// Debuffs
|
|
BuffKind::Bleeding { .. } => localized_strings.get_msg("buff-desc-bleed"),
|
|
BuffKind::Cursed { .. } => localized_strings.get_msg("buff-desc-cursed"),
|
|
BuffKind::Burning { .. } => localized_strings.get_msg("buff-desc-burn"),
|
|
BuffKind::Crippled { .. } => localized_strings.get_msg("buff-desc-crippled"),
|
|
BuffKind::Frozen { .. } => localized_strings.get_msg("buff-desc-frozen"),
|
|
BuffKind::Wet { .. } => localized_strings.get_msg("buff-desc-wet"),
|
|
BuffKind::Ensnared { .. } => localized_strings.get_msg("buff-desc-ensnared"),
|
|
BuffKind::Poisoned { .. } => localized_strings.get_msg("buff-desc-poisoned"),
|
|
BuffKind::Parried { .. } => localized_strings.get_msg("buff-desc-parried"),
|
|
BuffKind::PotionSickness { .. } => localized_strings.get_msg("buff-desc-potionsickness"),
|
|
}
|
|
}
|
|
|
|
pub fn get_sprite_desc(sprite: SpriteKind, localized_strings: &Localization) -> Option<Cow<str>> {
|
|
let i18n_key = match sprite {
|
|
SpriteKind::Empty => return None,
|
|
SpriteKind::GlassBarrier => return None,
|
|
SpriteKind::Anvil => "hud-crafting-anvil",
|
|
SpriteKind::Cauldron => "hud-crafting-cauldron",
|
|
SpriteKind::CookingPot => "hud-crafting-cooking_pot",
|
|
SpriteKind::CraftingBench => "hud-crafting-crafting_bench",
|
|
SpriteKind::Forge => "hud-crafting-forge",
|
|
SpriteKind::Loom => "hud-crafting-loom",
|
|
SpriteKind::SpinningWheel => "hud-crafting-spinning_wheel",
|
|
SpriteKind::TanningRack => "hud-crafting-tanning_rack",
|
|
SpriteKind::DismantlingBench => "hud-crafting-salvaging_station",
|
|
SpriteKind::ChestBuried
|
|
| SpriteKind::Chest
|
|
| SpriteKind::CoralChest
|
|
| SpriteKind::DungeonChest0
|
|
| SpriteKind::DungeonChest1
|
|
| SpriteKind::DungeonChest2
|
|
| SpriteKind::DungeonChest3
|
|
| SpriteKind::DungeonChest4
|
|
| SpriteKind::DungeonChest5 => "common-sprite-chest",
|
|
sprite => return Some(Cow::Owned(format!("{:?}", sprite))),
|
|
};
|
|
Some(localized_strings.get_msg(i18n_key))
|
|
}
|
|
|
|
pub fn angle_of_attack_text(
|
|
fluid: Option<comp::Fluid>,
|
|
velocity: Option<comp::Vel>,
|
|
character_state: Option<&comp::CharacterState>,
|
|
) -> String {
|
|
use comp::CharacterState;
|
|
|
|
let glider_ori = if let Some(CharacterState::Glide(data)) = character_state {
|
|
data.ori
|
|
} else {
|
|
return "Angle of Attack: Not gliding".to_owned();
|
|
};
|
|
|
|
let fluid = if let Some(fluid) = fluid {
|
|
fluid
|
|
} else {
|
|
return "Angle of Attack: Not in fluid".to_owned();
|
|
};
|
|
|
|
let velocity = if let Some(velocity) = velocity {
|
|
velocity
|
|
} else {
|
|
return "Angle of Attack: Player has no vel component".to_owned();
|
|
};
|
|
let rel_flow = fluid.relative_flow(&velocity).0;
|
|
let v_sq = rel_flow.magnitude_squared();
|
|
|
|
if v_sq.abs() > 0.0001 {
|
|
let rel_flow_dir = Dir::new(rel_flow / v_sq.sqrt());
|
|
let aoe = fluid_dynamics::angle_of_attack(&glider_ori, &rel_flow_dir);
|
|
format!("Angle of Attack: {:.1}", aoe.to_degrees())
|
|
} else {
|
|
"Angle of Attack: Not moving".to_owned()
|
|
}
|
|
}
|
|
|
|
/// Converts multiplier to percentage.
|
|
/// NOTE: floats are not the most precise type.
|
|
///
|
|
/// # Examples
|
|
/// ```
|
|
/// use veloren_voxygen::hud::multiplier_to_percentage;
|
|
///
|
|
/// let positive = multiplier_to_percentage(1.05);
|
|
/// assert!((positive - 5.0).abs() < 0.0001);
|
|
/// let negative = multiplier_to_percentage(0.85);
|
|
/// assert!((negative - (-15.0)).abs() < 0.0001);
|
|
/// ```
|
|
pub fn multiplier_to_percentage(value: f32) -> f32 { value * 100.0 - 100.0 }
|