diff --git a/CHANGELOG.md b/CHANGELOG.md index 479a78475a..dff6ff7633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Merchants now use `/tell` instead of `/say` to communicate prices - Entities catch on fire if they stand too close to campfires - Water extinguishes entities on fire +- Item pickups are shown in separate window and inventory-full shows above item ### Removed diff --git a/assets/voxygen/element/ui/chat/icons/loot_small.png b/assets/voxygen/element/ui/chat/icons/loot_small.png deleted file mode 100644 index 5c524b13d6..0000000000 --- a/assets/voxygen/element/ui/chat/icons/loot_small.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d40bf0685963c60fa6b10281dd7e4bd67eca96b035dbb2cad5f5d4bc7e2c6cbc -size 256 diff --git a/assets/voxygen/i18n/en/hud/misc.ron b/assets/voxygen/i18n/en/hud/misc.ron index 7f3f57c543..04b74ef477 100644 --- a/assets/voxygen/i18n/en/hud/misc.ron +++ b/assets/voxygen/i18n/en/hud/misc.ron @@ -9,6 +9,7 @@ "hud.you_died": "You Died", "hud.waypoint_saved": "Waypoint Saved", "hud.sp_arrow_txt": "SP", + "hud.inventory_full": "Inventory Full", "hud.press_key_to_show_keybindings_fmt": "[{key}] Keybindings", "hud.press_key_to_toggle_lantern_fmt": "[{key}] Lantern", diff --git a/client/src/lib.rs b/client/src/lib.rs index 22816cb656..c2499fe7bd 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1900,7 +1900,8 @@ impl Client { }, ServerGeneral::InventoryUpdate(inventory, event) => { match event { - InventoryUpdateEvent::CollectFailed => {}, + InventoryUpdateEvent::BlockCollectFailed(_) => {}, + InventoryUpdateEvent::EntityCollectFailed(_) => {}, _ => { // Push the updated inventory component to the client // FIXME: Figure out whether this error can happen under normal gameplay, @@ -2287,7 +2288,6 @@ impl Client { }, comp::ChatType::CommandError => message.to_string(), comp::ChatType::CommandInfo => message.to_string(), - comp::ChatType::Loot => message.to_string(), comp::ChatType::FactionMeta(_) => message.to_string(), comp::ChatType::GroupMeta(_) => message.to_string(), comp::ChatType::Kill(kill_source, victim) => { diff --git a/common/src/comp/chat.rs b/common/src/comp/chat.rs index a2e1b080db..fc5ce92855 100644 --- a/common/src/comp/chat.rs +++ b/common/src/comp/chat.rs @@ -113,8 +113,6 @@ pub enum ChatType { NpcTell(Uid, Uid, u16), /// Anything else Meta, - // Looted items - Loot, } impl ChatType { @@ -166,7 +164,6 @@ impl GenericChatMsg { ChatType::Offline(a) => ChatType::Offline(a), ChatType::CommandInfo => ChatType::CommandInfo, ChatType::CommandError => ChatType::CommandError, - ChatType::Loot => ChatType::Loot, ChatType::FactionMeta(a) => ChatType::FactionMeta(a), ChatType::GroupMeta(g) => ChatType::GroupMeta(f(g)), ChatType::Kill(a, b) => ChatType::Kill(a, b), @@ -214,7 +211,6 @@ impl GenericChatMsg { ChatType::Offline(_) => SpeechBubbleType::None, ChatType::CommandInfo => SpeechBubbleType::None, ChatType::CommandError => SpeechBubbleType::None, - ChatType::Loot => SpeechBubbleType::None, ChatType::FactionMeta(_) => SpeechBubbleType::None, ChatType::GroupMeta(_) => SpeechBubbleType::None, ChatType::Kill(_, _) => SpeechBubbleType::None, @@ -237,7 +233,6 @@ impl GenericChatMsg { ChatType::Offline(_) => None, ChatType::CommandInfo => None, ChatType::CommandError => None, - ChatType::Loot => None, ChatType::FactionMeta(_) => None, ChatType::GroupMeta(_) => None, ChatType::Kill(_, _) => None, diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index ff0bfb11f4..36ba9cd2c9 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -4,6 +4,7 @@ use specs::{Component, DerefFlaggedStorage}; use specs_idvs::IdvStorage; use std::{collections::HashMap, convert::TryFrom, mem, ops::Range}; use tracing::{debug, trace, warn}; +use vek::Vec3; use crate::{ comp::{ @@ -16,6 +17,7 @@ use crate::{ Item, }, recipe::{Recipe, RecipeInput}, + uid::Uid, LoadoutBuilder, }; @@ -817,7 +819,8 @@ pub enum InventoryUpdateEvent { Swapped, Dropped, Collected(Item), - CollectFailed, + BlockCollectFailed(Vec3), + EntityCollectFailed(Uid), Possession, Debug, Craft, diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index f1afa34712..556d40f966 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -158,7 +158,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv ); drop(item_storage); drop(inventories); - comp::InventoryUpdate::new(comp::InventoryUpdateEvent::CollectFailed) + comp::InventoryUpdate::new(comp::InventoryUpdateEvent::EntityCollectFailed(uid)) }, Ok(_) => { // We succeeded in picking up the item, so we may now delete its old entity @@ -216,7 +216,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv // drop it. Err(_) => ( Some(comp::InventoryUpdate::new( - comp::InventoryUpdateEvent::CollectFailed, + comp::InventoryUpdateEvent::BlockCollectFailed(pos), )), false, ), diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 0088b4ca92..c1a57cd7f9 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -543,7 +543,6 @@ impl StateExt for State { comp::ChatType::Offline(_) | comp::ChatType::CommandInfo | comp::ChatType::CommandError - | comp::ChatType::Loot | comp::ChatType::Meta | comp::ChatType::World(_) => self.notify_players(ServerGeneral::ChatMsg(resolved_msg)), comp::ChatType::Online(u) => { diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index b43417ed10..3a800eeb35 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -217,7 +217,8 @@ impl From<&InventoryUpdateEvent> for SfxEvent { _ => SfxEvent::Inventory(SfxInventoryEvent::Collected), } }, - InventoryUpdateEvent::CollectFailed => { + InventoryUpdateEvent::BlockCollectFailed(_) + | InventoryUpdateEvent::EntityCollectFailed(_) => { SfxEvent::Inventory(SfxInventoryEvent::CollectFailed) }, InventoryUpdateEvent::Consumed(consumable) => { diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 30d0c2cad1..3943a0891d 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -1,7 +1,6 @@ use super::{ img_ids::Imgs, ChatTab, ERROR_COLOR, FACTION_COLOR, GROUP_COLOR, INFO_COLOR, KILL_COLOR, - LOOT_COLOR, OFFLINE_COLOR, ONLINE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, - WORLD_COLOR, + OFFLINE_COLOR, ONLINE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, WORLD_COLOR, }; use crate::{i18n::Localization, settings::chat::MAX_CHAT_TABS, ui::fonts::Fonts, GlobalState}; use client::{cmd, Client}; @@ -722,7 +721,6 @@ fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod ChatType::Offline(_) => (OFFLINE_COLOR, imgs.chat_offline_small), ChatType::CommandError => (ERROR_COLOR, imgs.chat_command_error_small), ChatType::CommandInfo => (INFO_COLOR, imgs.chat_command_info_small), - ChatType::Loot => (LOOT_COLOR, imgs.chat_loot_small), ChatType::GroupMeta(_) => (GROUP_COLOR, imgs.chat_group_small), ChatType::FactionMeta(_) => (FACTION_COLOR, imgs.chat_faction_small), ChatType::Kill(_, _) => (KILL_COLOR, imgs.chat_kill_small), diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 712754a2b3..c16e3bdeea 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -561,7 +561,6 @@ image_ids! { chat_command_info_small: "voxygen.element.ui.chat.icons.command_info_small", chat_online_small: "voxygen.element.ui.chat.icons.online_small", chat_offline_small: "voxygen.element.ui.chat.icons.offline_small", - chat_loot_small: "voxygen.element.ui.chat.icons.loot_small", chat_faction: "voxygen.element.ui.chat.icons.faction", chat_group: "voxygen.element.ui.chat.icons.group", diff --git a/voxygen/src/hud/loot_scroller.rs b/voxygen/src/hud/loot_scroller.rs new file mode 100644 index 0000000000..1ec03ea6d1 --- /dev/null +++ b/voxygen/src/hud/loot_scroller.rs @@ -0,0 +1,384 @@ +use super::{ + animate_by_pulse, get_quality_col, + img_ids::{Imgs, ImgsRot}, + item_imgs::ItemImgs, + Show, TEXT_COLOR, +}; +use crate::{ + i18n::Localization, + ui::{fonts::Fonts, ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable}, +}; +use client::Client; +use common::comp::inventory::item::{ItemDef, MaterialStatManifest, Quality}; +use conrod_core::{ + color, + position::Dimension, + widget::{self, Image, List, Rectangle, Scrollbar, Text}, + widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, +}; +use std::{collections::VecDeque, sync::Arc}; + +widget_ids! { + struct Ids{ + frame, + message_box, + scrollbar, + message_icons[], + message_icon_bgs[], + message_icon_frames[], + message_texts[], + message_text_shadows[], + } +} + +const MAX_MESSAGES: usize = 50; + +const BOX_WIDTH: f64 = 300.0; +const BOX_HEIGHT: f64 = 350.0; + +const ICON_BG_SIZE: f64 = 33.0; +const ICON_SIZE: f64 = 30.0; +const ICON_LABEL_SPACER: f64 = 7.0; + +const MESSAGE_VERTICAL_PADDING: f64 = 1.0; + +const HOVER_FADE_OUT_TIME: f32 = 2.0; +const MESSAGE_FADE_OUT_TIME: f32 = 4.5; +const AUTO_SHOW_FADE_OUT_TIME: f32 = 1.0; + +const MAX_MERGE_TIME: f32 = MESSAGE_FADE_OUT_TIME; + +#[derive(WidgetCommon)] +pub struct LootScroller<'a> { + new_messages: &'a mut VecDeque, + + client: &'a Client, + show: &'a Show, + imgs: &'a Imgs, + item_imgs: &'a ItemImgs, + rot_imgs: &'a ImgsRot, + fonts: &'a Fonts, + localized_strings: &'a Localization, + msm: &'a MaterialStatManifest, + item_tooltip_manager: &'a mut ItemTooltipManager, + pulse: f32, + + #[conrod(common_builder)] + common: widget::CommonBuilder, +} +impl<'a> LootScroller<'a> { + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 + pub fn new( + new_messages: &'a mut VecDeque, + client: &'a Client, + show: &'a Show, + imgs: &'a Imgs, + item_imgs: &'a ItemImgs, + rot_imgs: &'a ImgsRot, + fonts: &'a Fonts, + localized_strings: &'a Localization, + msm: &'a MaterialStatManifest, + item_tooltip_manager: &'a mut ItemTooltipManager, + pulse: f32, + ) -> Self { + Self { + new_messages, + client, + show, + imgs, + item_imgs, + rot_imgs, + fonts, + localized_strings, + msm, + item_tooltip_manager, + pulse, + common: widget::CommonBuilder::default(), + } + } +} + +#[derive(Debug, PartialEq)] +pub struct LootMessage { + pub item: Arc, + pub amount: u32, +} + +pub struct State { + ids: Ids, + messages: VecDeque<(LootMessage, f32)>, // (message, timestamp) + + last_hover_pulse: Option, + last_auto_show_pulse: Option, // auto show if (for example) bag is open +} + +impl<'a> Widget for LootScroller<'a> { + type Event = (); + type State = State; + type Style = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + messages: VecDeque::new(), + last_hover_pulse: None, + last_auto_show_pulse: None, + } + } + + #[allow(clippy::unused_unit)] // TODO: Pending review in #587 + fn style(&self) -> Self::Style { () } + + fn update(self, args: widget::UpdateArgs) -> Self::Event { + let widget::UpdateArgs { state, ui, .. } = args; + + // Tooltips + let item_tooltip = ItemTooltip::new( + { + // Edge images [t, b, r, l] + // Corner images [tr, tl, br, bl] + let edge = &self.rot_imgs.tt_side; + let corner = &self.rot_imgs.tt_corner; + ImageFrame::new( + [edge.cw180, edge.none, edge.cw270, edge.cw90], + [corner.none, corner.cw270, corner.cw90, corner.cw180], + Color::Rgba(0.08, 0.07, 0.04, 1.0), + 5.0, + ) + }, + self.client, + self.imgs, + self.item_imgs, + self.pulse, + self.msm, + self.localized_strings, + ) + .title_font_size(self.fonts.cyri.scale(20)) + .parent(ui.window) + .desc_font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .desc_text_color(TEXT_COLOR); + + if !self.new_messages.is_empty() { + let pulse = self.pulse; + let oldest_merge_pulse = pulse - MAX_MERGE_TIME; + + state.update(|s| { + s.messages.retain(|(message, t)| { + if *t >= oldest_merge_pulse { + if let Some(i) = self + .new_messages + .iter() + .position(|m| m.item.id() == message.item.id()) + { + self.new_messages[i].amount += message.amount; + false + } else { + true + } + } else { + true + } + }); + s.messages + .extend(self.new_messages.drain(..).map(|message| (message, pulse))); + while s.messages.len() > MAX_MESSAGES { + s.messages.pop_front(); + } + }); + ui.scroll_widget(state.ids.message_box, [0.0, std::f64::MAX]); + } + + if self.show.social || self.show.trade { + if state.last_hover_pulse.is_some() || state.last_auto_show_pulse.is_some() { + state.update(|s| { + s.last_hover_pulse = None; + s.last_auto_show_pulse = None; + }); + } + } else { + if ui + .rect_of(state.ids.message_box) + .map_or(false, |r| r.is_over(ui.global_input().current.mouse.xy)) + { + state.update(|s| s.last_hover_pulse = Some(self.pulse)); + } + + if state.ids.message_icons.len() < state.messages.len() { + state.update(|s| { + s.ids + .message_icons + .resize(s.messages.len(), &mut ui.widget_id_generator()) + }); + } + if state.ids.message_icon_bgs.len() < state.messages.len() { + state.update(|s| { + s.ids + .message_icon_bgs + .resize(s.messages.len(), &mut ui.widget_id_generator()) + }); + } + if state.ids.message_icon_frames.len() < state.messages.len() { + state.update(|s| { + s.ids + .message_icon_frames + .resize(s.messages.len(), &mut ui.widget_id_generator()) + }); + } + if state.ids.message_texts.len() < state.messages.len() { + state.update(|s| { + s.ids + .message_texts + .resize(s.messages.len(), &mut ui.widget_id_generator()) + }); + } + if state.ids.message_text_shadows.len() < state.messages.len() { + state.update(|s| { + s.ids + .message_text_shadows + .resize(s.messages.len(), &mut ui.widget_id_generator()) + }); + } + + let hover_age = state + .last_hover_pulse + .map_or(1.0, |t| (self.pulse - t) / HOVER_FADE_OUT_TIME); + let auto_show_age = state + .last_auto_show_pulse + .map_or(1.0, |t| (self.pulse - t) / AUTO_SHOW_FADE_OUT_TIME); + + let show_all_age = hover_age.min(auto_show_age); + + let messages_to_display = state + .messages + .iter() + .rev() + .map(|(message, t)| { + let age = (self.pulse - t) / MESSAGE_FADE_OUT_TIME; + (message, age) + }) + .filter(|(_, age)| age.min(show_all_age) < 1.0) + .collect::>(); + + let (mut list_messages, _) = List::flow_up(messages_to_display.len()) + .w_h(BOX_WIDTH, BOX_HEIGHT) + .scroll_kids_vertically() + .bottom_left_with_margins_on(ui.window, 308.0, 20.0) + .set(state.ids.message_box, ui); + + if show_all_age < 1.0 + && ui + .widget_graph() + .widget(state.ids.message_box) + .map(|w| w.maybe_y_scroll_state) + .flatten() + .map_or(false, |s| s.scrollable_range_len > BOX_HEIGHT) + { + Scrollbar::y_axis(state.ids.message_box) + .thickness(5.0) + .rgba(0.33, 0.33, 0.33, 1.0 - show_all_age.powi(4)) + .left_from(state.ids.message_box, 1.0) + .set(state.ids.scrollbar, ui); + } + + while let Some(list_message) = list_messages.next(ui) { + let i = list_message.i; + + let (message, age) = messages_to_display[i]; + let LootMessage { item, amount } = message; + + let alpha = 1.0 - age.min(show_all_age).powi(4); + + let brightness = 1.0 / (age / 0.05 - 1.0).abs().clamp(0.01, 1.0); + + let shade_color = |color: Color| { + let color::Hsla(hue, sat, lum, alp) = color.to_hsl(); + color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha) + }; + + let quality_col_image = match item.quality { + Quality::Low => self.imgs.inv_slot_grey, + Quality::Common => self.imgs.inv_slot, + Quality::Moderate => self.imgs.inv_slot_green, + Quality::High => self.imgs.inv_slot_blue, + Quality::Epic => self.imgs.inv_slot_purple, + Quality::Legendary => self.imgs.inv_slot_gold, + Quality::Artifact => self.imgs.inv_slot_orange, + _ => self.imgs.inv_slot_red, + }; + let quality_col = get_quality_col(&**item); + + Image::new(self.imgs.pixel) + .color(Some(shade_color(quality_col.alpha(0.7)))) + .w_h(ICON_BG_SIZE, ICON_BG_SIZE) + .top_left_with_margins_on(list_message.widget_id, MESSAGE_VERTICAL_PADDING, 0.0) + .set(state.ids.message_icon_bgs[i], ui); + + Image::new(quality_col_image) + .color(Some(shade_color(color::hsla(0.0, 0.0, 1.0, 1.0)))) + .wh_of(state.ids.message_icon_bgs[i]) + .middle_of(state.ids.message_icon_bgs[i]) + .set(state.ids.message_icon_frames[i], ui); + + Image::new(animate_by_pulse( + &self.item_imgs.img_ids_or_not_found_img((&**item).into()), + self.pulse, + )) + .color(Some(shade_color(color::hsla(0.0, 0.0, 1.0, 1.0)))) + .w_h(ICON_SIZE, ICON_SIZE) + .middle_of(state.ids.message_icon_bgs[i]) + .with_item_tooltip(self.item_tooltip_manager, &**item, &None, &item_tooltip) + .set(state.ids.message_icons[i], ui); + + let label = if *amount == 1 { + item.name.to_string() + } else { + format!("{}x {}", amount, item.name) + }; + let label_font_size = 20; + + Text::new(&label) + .top_left_with_margins_on( + list_message.widget_id, + MESSAGE_VERTICAL_PADDING + 1.0, + ICON_BG_SIZE + ICON_LABEL_SPACER, + ) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(label_font_size)) + .color(shade_color(quality_col)) + .graphics_for(state.ids.message_icons[i]) + .and(|text| { + let text_width = match text.get_x_dimension(ui) { + Dimension::Absolute(x) => x, + _ => std::f64::MAX, + } + .min(BOX_WIDTH - (ICON_BG_SIZE + ICON_LABEL_SPACER)); + text.w(text_width) + }) + .set(state.ids.message_texts[i], ui); + Text::new(&label) + .depth(1.0) + .parent(list_message.widget_id) + .x_y_relative_to(state.ids.message_texts[i], -1.0, -1.0) + .wh_of(state.ids.message_texts[i]) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(label_font_size)) + .color(shade_color(color::rgba(0.0, 0.0, 0.0, 1.0))) + .set(state.ids.message_text_shadows[i], ui); + + let height = 2.0 * MESSAGE_VERTICAL_PADDING + + ICON_BG_SIZE.max( + 1.0 + ui + .rect_of(state.ids.message_texts[i]) + .map_or(0.0, |r| r.h() + label_font_size as f64 / 3.0), + /* add to height since rect height does not account for lower parts of + * letters */ + ); + + let rect = Rectangle::fill_with([BOX_WIDTH, height], color::TRANSPARENT); + + list_message.set(rect, ui); + } + } + } +} diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 3bc56d24ee..22ee597401 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -9,6 +9,7 @@ mod group; mod hotbar; pub mod img_ids; pub mod item_imgs; +mod loot_scroller; mod map; mod minimap; mod overhead; @@ -25,6 +26,7 @@ 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; @@ -38,6 +40,7 @@ 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; use popup::Popup; @@ -60,7 +63,8 @@ use crate::{ }, settings::chat::ChatFilter, ui::{ - fonts::Fonts, img_ids::Rotations, slot, slot::SlotKey, Graphic, Ingameable, ScaleMode, Ui, + self, fonts::Fonts, img_ids::Rotations, slot, slot::SlotKey, Graphic, Ingameable, + ScaleMode, Ui, }, window::{Event as WinEvent, GameInput}, GlobalState, @@ -95,7 +99,7 @@ use conrod_core::{ }; use hashbrown::HashMap; use rand::Rng; -use specs::{Join, WorldExt}; +use specs::{Entity as EcsEntity, Join, WorldExt}; use std::{ borrow::Cow, collections::VecDeque, @@ -161,8 +165,6 @@ const REGION_COLOR: Color = Color::Rgba(0.8, 1.0, 0.8, 1.0); 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); -/// Color for collected loot messages -const LOOT_COLOR: Color = Color::Rgba(0.69, 0.57, 1.0, 1.0); //Nametags const GROUP_MEMBER: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0); @@ -258,6 +260,7 @@ widget_ids! { // External chat, + loot_scroller, map, world_map, character_window, @@ -728,7 +731,7 @@ impl Show { /// 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) { + fn toggle_cursor_on_menu_close(&self, global_state: &mut GlobalState, ui: &mut Ui) { if !self.bag && !self.trade && !self.esc_menu @@ -740,6 +743,9 @@ impl Show { && !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(); } } @@ -782,6 +788,9 @@ pub struct Hud { item_imgs: ItemImgs, fonts: Fonts, rot_imgs: ImgsRot, + failed_block_pickups: HashMap, f32>, + failed_entity_pickups: HashMap, + new_loot_messages: VecDeque, new_messages: VecDeque, new_notifications: VecDeque, speech_bubbles: HashMap, @@ -864,6 +873,9 @@ impl Hud { 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(), @@ -1434,8 +1446,9 @@ impl Hud { 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, active, fonts| { + let make_overitem = |item: &Item, pos, distance, properties, fonts| { let text = if item.amount() > 1 { format!("{} x {}", item.amount(), item.name()) } else { @@ -1450,15 +1463,22 @@ impl Hud { quality, distance, fonts, + &i18n, &global_state.settings.controls, // If we're currently set to interact with the item... - active, + properties, + pulse, &global_state.window.key_layout, ) .x_y(0.0, 100.0) .position_ingame(pos) }; + self.failed_block_pickups + .retain(|_, t| pulse - *t < overitem::PICKUP_FAILED_FADE_OUT_TIME); + self.failed_entity_pickups + .retain(|_, t| pulse - *t < overitem::PICKUP_FAILED_FADE_OUT_TIME); + // Render overitem: name, etc. for (entity, pos, item, distance) in (&entities, &pos, &items) .join() @@ -1474,7 +1494,10 @@ impl Hud { item, pos.0 + Vec3::unit_z() * 1.2, distance, - interactable.as_ref().and_then(|i| i.entity()) == Some(entity), + overitem::OveritemProperties { + active: interactable.as_ref().and_then(|i| i.entity()) == Some(entity), + pickup_failed_pulse: self.failed_entity_pickups.get(&entity).copied(), + }, &self.fonts, ) .set(overitem_id, ui_widgets); @@ -1487,6 +1510,10 @@ impl Hud { &mut ui_widgets.widget_id_generator(), ); + let overitem_properties = overitem::OveritemProperties { + active: true, + pickup_failed_pulse: self.failed_block_pickups.get(&pos).copied(), + }; let pos = pos.map(|e| e as f32 + 0.5); let over_pos = pos + Vec3::unit_z() * 0.7; @@ -1497,8 +1524,10 @@ impl Hud { overitem::TEXT_COLOR, pos.distance_squared(player_pos), &self.fonts, + &i18n, &global_state.settings.controls, - true, + overitem_properties, + self.pulse, &global_state.window.key_layout, ) .x_y(0.0, 100.0) @@ -1509,7 +1538,7 @@ impl Hud { &item, over_pos, pos.distance_squared(player_pos), - true, + overitem_properties, &self.fonts, ) .set(overitem_id, ui_widgets); @@ -1520,8 +1549,10 @@ impl Hud { overitem::TEXT_COLOR, pos.distance_squared(player_pos), &self.fonts, + &i18n, &global_state.settings.controls, - true, + overitem_properties, + self.pulse, &global_state.window.key_layout, ) .x_y(0.0, 100.0) @@ -2646,6 +2677,24 @@ impl Hud { self.new_messages = VecDeque::new(); self.new_notifications = VecDeque::new(); + //Loot + LootScroller::new( + &mut self.new_loot_messages, + client, + &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 @@ -3201,6 +3250,18 @@ impl Hud { events } + pub fn add_failed_block_pickup(&mut self, pos: Vec3) { + self.failed_block_pickups.insert(pos, self.pulse); + } + + pub fn add_failed_entity_pickup(&mut self, entity: EcsEntity) { + self.failed_entity_pickups.insert(entity, self.pulse); + } + + 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); } @@ -3443,7 +3504,8 @@ impl Hud { // When a player closes all menus, resets the cursor // to the center of the screen - self.show.toggle_cursor_on_menu_close(global_state); + self.show + .toggle_cursor_on_menu_close(global_state, &mut self.ui); matching_key }, // Else the player is typing in chat diff --git a/voxygen/src/hud/overitem.rs b/voxygen/src/hud/overitem.rs index 07fbb02b03..0b941ff9d8 100644 --- a/voxygen/src/hud/overitem.rs +++ b/voxygen/src/hud/overitem.rs @@ -1,17 +1,21 @@ use crate::{ + i18n::Localization, settings::ControlSettings, ui::{fonts::Fonts, Ingameable}, window::GameInput, }; use conrod_core::{ + color, widget::{self, RoundedRectangle, Text}, widget_ids, Color, Colorable, Positionable, Widget, WidgetCommon, }; use std::borrow::Cow; -pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0); use keyboard_keynames::key_layout::KeyLayout; +pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0); +pub const PICKUP_FAILED_FADE_OUT_TIME: f32 = 1.5; + widget_ids! { struct Ids { // Name @@ -20,6 +24,9 @@ widget_ids! { // Key btn_bg, btn, + // Inventory full + inv_full_bg, + inv_full, } } @@ -31,10 +38,12 @@ pub struct Overitem<'a> { quality: Color, distance_from_player_sqr: f32, fonts: &'a Fonts, + localized_strings: &'a Localization, controls: &'a ControlSettings, #[conrod(common_builder)] common: widget::CommonBuilder, - active: bool, + properties: OveritemProperties, + pulse: f32, key_layout: &'a Option, } @@ -44,8 +53,10 @@ impl<'a> Overitem<'a> { quality: Color, distance_from_player_sqr: f32, fonts: &'a Fonts, + localized_strings: &'a Localization, controls: &'a ControlSettings, - active: bool, + properties: OveritemProperties, + pulse: f32, key_layout: &'a Option, ) -> Self { Self { @@ -53,14 +64,21 @@ impl<'a> Overitem<'a> { quality, distance_from_player_sqr, fonts, + localized_strings, controls, common: widget::CommonBuilder::default(), - active, + properties, + pulse, key_layout, } } } +pub struct OveritemProperties { + pub active: bool, + pub pickup_failed_pulse: Option, +} + pub struct State { ids: Ids, } @@ -74,10 +92,14 @@ impl<'a> Ingameable for Overitem<'a> { 2 + match self .controls .get_binding(GameInput::Interact) - .filter(|_| self.active) + .filter(|_| self.properties.active) { Some(_) => 2, None => 0, + } + if self.properties.pickup_failed_pulse.is_some() { + 2 + } else { + 0 } } } @@ -114,14 +136,19 @@ impl<'a> Widget for Overitem<'a> { // * 20.0) // .into(); let scale = 30.0; + let text_font_size = scale * 1.0; let text_pos_y = scale * 1.2; + let btn_rect_size = scale * 0.8; let btn_font_size = scale * 0.6; let btn_rect_pos_y = 0.0; let btn_text_pos_y = btn_rect_pos_y + ((btn_rect_size - btn_font_size) * 0.5); let btn_radius = btn_rect_size / 5.0; + let inv_full_font_size = scale * 1.0; + let inv_full_pos_y = scale * 2.4; + // Item Name Text::new(&self.name) .font_id(self.fonts.cyri.conrod_id) @@ -144,7 +171,7 @@ impl<'a> Widget for Overitem<'a> { if let Some(key_button) = self .controls .get_binding(GameInput::Interact) - .filter(|_| self.active) + .filter(|_| self.properties.active) { RoundedRectangle::fill_with([btn_rect_size, btn_rect_size], btn_radius, btn_color) .x_y(0.0, btn_rect_pos_y) @@ -160,5 +187,34 @@ impl<'a> Widget for Overitem<'a> { .parent(id) .set(state.ids.btn, ui); } + if let Some(time) = self.properties.pickup_failed_pulse { + //should never exceed 1.0, but just in case + let age = ((self.pulse - time) / PICKUP_FAILED_FADE_OUT_TIME).clamp(0.0, 1.0); + + let alpha = 1.0 - age.powi(4); + let brightness = 1.0 / (age / 0.07 - 1.0).abs().clamp(0.01, 1.0); + let shade_color = |color: Color| { + let color::Hsla(hue, sat, lum, alp) = color.to_hsl(); + color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha) + }; + + Text::new(self.localized_strings.get("hud.inventory_full")) + .font_id(self.fonts.cyri.conrod_id) + .font_size(inv_full_font_size as u32) + .color(shade_color(Color::Rgba(0.0, 0.0, 0.0, 1.0))) + .x_y(-1.0, inv_full_pos_y - 2.0) + .parent(id) + .depth(self.distance_from_player_sqr + 6.0) + .set(state.ids.inv_full_bg, ui); + + Text::new(self.localized_strings.get("hud.inventory_full")) + .font_id(self.fonts.cyri.conrod_id) + .font_size(inv_full_font_size as u32) + .color(shade_color(Color::Rgba(1.0, 0.0, 0.0, 1.0))) + .x_y(0.0, inv_full_pos_y) + .parent(id) + .depth(self.distance_from_player_sqr + 5.0) + .set(state.ids.inv_full, ui); + } } } diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 86a33f669a..479fc9394e 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -1,19 +1,20 @@ pub mod settings_change; -use std::{cell::RefCell, collections::HashSet, rc::Rc, time::Duration}; +use std::{cell::RefCell, collections::HashSet, rc::Rc, result::Result, sync::Arc, time::Duration}; use ordered_float::OrderedFloat; use specs::{Join, WorldExt}; -use tracing::{error, info}; +use tracing::{error, info, warn}; use vek::*; use client::{self, Client}; use common::{ + assets::AssetExt, comp, comp::{ inventory::slot::{EquipSlot, Slot}, invite::InviteKind, - item::{tool::ToolKind, ItemDesc}, + item::{tool::ToolKind, ItemDef, ItemDesc}, ChatMsg, ChatType, InputKind, InventoryUpdateEvent, Pos, Vel, }, consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE}, @@ -34,7 +35,7 @@ use common_net::{ use crate::{ audio::sfx::SfxEvent, - hud::{DebugInfo, Event as HudEvent, Hud, HudInfo, PromptDialogSettings}, + hud::{DebugInfo, Event as HudEvent, Hud, HudInfo, LootMessage, PromptDialogSettings}, key_state::KeyState, menu::char_selection::CharSelectionState, render::Renderer, @@ -179,22 +180,31 @@ impl SessionState { let sfx_trigger_item = sfx_triggers.get_key_value(&SfxEvent::from(&inv_event)); global_state.audio.emit_sfx_item(sfx_trigger_item); - let i18n = global_state.i18n.read(); - match inv_event { - InventoryUpdateEvent::CollectFailed => { - self.hud.new_message(ChatMsg { - message: i18n.get("hud.chat.loot_fail").to_string(), - chat_type: ChatType::CommandError, - }); + InventoryUpdateEvent::BlockCollectFailed(pos) => { + self.hud.add_failed_block_pickup(pos); + }, + InventoryUpdateEvent::EntityCollectFailed(uid) => { + if let Some(entity) = client.state().ecs().entity_from_uid(uid.into()) { + self.hud.add_failed_entity_pickup(entity); + } }, InventoryUpdateEvent::Collected(item) => { - self.hud.new_message(ChatMsg { - message: i18n - .get("hud.chat.loot_msg") - .replace("{item}", item.name()), - chat_type: ChatType::Loot, - }); + match Arc::::load_cloned(item.item_definition_id()) { + Result::Ok(item_def) => { + self.hud.new_loot_message(LootMessage { + item: item_def, + amount: item.amount(), + }); + }, + Result::Err(e) => { + warn!( + ?e, + "Item not present on client: {}", + item.item_definition_id() + ); + }, + } }, _ => {}, }; diff --git a/voxygen/src/settings/chat.rs b/voxygen/src/settings/chat.rs index 74a7758d79..3df61e1964 100644 --- a/voxygen/src/settings/chat.rs +++ b/voxygen/src/settings/chat.rs @@ -44,7 +44,6 @@ impl ChatFilter { ChatType::NpcSay(..) => true, ChatType::NpcTell(..) => true, ChatType::Meta => true, - ChatType::Loot => true, } } }