From 0a723614f940d0e5bcbdbe72001906b89718cacc Mon Sep 17 00:00:00 2001 From: CapsizeGlimmer <5827660-CapsizeGlimmer@users.noreply.gitlab.com> Date: Thu, 14 May 2020 16:56:10 +0000 Subject: [PATCH] Popup message when setting waypoint --- CHANGELOG.md | 1 + assets/voxygen/i18n/en.ron | 1 + chat-cli/src/main.rs | 1 + client/src/lib.rs | 9 +- common/src/msg/mod.rs | 4 +- common/src/msg/server.rs | 7 ++ server/src/cmd.rs | 5 +- server/src/sys/waypoint.rs | 25 +++- voxygen/src/hud/minimap.rs | 60 +--------- voxygen/src/hud/mod.rs | 17 +++ voxygen/src/hud/popup.rs | 233 +++++++++++++++++++++++++++++++++++++ voxygen/src/session.rs | 6 +- 12 files changed, 300 insertions(+), 69 deletions(-) create mode 100644 voxygen/src/hud/popup.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a208eb03f8..25dd9ec1cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Player now starts with a lantern. Equipping/unequipping a lantern has the same effect as the `/lantern` command - Added tab completion in chat for player names and chat commands - Added server persistence for character stats +- Added a popup when setting your character's waypoint ### Changed diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 86977c795a..6d7ed5d254 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -147,6 +147,7 @@ https://account.veloren.net."#, "hud.show_tips": "Show Tips", "hud.quests": "Quests", "hud.you_died": "You Died", + "hud.waypoint_saved": "Waypoint Saved", "hud.press_key_to_show_keybindings_fmt": "Press {key} to show keybindings", "hud.press_key_to_show_debug_info_fmt": "Press {key} to show debug info", diff --git a/chat-cli/src/main.rs b/chat-cli/src/main.rs index 312094231c..bd01bcfc71 100644 --- a/chat-cli/src/main.rs +++ b/chat-cli/src/main.rs @@ -93,6 +93,7 @@ fn main() { println!("{}", message) }, + Event::Notification(_) => {}, // TODO? } } diff --git a/client/src/lib.rs b/client/src/lib.rs index ebcb638db9..6bb713dd93 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -22,8 +22,9 @@ use common::{ }, event::{EventBus, SfxEvent, SfxEventItem}, msg::{ - validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, PlayerListUpdate, - RegisterError, RequestStateError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG, + validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, Notification, + PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, + MAX_BYTES_CHAT_MSG, }, net::PostBox, state::State, @@ -59,6 +60,7 @@ pub enum Event { }, Disconnect, DisconnectionNotification(u64), + Notification(Notification), } pub struct Client { @@ -865,6 +867,9 @@ impl Client { warn!("CharacterActionError: {:?}.", error); self.character_list.error = Some(error); }, + ServerMsg::Notification(n) => { + frontend_events.push(Event::Notification(n)); + }, } } } else if let Some(err) = self.postbox.error() { diff --git a/common/src/msg/mod.rs b/common/src/msg/mod.rs index 1c04fece94..c045383dd0 100644 --- a/common/src/msg/mod.rs +++ b/common/src/msg/mod.rs @@ -6,7 +6,9 @@ pub mod server; pub use self::{ client::ClientMsg, ecs_packet::EcsCompPacket, - server::{PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg}, + server::{ + Notification, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, + }, }; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index d857b74755..ae0a3a4aa8 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -26,6 +26,11 @@ pub enum PlayerListUpdate { Alias(u64, String), } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Notification { + WaypointSaved, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ServerMsg { InitialSync { @@ -64,6 +69,8 @@ pub enum ServerMsg { Disconnect, Shutdown, TooManyPlayers, + /// Send a popup notification such as "Waypoint Saved" + Notification(Notification), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 38f405198e..8f421902dc 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -9,7 +9,7 @@ use common::{ cmd::{ChatCommand, CHAT_COMMANDS}, comp, event::{EventBus, ServerEvent}, - msg::{PlayerListUpdate, ServerMsg}, + msg::{Notification, PlayerListUpdate, ServerMsg}, npc::{self, get_npc_name}, state::TimeOfDay, sync::{Uid, WorldSyncExt}, @@ -838,7 +838,8 @@ fn handle_waypoint( .ecs() .write_storage::() .insert(target, comp::Waypoint::new(pos.0)); - server.notify_client(client, ServerMsg::private(String::from("Waypoint set!"))); + server.notify_client(client, ServerMsg::private(String::from("Waypoint saved!"))); + server.notify_client(client, ServerMsg::Notification(Notification::WaypointSaved)); }, None => server.notify_client( client, diff --git a/server/src/sys/waypoint.rs b/server/src/sys/waypoint.rs index 6234b95d22..19abe9572e 100644 --- a/server/src/sys/waypoint.rs +++ b/server/src/sys/waypoint.rs @@ -1,7 +1,13 @@ use super::SysTimer; -use common::comp::{Player, Pos, Waypoint, WaypointArea}; +use crate::client::Client; +use common::{ + comp::{Player, Pos, Waypoint, WaypointArea}, + msg::{Notification, ServerMsg}, +}; use specs::{Entities, Join, ReadStorage, System, Write, WriteStorage}; +const NOTIFY_DISTANCE: f32 = 10.0; + /// This system updates player waypoints /// TODO: Make this faster by only considering local waypoints pub struct Sys; @@ -12,19 +18,28 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Player>, ReadStorage<'a, WaypointArea>, WriteStorage<'a, Waypoint>, + WriteStorage<'a, Client>, Write<'a, SysTimer>, ); fn run( &mut self, - (entities, positions, players, waypoint_areas, mut waypoints, mut timer): Self::SystemData, + (entities, positions, players, waypoint_areas, mut waypoints, mut clients, mut timer): Self::SystemData, ) { timer.start(); - for (entity, player_pos, _) in (&entities, &positions, &players).join() { + for (entity, player_pos, _, client) in + (&entities, &positions, &players, &mut clients).join() + { for (waypoint_pos, waypoint_area) in (&positions, &waypoint_areas).join() { - if player_pos.0.distance_squared(waypoint_pos.0) < waypoint_area.radius().powf(2.0) - { + if player_pos.0.distance_squared(waypoint_pos.0) < waypoint_area.radius().powi(2) { + if let Some(wp) = waypoints.get(entity) { + if player_pos.0.distance_squared(wp.get_pos()) > NOTIFY_DISTANCE.powi(2) { + client + .postbox + .send_message(ServerMsg::Notification(Notification::WaypointSaved)); + } + } let _ = waypoints.insert(entity, Waypoint::new(player_pos.0)); } } diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 484fd6a1a1..6ba0ca821d 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -1,6 +1,6 @@ use super::{ img_ids::{Imgs, ImgsRot}, - Show, HP_COLOR, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, + Show, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, }; use crate::ui::{fonts::ConrodVoxygenFonts, img_ids}; use client::{self, Client}; @@ -8,10 +8,9 @@ use common::{comp, terrain::TerrainChunkSize, vol::RectVolSize}; use conrod_core::{ color, position, widget::{self, Button, Image, Rectangle, Text}, - widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, + widget_ids, Colorable, Positionable, Sizeable, Widget, WidgetCommon, }; use specs::WorldExt; -use std::time::{Duration, Instant}; use vek::*; widget_ids! { @@ -68,8 +67,6 @@ impl<'a> MiniMap<'a> { pub struct State { ids: Ids, - last_region_name: Option, - last_update: Instant, zoom: f64, } @@ -86,8 +83,6 @@ impl<'a> Widget for MiniMap<'a> { State { ids: Ids::new(id_gen), - last_region_name: None, - last_update: Instant::now(), zoom: { let min_world_dim = self.world_map.1.reduce_partial_min() as f64; min_world_dim.min( @@ -250,57 +245,6 @@ impl<'a> Widget for MiniMap<'a> { return Some(Event::Toggle); } - // Display zone name on entry - - const FADE_IN: f32 = 0.5; - const FADE_HOLD: f32 = 1.0; - const FADE_OUT: f32 = 3.0; - match self.client.current_chunk() { - Some(chunk) => { - let current = chunk.meta().name(); - // Check if no other popup is displayed and a new one is needed - if state.last_update.elapsed() - > Duration::from_secs_f32(FADE_IN + FADE_HOLD + FADE_OUT) - && state - .last_region_name - .as_ref() - .map(|l| l != current) - .unwrap_or(true) - { - // Update last_region - state.update(|s| s.last_region_name = Some(current.to_owned())); - state.update(|s| s.last_update = Instant::now()); - } - - let seconds = state.last_update.elapsed().as_secs_f32(); - let fade = if seconds < FADE_IN { - seconds / FADE_IN - } else if seconds < FADE_IN + FADE_HOLD { - 1.0 - } else { - (1.0 - (seconds - FADE_IN - FADE_HOLD) / FADE_OUT).max(0.0) - }; - // Region Name - Text::new(state.last_region_name.as_ref().unwrap_or(&"".to_owned())) - .mid_top_with_margin_on(ui.window, 200.0) - .font_size(self.fonts.alkhemi.scale(70)) - .font_id(self.fonts.alkhemi.conrod_id) - .color(Color::Rgba(0.0, 0.0, 0.0, fade)) - .set(state.ids.zone_display_bg, ui); - Text::new(state.last_region_name.as_ref().unwrap_or(&"".to_owned())) - .top_left_with_margins_on(state.ids.zone_display_bg, -2.5, -2.5) - .font_size(self.fonts.alkhemi.scale(70)) - .font_id(self.fonts.alkhemi.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, fade)) - .set(state.ids.zone_display, ui); - }, - None => Text::new(" ") - .middle_of(ui.window) - .font_size(self.fonts.alkhemi.scale(14)) - .color(HP_COLOR) - .set(state.ids.zone_display, ui), - } - // TODO: Subregion name display // Title diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index c47ea9cdae..74a63dba8a 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -7,6 +7,7 @@ mod img_ids; mod item_imgs; mod map; mod minimap; +mod popup; mod settings_window; mod skillbar; mod slots; @@ -26,6 +27,7 @@ use img_ids::Imgs; use item_imgs::ItemImgs; use map::Map; use minimap::MiniMap; +use popup::Popup; use serde::{Deserialize, Serialize}; use settings_window::{SettingsTab, SettingsWindow}; use skillbar::Skillbar; @@ -174,6 +176,7 @@ widget_ids! { map, world_map, character_window, + popup, minimap, bag, social, @@ -1641,6 +1644,10 @@ impl Hud { None => {}, } + // Popup + Popup::new(&self.voxygen_i18n, client, &self.new_messages, &self.fonts) + .set(self.ids.popup, ui_widgets); + // MiniMap match MiniMap::new( &self.show, @@ -1734,6 +1741,16 @@ impl Hud { .set(self.ids.skillbar, ui_widgets); } + // The chat box breaks if it has non-chat messages left in the queue, so take + // them out. + self.new_messages.retain(|msg| { + if let ClientEvent::Chat { .. } = &msg { + true + } else { + false + } + }); + // Chat box match Chat::new( &mut self.new_messages, diff --git a/voxygen/src/hud/popup.rs b/voxygen/src/hud/popup.rs new file mode 100644 index 0000000000..b2dc72e8fc --- /dev/null +++ b/voxygen/src/hud/popup.rs @@ -0,0 +1,233 @@ +use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; +use client::{self, Client, Event as ClientEvent}; +use common::msg::Notification; +use conrod_core::{ + widget::{self, Text}, + widget_ids, Color, Colorable, Positionable, Widget, WidgetCommon, +}; +use std::{collections::VecDeque, time::Instant}; + +widget_ids! { + struct Ids { + error_bg, + error_text, + info_bg, + info_text, + message_bg, + message_text, + } +} + +#[derive(WidgetCommon)] +pub struct Popup<'a> { + voxygen_i18n: &'a std::sync::Arc, + client: &'a Client, + new_messages: &'a VecDeque, + fonts: &'a ConrodVoxygenFonts, + #[conrod(common_builder)] + common: widget::CommonBuilder, +} + +/// Popup notifications for messages such as , Waypoint Saved, +/// Dungeon Cleared (TODO), and Quest Completed (TODO) +impl<'a> Popup<'a> { + pub fn new( + voxygen_i18n: &'a std::sync::Arc, + client: &'a Client, + new_messages: &'a VecDeque, + fonts: &'a ConrodVoxygenFonts, + ) -> Self { + Self { + voxygen_i18n, + client, + new_messages, + fonts, + common: widget::CommonBuilder::default(), + } + } +} + +pub struct State { + ids: Ids, + errors: VecDeque, + infos: VecDeque, + messages: VecDeque, + last_error_update: Instant, + last_info_update: Instant, + last_message_update: Instant, + last_region_name: Option, +} + +impl<'a> Widget for Popup<'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), + errors: VecDeque::new(), + infos: VecDeque::new(), + messages: VecDeque::new(), + last_error_update: Instant::now(), + last_info_update: Instant::now(), + last_message_update: Instant::now(), + last_region_name: None, + } + } + + fn style(&self) -> Self::Style { () } + + fn update(self, args: widget::UpdateArgs) -> Self::Event { + let widget::UpdateArgs { state, ui, .. } = args; + + const FADE_IN: f32 = 0.5; + const FADE_HOLD: f32 = 1.0; + const FADE_OUT: f32 = 3.0; + + let bg_color = |fade| Color::Rgba(0.0, 0.0, 0.0, fade); + let error_color = |fade| Color::Rgba(1.0, 0.0, 0.0, fade); + let info_color = |fade| Color::Rgba(1.0, 1.0, 0.0, fade); + let message_color = |fade| Color::Rgba(1.0, 1.0, 1.0, fade); + + // Push chunk name to message queue + if let Some(chunk) = self.client.current_chunk() { + let current = chunk.meta().name(); + // Check if no other popup is displayed and a new one is needed + if state.messages.is_empty() + && state + .last_region_name + .as_ref() + .map(|l| l != current) + .unwrap_or(true) + { + // Update last_region + state.update(|s| { + if s.messages.is_empty() { + s.last_message_update = Instant::now(); + } + s.last_region_name = Some(current.to_owned()); + s.messages.push_back(current.to_owned()); + }); + } + } + + // Push waypoint to message queue + for notification in self.new_messages { + match notification { + ClientEvent::Notification(Notification::WaypointSaved) => { + state.update(|s| { + if s.infos.is_empty() { + s.last_info_update = Instant::now(); + } + let text = self.voxygen_i18n.get("hud.waypoint_saved"); + s.infos.push_back(text.to_string()); + }); + }, + _ => {}, + } + } + + // Get next error from queue + if !state.errors.is_empty() + && state.last_error_update.elapsed().as_secs_f32() > FADE_IN + FADE_HOLD + FADE_OUT + { + state.update(|s| { + s.errors.pop_front(); + s.last_error_update = Instant::now(); + }); + } + + // Display error as popup + if let Some(error) = state.errors.front() { + let seconds = state.last_error_update.elapsed().as_secs_f32(); + let fade = if seconds < FADE_IN { + seconds / FADE_IN + } else if seconds < FADE_IN + FADE_HOLD { + 1.0 + } else { + (1.0 - (seconds - FADE_IN - FADE_HOLD) / FADE_OUT).max(0.0) + }; + Text::new(error) + .mid_top_with_margin_on(ui.window, 50.0) + .font_size(self.fonts.cyri.scale(20)) + .font_id(self.fonts.cyri.conrod_id) + .color(bg_color(fade)) + .set(state.ids.error_bg, ui); + Text::new(error) + .top_left_with_margins_on(state.ids.error_bg, -1.0, -1.0) + .font_size(self.fonts.cyri.scale(20)) + .font_id(self.fonts.cyri.conrod_id) + .color(error_color(fade)) + .set(state.ids.error_text, ui); + } + + // Get next info from queue + if !state.infos.is_empty() + && state.last_info_update.elapsed().as_secs_f32() > FADE_IN + FADE_HOLD + FADE_OUT + { + state.update(|s| { + s.infos.pop_front(); + s.last_info_update = Instant::now(); + }); + } + + // Display info as popup + if let Some(info) = state.infos.front() { + let seconds = state.last_info_update.elapsed().as_secs_f32(); + let fade = if seconds < FADE_IN { + seconds / FADE_IN + } else if seconds < FADE_IN + FADE_HOLD { + 1.0 + } else { + (1.0 - (seconds - FADE_IN - FADE_HOLD) / FADE_OUT).max(0.0) + }; + Text::new(info) + .mid_top_with_margin_on(ui.window, 100.0) + .font_size(self.fonts.cyri.scale(20)) + .font_id(self.fonts.cyri.conrod_id) + .color(bg_color(fade)) + .set(state.ids.info_bg, ui); + Text::new(info) + .top_left_with_margins_on(state.ids.info_bg, -1.0, -1.0) + .font_size(self.fonts.cyri.scale(20)) + .font_id(self.fonts.cyri.conrod_id) + .color(info_color(fade)) + .set(state.ids.info_text, ui); + } + + // Get next message from queue + if !state.messages.is_empty() + && state.last_message_update.elapsed().as_secs_f32() > FADE_IN + FADE_HOLD + FADE_OUT + { + state.update(|s| { + s.messages.pop_front(); + s.last_message_update = Instant::now(); + }); + } + + // Display message as popup + if let Some(message) = state.messages.front() { + let seconds = state.last_message_update.elapsed().as_secs_f32(); + let fade = if seconds < FADE_IN { + seconds / FADE_IN + } else if seconds < FADE_IN + FADE_HOLD { + 1.0 + } else { + (1.0 - (seconds - FADE_IN - FADE_HOLD) / FADE_OUT).max(0.0) + }; + Text::new(message) + .mid_top_with_margin_on(ui.window, 200.0) + .font_size(self.fonts.alkhemi.scale(70)) + .font_id(self.fonts.alkhemi.conrod_id) + .color(bg_color(fade)) + .set(state.ids.message_bg, ui); + Text::new(message) + .top_left_with_margins_on(state.ids.message_bg, -2.5, -2.5) + .font_size(self.fonts.alkhemi.scale(70)) + .font_id(self.fonts.alkhemi.conrod_id) + .color(message_color(fade)) + .set(state.ids.message_text, ui); + } + } +} diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 49029e0e1b..80f0d57d1b 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -15,7 +15,7 @@ use common::{ clock::Clock, comp, comp::{Pos, Vel, MAX_PICKUP_RANGE_SQR}, - msg::ClientState, + msg::{ClientState, Notification}, terrain::{Block, BlockKind}, util::Dir, vol::ReadVol, @@ -102,6 +102,10 @@ impl SessionState { message, }); }, + client::Event::Notification(Notification::WaypointSaved) => { + self.hud + .new_message(client::Event::Notification(Notification::WaypointSaved)); + }, } }