diff --git a/assets/voxygen/element/buttons/group.png b/assets/voxygen/element/buttons/group.png new file mode 100644 index 0000000000..e059af9154 --- /dev/null +++ b/assets/voxygen/element/buttons/group.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55e113c52f38efe8fc5dcea32099f8821cc1c6b72810244412d8d4398ae36cac +size 2152 diff --git a/assets/voxygen/element/buttons/group_hover.png b/assets/voxygen/element/buttons/group_hover.png new file mode 100644 index 0000000000..c405b87a66 --- /dev/null +++ b/assets/voxygen/element/buttons/group_hover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86380af53936c13c354d13616e662d9e25ca115d7857b883fda74a6cb7f57f74 +size 2243 diff --git a/assets/voxygen/element/buttons/group_press.png b/assets/voxygen/element/buttons/group_press.png new file mode 100644 index 0000000000..0fd69da245 --- /dev/null +++ b/assets/voxygen/element/buttons/group_press.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dda830676b251480e41ffe3cbd85a0531893bb299a429b0d26dce9edd5cdccc6 +size 2253 diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index 8052ecf4b7..a52a535254 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -65,6 +65,7 @@ VoxygenLocalization( "common.create": "Create", "common.okay": "Okay", "common.accept": "Accept", + "common.reject": "Reject", "common.disclaimer": "Disclaimer", "common.cancel": "Cancel", "common.none": "None", @@ -319,7 +320,14 @@ magically infused items?"#, "hud.crafting.craft": "Craft", "hud.crafting.tool_cata": "Requires:", - "hud.spell": "Spells", + "hud.group": "Group", + "hud.group.invite_to_join": "{name} invited you to their group.", + "hud.group.invite": "Invite", + "hud.group.kick": "Kick", + "hud.group.assign_leader": "Assign Leader", + "hud.group.leave": "Leave Group", + + "hud.spell": "Spells", "hud.free_look_indicator": "Free look active", "hud.auto_walk_indicator": "Auto walk active", diff --git a/client/src/lib.rs b/client/src/lib.rs index 08b10a84e6..4347b9af19 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -383,7 +383,7 @@ impl Client { } pub fn pick_up(&mut self, entity: EcsEntity) { - if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { + if let Some(uid) = self.state.read_component_copied(entity) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::InventoryManip( InventoryManip::Pickup(uid), @@ -436,6 +436,12 @@ impl Client { pub fn group_leader(&self) -> Option { self.group_leader } + pub fn send_group_invite(&mut self, invitee: Uid) { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( GroupManip::Invite(invitee) ))) + .unwrap() + } + pub fn accept_group_invite(&mut self) { // Clear invite self.group_invite.take(); @@ -461,22 +467,18 @@ impl Client { ))).unwrap(); } - pub fn kick_from_group(&mut self, entity: specs::Entity) { - if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { - self.singleton_stream - .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( - GroupManip::Kick(uid), - ))).unwrap(); - } + pub fn kick_from_group(&mut self, uid: Uid) { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::Kick(uid), + ))).unwrap(); } - pub fn assign_group_leader(&mut self, entity: specs::Entity) { - if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { - self.singleton_stream - .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( - GroupManip::AssignLeader(uid), - ))).unwrap(); - } + pub fn assign_group_leader(&mut self, uid: Uid) { + self.singleton_stream + .send(ClientMsg::ControlEvent(ControlEvent::GroupManip( + GroupManip::AssignLeader(uid), + ))).unwrap(); } pub fn is_mounted(&self) -> bool { @@ -488,7 +490,7 @@ impl Client { } pub fn mount(&mut self, entity: EcsEntity) { - if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { + if let Some(uid) = self.state.read_component_copied(entity) { self.singleton_stream .send(ClientMsg::ControlEvent(ControlEvent::Mount(uid))) .unwrap(); @@ -1069,7 +1071,7 @@ impl Client { self.state.ecs_mut().apply_entity_package(entity_package); }, ServerMsg::DeleteEntity(entity) => { - if self.state.read_component_cloned::(self.entity) != Some(entity) { + if self.uid() != Some(entity) { self.state .ecs_mut() .delete_entity_and_clear_from_uid_allocator(entity.0); @@ -1179,6 +1181,11 @@ impl Client { /// Get the player's entity. pub fn entity(&self) -> EcsEntity { self.entity } + /// Get the player's Uid. + pub fn uid(&self) -> Option { + self.state.read_component_copied(self.entity) + } + /// Get the client state pub fn get_client_state(&self) -> ClientState { self.client_state } @@ -1230,7 +1237,7 @@ impl Client { pub fn is_admin(&self) -> bool { let client_uid = self .state - .read_component_cloned::(self.entity) + .read_component_copied::(self.entity) .expect("Client doesn't have a Uid!!!"); self.player_list @@ -1241,8 +1248,7 @@ impl Client { /// Clean client ECS state fn clean_state(&mut self) { let client_uid = self - .state - .read_component_cloned::(self.entity) + .uid() .map(|u| u.into()) .expect("Client doesn't have a Uid!!!"); @@ -1313,7 +1319,7 @@ impl Client { comp::ChatType::Tell(from, to) => { let from_alias = alias_of_uid(from); let to_alias = alias_of_uid(to); - if Some(from) == self.state.ecs().read_storage::().get(self.entity) { + if Some(*from) == self.uid() { format!("To [{}]: {}", to_alias, message) } else { format!("From [{}]: {}", from_alias, message) diff --git a/common/src/comp/group.rs b/common/src/comp/group.rs index 339919a9dc..99f2167cc3 100644 --- a/common/src/comp/group.rs +++ b/common/src/comp/group.rs @@ -130,6 +130,12 @@ impl GroupManager { uids: &Uids, mut notifier: impl FnMut(specs::Entity, ChangeNotification), ) { + // Ensure leader is not inviting themselves + if leader == new_member { + warn!("Attempt to form group with leader as the only member (this is disallowed)"); + return; + } + // Get uid let new_member_uid = if let Some(uid) = uids.get(new_member) { *uid diff --git a/common/src/state.rs b/common/src/state.rs index c4aca3a85d..9d5a992e04 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -198,8 +198,8 @@ impl State { } /// Read a component attributed to a particular entity. - pub fn read_component_cloned(&self, entity: EcsEntity) -> Option { - self.ecs.read_storage().get(entity).cloned() + pub fn read_component_copied(&self, entity: EcsEntity) -> Option { + self.ecs.read_storage().get(entity).copied() } /// Get a read-only reference to the storage of a particular component type. diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 821857507c..344f7c66b0 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -227,7 +227,7 @@ fn handle_jump( action: &ChatCommand, ) { if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) { - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(current_pos) => { server .state @@ -252,7 +252,7 @@ fn handle_goto( if let Ok((x, y, z)) = scan_fmt!(&args, &action.arg_fmt(), f32, f32, f32) { if server .state - .read_component_cloned::(target) + .read_component_copied::(target) .is_some() { server @@ -463,9 +463,9 @@ fn handle_tp( ); return; }; - if let Some(_pos) = server.state.read_component_cloned::(target) { + if let Some(_pos) = server.state.read_component_copied::(target) { if let Some(player) = opt_player { - if let Some(pos) = server.state.read_component_cloned::(player) { + if let Some(pos) = server.state.read_component_copied::(player) { server.state.write_component(target, pos); server.state.write_component(target, comp::ForceUpdate); } else { @@ -510,7 +510,7 @@ fn handle_spawn( (Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => { let uid = server .state - .read_component_cloned(target) + .read_component_copied(target) .expect("Expected player to have a UID"); if let Some(alignment) = parse_alignment(uid, &opt_align) { let amount = opt_amount @@ -521,7 +521,7 @@ fn handle_spawn( let ai = opt_ai.unwrap_or_else(|| "true".to_string()); - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { let agent = if let comp::Alignment::Owned(_) | comp::Alignment::Npc = alignment { @@ -630,7 +630,7 @@ fn handle_spawn_training_dummy( _args: String, _action: &ChatCommand, ) { - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { let vel = Vec3::new( rand::thread_rng().gen_range(-2.0, 3.0), @@ -997,7 +997,7 @@ fn handle_explosion( let ecs = server.state.ecs(); - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { ecs.read_resource::>() .emit_now(ServerEvent::Explosion { @@ -1020,7 +1020,7 @@ fn handle_waypoint( _args: String, _action: &ChatCommand, ) { - match server.state.read_component_cloned::(target) { + match server.state.read_component_copied::(target) { Some(pos) => { let time = server.state.ecs().read_resource(); let _ = server @@ -1056,7 +1056,7 @@ fn handle_adminify( Some(player) => { let is_admin = if server .state - .read_component_cloned::(player) + .read_component_copied::(player) .is_some() { ecs.write_storage::().remove(player); @@ -1662,7 +1662,7 @@ fn handle_remove_lights( action: &ChatCommand, ) { let opt_radius = scan_fmt_some!(&args, &action.arg_fmt(), f32); - let opt_player_pos = server.state.read_component_cloned::(target); + let opt_player_pos = server.state.read_component_copied::(target); let mut to_delete = vec![]; match opt_player_pos { diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 259cd87859..8033ccebdb 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -189,7 +189,7 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) { .is_some() { let respawn_point = state - .read_component_cloned::(entity) + .read_component_copied::(entity) .map(|wp| wp.get_pos()) .unwrap_or(state.ecs().read_resource::().0); diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs index 6c4d4a232c..2c6447541b 100644 --- a/server/src/events/group_manip.rs +++ b/server/src/events/group_manip.rs @@ -10,6 +10,7 @@ use common::{ sync::WorldSyncExt, }; use specs::world::WorldExt; +use tracing::warn; // TODO: turn chat messages into enums pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) { @@ -23,15 +24,26 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani None => { // Inform of failure if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg( - "Leadership transfer failed, target does not exist".to_owned(), - )); + client.notify( + ChatType::Meta + .server_msg("Invite failed, target does not exist".to_owned()), + ); } return; }, }; let uids = state.ecs().read_storage::(); + + // Check if entity is trying to invite themselves to a group + if uids + .get(entity) + .map_or(false, |inviter_uid| *inviter_uid == uid) + { + warn!("Entity tried to invite themselves into a group"); + return; + } + let alignments = state.ecs().read_storage::(); let agents = state.ecs().read_storage::(); let mut already_has_invite = false; @@ -54,10 +66,15 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani ) { add_to_group = true; // Wipe agent state + drop(agents); let _ = state .ecs() .write_storage() .insert(invitee, comp::Agent::default()); + } else { + if let Some(client) = clients.get_mut(entity) { + client.notify(ChatType::Meta.server_msg("Invite rejected".to_owned())); + } } if already_has_invite { @@ -76,7 +93,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani invitee, &state.ecs().entities(), &mut state.ecs().write_storage(), - &state.ecs().read_storage(), + &alignments, &uids, |entity, group_change| { clients @@ -163,9 +180,10 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani None => { // Inform of failure if let Some(client) = clients.get_mut(entity) { - client.notify(ChatType::Meta.server_msg( - "Leadership transfer failed, target does not exist".to_owned(), - )); + client.notify( + ChatType::Meta + .server_msg("Kick failed, target does not exist".to_owned()), + ); } return; }, diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index f265c36bea..e1d658fb04 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -167,10 +167,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv thrown_items.push(( *pos, state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), *kind, )); @@ -185,7 +185,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv state.read_storage::().get(entity) { let uid = state - .read_component_cloned(entity) + .read_component_copied(entity) .expect("Expected player to have a UID"); if ( &state.read_storage::(), @@ -339,7 +339,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv dropped_items.push(( *pos, state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), item, )); @@ -371,10 +371,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv for _ in 0..amount { dropped_items.push(( state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), state - .read_component_cloned::(entity) + .read_component_copied::(entity) .unwrap_or_default(), item.clone(), )); @@ -405,7 +405,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv + Vec3::unit_z() * 15.0 + Vec3::::zero().map(|_| rand::thread_rng().gen::() - 0.5) * 4.0; - let uid = state.read_component_cloned::(entity); + let uid = state.read_component_copied::(entity); let mut new_entity = state .create_object(Default::default(), match kind { diff --git a/server/src/events/player.rs b/server/src/events/player.rs index fa802968b5..1e777c1daf 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -20,7 +20,7 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { // Note: If other `ServerEvent`s are referring to this entity they will be // disrupted let maybe_client = state.ecs().write_storage::().remove(entity); - let maybe_uid = state.read_component_cloned::(entity); + let maybe_uid = state.read_component_copied::(entity); let maybe_player = state.ecs().write_storage::().remove(entity); let maybe_group = state .ecs() diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 24333d9bb3..cdce5d44d3 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -173,7 +173,7 @@ impl StateExt for State { self.write_component(entity, comp::CharacterState::default()); self.write_component( entity, - comp::Alignment::Owned(self.read_component_cloned(entity).unwrap()), + comp::Alignment::Owned(self.read_component_copied(entity).unwrap()), ); // Set the character id for the player @@ -213,7 +213,7 @@ impl StateExt for State { // Notify clients of a player list update let client_uid = self - .read_component_cloned::(entity) + .read_component_copied::(entity) .map(|u| u) .expect("Client doesn't have a Uid!!!"); diff --git a/voxygen/src/hud/buttons.rs b/voxygen/src/hud/buttons.rs index 8ae1f6b7c4..499242ef1b 100644 --- a/voxygen/src/hud/buttons.rs +++ b/voxygen/src/hud/buttons.rs @@ -41,7 +41,7 @@ widget_ids! { crafting_button_bg, crafting_text, crafting_text_bg, - + group_button, } } #[derive(WidgetCommon)] @@ -98,6 +98,7 @@ pub enum Event { ToggleSocial, ToggleSpell, ToggleCrafting, + ToggleGroup, } impl<'a> Widget for Buttons<'a> { @@ -360,6 +361,7 @@ impl<'a> Widget for Buttons<'a> { .color(TEXT_COLOR) .set(state.ids.spellbook_text, ui); } + // Crafting if Button::image(self.imgs.crafting_icon) .w_h(25.0, 25.0) @@ -396,6 +398,26 @@ impl<'a> Widget for Buttons<'a> { .color(TEXT_COLOR) .set(state.ids.crafting_text, ui); } + + // Group + if Button::image(self.imgs.group_icon) + .w_h(49.0, 26.0) + .bottom_left_with_margins_on(ui.window, 190.0, 10.0) + .hover_image(self.imgs.group_icon_hover) + .press_image(self.imgs.group_icon_press) + .with_tooltip( + self.tooltip_manager, + &localized_strings.get("hud.group"), + "", + &button_tooltip, + ) + .bottom_offset(TOOLTIP_UPSHIFT) + .set(state.ids.group_button, ui) + .was_clicked() + { + return Some(Event::ToggleGroup); + } + None } } diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs new file mode 100644 index 0000000000..24b43e360b --- /dev/null +++ b/voxygen/src/hud/group.rs @@ -0,0 +1,329 @@ +use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, TEXT_COLOR_GREY, UI_MAIN}; + +use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; +use client::{self, Client}; +use common::{ + comp::Stats, + sync::{Uid, WorldSyncExt}, +}; +use conrod_core::{ + color, + widget::{self, Button, Image, Rectangle, Scrollbar, Text}, + widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, +}; +use specs::WorldExt; +use std::time::Instant; + +widget_ids! { + pub struct Ids { + bg, + title, + close, + btn_bg, + btn_friend, + btn_leader, + btn_link, + btn_kick, + btn_leave, + members[], + invite_bubble, + bubble_frame, + btn_accept, + btn_decline, + // TEST + test_leader, + test_member1, + test_member2, + test_member3, + test_member4, + test_member5, + } +} + +pub struct State { + ids: Ids, + // Holds the time when selection is made since this selection can be overriden + // by selecting an entity in-game + selected_uid: Option<(Uid, Instant)>, + // Selected group member + selected_member: Option, +} + +#[derive(WidgetCommon)] +pub struct Group<'a> { + show: &'a Show, + client: &'a Client, + imgs: &'a Imgs, + fonts: &'a ConrodVoxygenFonts, + localized_strings: &'a std::sync::Arc, + + selected_entity: Option<(specs::Entity, Instant)>, + + #[conrod(common_builder)] + common: widget::CommonBuilder, +} + +impl<'a> Group<'a> { + pub fn new( + show: &'a Show, + client: &'a Client, + imgs: &'a Imgs, + fonts: &'a ConrodVoxygenFonts, + localized_strings: &'a std::sync::Arc, + selected_entity: Option<(specs::Entity, Instant)>, + ) -> Self { + Self { + show, + client, + imgs, + fonts, + localized_strings, + selected_entity, + common: widget::CommonBuilder::default(), + } + } +} + +pub enum Event { + Close, + Accept, + Reject, + Kick(Uid), + LeaveGroup, + AssignLeader(Uid), +} + +impl<'a> Widget for Group<'a> { + type Event = Vec; + type State = State; + type Style = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + Self::State { + ids: Ids::new(id_gen), + selected_uid: None, + selected_member: 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; + + let mut events = Vec::new(); + + let player_leader = true; + let in_group = true; + let open_invite = false; + + if in_group || open_invite { + // Frame + Rectangle::fill_with([220.0, 230.0], color::Color::Rgba(0.0, 0.0, 0.0, 0.8)) + .bottom_left_with_margins_on(ui.window, 220.0, 10.0) + .set(state.ids.bg, ui); + if open_invite { + // yellow animated border + } + } + + // Buttons + if in_group { + Text::new("Group Name") + .mid_top_with_margin_on(state.ids.bg, 2.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.title, ui); + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .top_right_with_margins_on(state.ids.bg, 30.0, 5.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("Add to Friends") + .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_friend, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label("Leave Group") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_leave, ui) + .was_clicked() + {}; + // Group leader functions + if player_leader { + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label("Assign Leader") + .label_color(TEXT_COLOR) // Grey when no player is selected + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_leader, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("Link Group") + .label_color(TEXT_COLOR_GREY) // Change this when the linking is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_link, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .mid_bottom_with_margin_on(state.ids.btn_link, -27.0) + .down_from(state.ids.btn_link, 5.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label("Kick") + .label_color(TEXT_COLOR) // Grey when no player is selected + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(10)) + .set(state.ids.btn_kick, ui) + .was_clicked() + {}; + } + // Group Members, only character names, cut long names when they exceed the button size + // TODO Insert loop here + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .top_left_with_margins_on(state.ids.bg, 30.0, 5.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Leader") // Grey when no player is selected + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_leader, ui) + .was_clicked() + { + //Select the Leader + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_leader, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member1, ui) + .was_clicked() + { + // Select the group member + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_member1, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member2, ui) + .was_clicked() + { + // Select the group member + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_member2, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member3, ui) + .was_clicked() + { + // Select the group member + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_member3, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member4, ui) + .was_clicked() + { + // Select the group member + }; + if Button::image(self.imgs.nothing) // if selected self.imgs.selection + .w_h(90.0, 22.0) + .down_from(state.ids.test_member4, 10.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press) + .label("Other Player") + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(12)) + .set(state.ids.test_member5, ui) + .was_clicked() + { + // Select the group member + }; + // Maximum of 6 Players/Npcs per Group + // Player pets count as group members, too. They are not counted into the maximum group size. + + } + if open_invite { + //self.show.group = true; Auto open group menu + Text::new("Player wants to invite you!") + .mid_top_with_margin_on(state.ids.bg, 20.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.title, ui); + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .bottom_left_with_margins_on(state.ids.bg, 15.0, 15.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("[U] Accept") + .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(15)) + .set(state.ids.btn_friend, ui) + .was_clicked() + {}; + if Button::image(self.imgs.button) + .w_h(90.0, 22.0) + .bottom_right_with_margins_on(state.ids.bg, 15.0, 15.0) + .hover_image(self.imgs.button) + .press_image(self.imgs.button) + .label("[I] Decline") + .label_color(TEXT_COLOR_GREY) // Change this when the friendslist is working + .label_font_id(self.fonts.cyri.conrod_id) + .label_font_size(self.fonts.cyri.scale(15)) + .set(state.ids.btn_friend, ui) + .was_clicked() + {}; + + } + events + } +} diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 240860dd6e..afb6c54b3f 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -74,6 +74,8 @@ image_ids! { crafting_icon_hover: "voxygen.element.buttons.anvil_hover", crafting_icon_press: "voxygen.element.buttons.anvil_press", + // Group Window + // Chat-Arrows chat_arrow: "voxygen.element.buttons.arrow_down", @@ -94,7 +96,6 @@ image_ids! { slider_indicator_small: "voxygen.element.slider.indicator_round", // Buttons - settings: "voxygen.element.buttons.settings", settings_hover: "voxygen.element.buttons.settings_hover", settings_press: "voxygen.element.buttons.settings_press", @@ -111,6 +112,10 @@ image_ids! { spellbook_hover: "voxygen.element.buttons.spellbook_hover", spellbook_press: "voxygen.element.buttons.spellbook_press", + group_icon: "voxygen.element.buttons.group", + group_icon_hover: "voxygen.element.buttons.group_hover", + group_icon_press: "voxygen.element.buttons.group_press", + // Skill Icons twohsword_m1: "voxygen.element.icons.2hsword_m1", twohsword_m2: "voxygen.element.icons.2hsword_m2", diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index cd23a0223f..93caf26a22 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -3,6 +3,7 @@ mod buttons; mod chat; mod crafting; mod esc_menu; +mod group; mod hotbar; mod img_ids; mod item_imgs; @@ -30,6 +31,7 @@ use chat::Chat; use chrono::NaiveTime; use crafting::Crafting; use esc_menu::EscMenu; +use group::Group; use img_ids::Imgs; use item_imgs::ItemImgs; use map::Map; @@ -69,7 +71,7 @@ const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 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_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5); const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4); //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); @@ -208,6 +210,7 @@ widget_ids! { social_window, crafting_window, settings_window, + group_window, // Free look indicator free_look_txt, @@ -243,7 +246,7 @@ pub struct HudInfo { pub is_aiming: bool, pub is_first_person: bool, pub target_entity: Option, - pub selected_entity: Option, + pub selected_entity: Option<(specs::Entity, std::time::Instant)>, } pub enum Event { @@ -299,6 +302,12 @@ pub enum Event { ChangeAutoWalkBehavior(PressBehavior), ChangeStopAutoWalkOnInput(bool), CraftRecipe(String), + InviteMember(common::sync::Uid), + AcceptInvite, + RejectInvite, + KickMember(common::sync::Uid), + LeaveGroup, + AssignLeader(common::sync::Uid), } // TODO: Are these the possible layouts we want? @@ -354,6 +363,7 @@ pub struct Show { bag: bool, social: bool, spell: bool, + group: bool, esc_menu: bool, open_windows: Windows, map: bool, @@ -388,6 +398,11 @@ impl Show { } } + fn group(&mut self, open: bool) { + self.group = open; + self.want_grab = !open; + } + fn social(&mut self, open: bool) { if !self.esc_menu { self.social = open; @@ -416,6 +431,8 @@ impl Show { fn toggle_map(&mut self) { self.map(!self.map) } + fn toggle_group(&mut self) { self.group(!self.group) } + fn toggle_mini_map(&mut self) { self.mini_map = !self.mini_map; } fn settings(&mut self, open: bool) { @@ -600,6 +617,7 @@ impl Hud { ui: true, social: false, spell: false, + group: false, mini_map: true, settings_tab: SettingsTab::Interface, social_tab: SocialTab::Online, @@ -1050,7 +1068,7 @@ impl Hud { .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead && (stats.health.current() != stats.health.maximum() || info.target_entity.map_or(false, |e| e == *entity) - || info.selected_entity.map_or(false, |e| e == *entity) + || info.selected_entity.map_or(false, |s| s.0 == *entity) )) // Don't show outside a certain range .filter(|(_, pos, _, _, _, _, _, _, hpfl, _)| { @@ -1551,6 +1569,7 @@ impl Hud { Some(buttons::Event::ToggleSpell) => self.show.toggle_spell(), Some(buttons::Event::ToggleMap) => self.show.toggle_map(), Some(buttons::Event::ToggleCrafting) => self.show.toggle_crafting(), + Some(buttons::Event::ToggleGroup) => self.show.toggle_group(), None => {}, } } @@ -1875,6 +1894,7 @@ impl Hud { &self.imgs, &self.fonts, &self.voxygen_i18n, + info.selected_entity, ) .set(self.ids.social_window, ui_widgets) { @@ -1883,6 +1903,34 @@ impl Hud { social::Event::ChangeSocialTab(social_tab) => { self.show.open_social_tab(social_tab) }, + social::Event::Invite(uid) => events.push(Event::InviteMember(uid)), + social::Event::Accept => events.push(Event::AcceptInvite), + social::Event::Reject => events.push(Event::RejectInvite), + social::Event::Kick(uid) => events.push(Event::KickMember(uid)), + social::Event::LeaveGroup => events.push(Event::LeaveGroup), + social::Event::AssignLeader(uid) => events.push(Event::AssignLeader(uid)), + } + } + } + // Group Window + if self.show.group { + for event in Group::new( + &self.show, + client, + &self.imgs, + &self.fonts, + &self.voxygen_i18n, + info.selected_entity, + ) + .set(self.ids.group_window, ui_widgets) + { + match event { + group::Event::Close => self.show.social(false), + group::Event::Accept => events.push(Event::AcceptInvite), + group::Event::Reject => events.push(Event::RejectInvite), + 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)), } } } diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index 1bf7164bbe..cfbe8b9e7b 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -2,11 +2,17 @@ use super::{img_ids::Imgs, Show, TEXT_COLOR, TEXT_COLOR_3, UI_MAIN}; use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; use client::{self, Client}; +use common::{ + comp::Stats, + sync::{Uid, WorldSyncExt}, +}; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; +use specs::WorldExt; +use std::time::Instant; widget_ids! { pub struct Ids { @@ -25,9 +31,27 @@ widget_ids! { friends_test, faction_test, player_names[], + group, + group_invite, + member_names[], + accept_invite_button, + reject_invite_button, + invite_button, + kick_button, + assign_leader_button, + leave_button, } } +pub struct State { + ids: Ids, + // Holds the time when selection is made since this selection can be overriden + // by selecting an entity in-game + selected_uid: Option<(Uid, Instant)>, + // Selected group member + selected_member: Option, +} + pub enum SocialTab { Online, Friends, @@ -42,6 +66,8 @@ pub struct Social<'a> { fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + selected_entity: Option<(specs::Entity, Instant)>, + #[conrod(common_builder)] common: widget::CommonBuilder, } @@ -53,6 +79,7 @@ impl<'a> Social<'a> { imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, + selected_entity: Option<(specs::Entity, Instant)>, ) -> Self { Self { show, @@ -60,6 +87,7 @@ impl<'a> Social<'a> { imgs, fonts, localized_strings, + selected_entity, common: widget::CommonBuilder::default(), } } @@ -68,24 +96,32 @@ impl<'a> Social<'a> { pub enum Event { Close, ChangeSocialTab(SocialTab), + Invite(Uid), + Accept, + Reject, + Kick(Uid), + LeaveGroup, + AssignLeader(Uid), } impl<'a> Widget for Social<'a> { type Event = Vec; - type State = Ids; + type State = State; type Style = (); - fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { Ids::new(id_gen) } + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + Self::State { + ids: Ids::new(id_gen), + selected_uid: None, + selected_member: 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 { - /* id, */ state: ids, - ui, - .. - } = args; + let widget::UpdateArgs { state, ui, .. } = args; let mut events = Vec::new(); @@ -93,15 +129,15 @@ impl<'a> Widget for Social<'a> { .top_left_with_margins_on(ui.window, 200.0, 25.0) .color(Some(UI_MAIN)) .w_h(103.0 * 4.0, 122.0 * 4.0) - .set(ids.social_frame, ui); + .set(state.ids.social_frame, ui); // X-Button if Button::image(self.imgs.close_button) .w_h(28.0, 28.0) .hover_image(self.imgs.close_button_hover) .press_image(self.imgs.close_button_press) - .top_right_with_margins_on(ids.social_frame, 0.0, 0.0) - .set(ids.social_close, ui) + .top_right_with_margins_on(state.ids.social_frame, 0.0, 0.0) + .set(state.ids.social_close, ui) .was_clicked() { events.push(Event::Close); @@ -109,32 +145,32 @@ impl<'a> Widget for Social<'a> { // Title Text::new(&self.localized_strings.get("hud.social")) - .mid_top_with_margin_on(ids.social_frame, 6.0) + .mid_top_with_margin_on(state.ids.social_frame, 6.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .color(TEXT_COLOR) - .set(ids.social_title, ui); + .set(state.ids.social_title, ui); // Alignment Rectangle::fill_with([99.0 * 4.0, 112.0 * 4.0], color::TRANSPARENT) - .mid_top_with_margin_on(ids.social_frame, 8.0 * 4.0) - .set(ids.align, ui); + .mid_top_with_margin_on(state.ids.social_frame, 8.0 * 4.0) + .set(state.ids.align, ui); // Content Alignment Rectangle::fill_with([94.0 * 4.0, 94.0 * 4.0], color::TRANSPARENT) - .middle_of(ids.frame) + .middle_of(state.ids.frame) .scroll_kids() .scroll_kids_vertically() - .set(ids.content_align, ui); - Scrollbar::y_axis(ids.content_align) + .set(state.ids.content_align, ui); + Scrollbar::y_axis(state.ids.content_align) .thickness(5.0) .rgba(0.33, 0.33, 0.33, 1.0) - .set(ids.scrollbar, ui); + .set(state.ids.scrollbar, ui); // Frame Image::new(self.imgs.social_frame) .w_h(99.0 * 4.0, 100.0 * 4.0) - .mid_bottom_of(ids.align) + .mid_bottom_of(state.ids.align) .color(Some(UI_MAIN)) - .set(ids.frame, ui); + .set(state.ids.frame, ui); // Online Tab @@ -154,14 +190,14 @@ impl<'a> Widget for Social<'a> { } else { self.imgs.social_button_press }) - .top_left_with_margins_on(ids.align, 4.0, 0.0) + .top_left_with_margins_on(state.ids.align, 4.0, 0.0) .label(&self.localized_strings.get("hud.social.online")) .label_font_size(self.fonts.cyri.scale(14)) .label_font_id(self.fonts.cyri.conrod_id) - .parent(ids.frame) + .parent(state.ids.frame) .color(UI_MAIN) .label_color(TEXT_COLOR) - .set(ids.online_tab, ui) + .set(state.ids.online_tab, ui) .was_clicked() { events.push(Event::ChangeSocialTab(SocialTab::Online)); @@ -170,16 +206,15 @@ impl<'a> Widget for Social<'a> { // Contents if let SocialTab::Online = self.show.social_tab { - // TODO Needs to be a string sent from the server - // Players list // TODO: this list changes infrequently enough that it should not have to be // recreated every frame - let players = self.client.player_list.values().filter(|p| p.is_online); + let players = self.client.player_list.iter().filter(|(_, p)| p.is_online); let count = players.clone().count(); - if ids.player_names.len() < count { - ids.update(|ids| { - ids.player_names + if state.ids.player_names.len() < count { + state.update(|s| { + s.ids + .player_names .resize(count, &mut ui.widget_id_generator()) }) } @@ -189,25 +224,276 @@ impl<'a> Widget for Social<'a> { .get("hud.social.play_online_fmt") .replace("{nb_player}", &format!("{:?}", count)), ) - .top_left_with_margins_on(ids.content_align, -2.0, 7.0) + .top_left_with_margins_on(state.ids.content_align, -2.0, 7.0) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) - .set(ids.online_title, ui); - for (i, player_info) in players.enumerate() { - Text::new(&format!( - "[{}] {}", - player_info.player_alias, - match &player_info.character { - Some(character) => format!("{} Lvl {}", &character.name, &character.level), - None => "".to_string(), // character select or spectating - } - )) - .down(3.0) - .font_size(self.fonts.cyri.scale(15)) + .set(state.ids.online_title, ui); + + // Clear selected player if an entity was selected + if state + .selected_uid + .zip(self.selected_entity) + // Compare instants + .map_or(false, |(u, e)| u.1 < e.1) + { + state.update(|s| s.selected_uid = None); + } + + for (i, (&uid, player_info)) in players.enumerate() { + let selected = state.selected_uid.map_or(false, |u| u.0 == uid); + let alias = &player_info.player_alias; + let character_name_level = match &player_info.character { + Some(character) => format!("{} Lvl {}", &character.name, &character.level), + None => "".to_string(), // character select or spectating + }; + let text = if selected { + format!("-> [{}] {}", alias, character_name_level) + } else { + format!("[{}] {}", alias, character_name_level) + }; + Text::new(&text) + .down(3.0) + .font_size(self.fonts.cyri.scale(15)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.player_names[i], ui); + // Check for click + if ui + .widget_input(state.ids.player_names[i]) + .clicks() + .left() + .next() + .is_some() + { + state.update(|s| s.selected_uid = Some((uid, Instant::now()))); + } + } + + Text::new(&self.localized_strings.get("hud.group")) + .down(10.0) + .font_size(self.fonts.cyri.scale(20)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR) - .set(ids.player_names[i], ui); + .set(state.ids.group, ui); + + // Helper + let uid_to_name_text = |uid, client: &Client| match client.player_list.get(&uid) { + Some(player_info) => { + let alias = &player_info.player_alias; + let character_name_level = match &player_info.character { + Some(character) => format!("{} Lvl {}", &character.name, &character.level), + None => "".to_string(), // character select or spectating + }; + format!("[{}] {}", alias, character_name_level) + }, + None => self + .client + .state() + .ecs() + .entity_from_uid(uid.0) + .and_then(|entity| { + self.client + .state() + .ecs() + .read_storage::() + .get(entity) + .map(|stats| stats.name.clone()) + }) + .unwrap_or_else(|| format!("NPC Uid: {}", uid)), + }; + + // Accept/Reject Invite + if let Some(invite_uid) = self.client.group_invite() { + let name = uid_to_name_text(invite_uid, &self.client); + let text = self + .localized_strings + .get("hud.group.invite_to_join") + .replace("{name}", &name); + Text::new(&text) + .down(10.0) + .font_size(self.fonts.cyri.scale(15)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.group_invite, ui); + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("common.accept")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.accept_invite_button, ui) + .was_clicked() + { + events.push(Event::Accept); + } + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("common.reject")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.reject_invite_button, ui) + .was_clicked() + { + events.push(Event::Reject); + } + } else if self // Invite Button + .client + .group_leader() + .map_or(true, |l_uid| self.client.uid() == Some(l_uid)) + { + let selected = state.selected_uid.map(|s| s.0).or_else(|| { + self.selected_entity + .and_then(|s| self.client.state().read_component_copied(s.0)) + }); + + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.invite")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if selected.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.invite_button, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::Invite(uid)); + } + } + } + + // Show group members + if let Some(leader) = self.client.group_leader() { + let group_size = self.client.group_members.len() + 1; + if state.ids.member_names.len() < group_size { + state.update(|s| { + s.ids + .member_names + .resize(group_size, &mut ui.widget_id_generator()) + }) + } + // List member names + for (i, &uid) in self + .client + .uid() + .iter() + .chain(self.client.group_members.iter()) + .enumerate() + { + let selected = state.selected_member.map_or(false, |u| u == uid); + let text = uid_to_name_text(uid, &self.client); + let text = if selected { + format!("-> {}", &text) + } else { + text + }; + let text = if uid == leader { + format!("{} (Leader)", &text) + } else { + text + }; + Text::new(&text) + .down(3.0) + .font_size(self.fonts.cyri.scale(15)) + .font_id(self.fonts.cyri.conrod_id) + .color(TEXT_COLOR) + .set(state.ids.member_names[i], ui); + // Check for click + if ui + .widget_input(state.ids.member_names[i]) + .clicks() + .left() + .next() + .is_some() + { + state.update(|s| { + s.selected_member = if selected { None } else { Some(uid) } + }); + } + } + + // Show more buttons if leader + if self.client.uid() == Some(leader) { + let selected = state.selected_member; + // Kick + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.kick")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if selected.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.kick_button, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::Kick(uid)); + } + } + // Assign leader + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.assign_leader")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(if selected.is_some() { + TEXT_COLOR + } else { + TEXT_COLOR_3 + }) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.assign_leader_button, ui) + .was_clicked() + { + if let Some(uid) = selected { + events.push(Event::AssignLeader(uid)); + } + } + } + + // Leave group button + if Button::image(self.imgs.button) + .down(3.0) + .w_h(150.0, 30.0) + .hover_image(self.imgs.button_hover) + .press_image(self.imgs.button_press) + .label(&self.localized_strings.get("hud.group.leave")) + .label_y(conrod_core::position::Relative::Scalar(3.0)) + .label_color(TEXT_COLOR) + .label_font_size(self.fonts.cyri.scale(15)) + .label_font_id(self.fonts.cyri.conrod_id) + .set(state.ids.leave_button, ui) + .was_clicked() + { + events.push(Event::LeaveGroup); + } } } @@ -229,14 +515,14 @@ impl<'a> Widget for Social<'a> { } else { self.imgs.social_button }) - .right_from(ids.online_tab, 0.0) + .right_from(state.ids.online_tab, 0.0) .label(&self.localized_strings.get("hud.social.friends")) .label_font_size(self.fonts.cyri.scale(14)) .label_font_id(self.fonts.cyri.conrod_id) - .parent(ids.frame) + .parent(state.ids.frame) .color(UI_MAIN) .label_color(TEXT_COLOR_3) - .set(ids.friends_tab, ui) + .set(state.ids.friends_tab, ui) .was_clicked() { events.push(Event::ChangeSocialTab(SocialTab::Friends)); @@ -246,11 +532,11 @@ impl<'a> Widget for Social<'a> { if let SocialTab::Friends = self.show.social_tab { Text::new(&self.localized_strings.get("hud.social.not_yet_available")) - .middle_of(ids.content_align) + .middle_of(state.ids.content_align) .font_size(self.fonts.cyri.scale(18)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR_3) - .set(ids.friends_test, ui); + .set(state.ids.friends_test, ui); } // Faction Tab @@ -261,14 +547,14 @@ impl<'a> Widget for Social<'a> { }; if Button::image(button_img) .w_h(30.0 * 4.0, 12.0 * 4.0) - .right_from(ids.friends_tab, 0.0) + .right_from(state.ids.friends_tab, 0.0) .label(&self.localized_strings.get("hud.social.faction")) - .parent(ids.frame) + .parent(state.ids.frame) .label_font_size(self.fonts.cyri.scale(14)) .label_font_id(self.fonts.cyri.conrod_id) .color(UI_MAIN) .label_color(TEXT_COLOR_3) - .set(ids.faction_tab, ui) + .set(state.ids.faction_tab, ui) .was_clicked() { events.push(Event::ChangeSocialTab(SocialTab::Faction)); @@ -278,11 +564,11 @@ impl<'a> Widget for Social<'a> { if let SocialTab::Faction = self.show.social_tab { Text::new(&self.localized_strings.get("hud.social.not_yet_available")) - .middle_of(ids.content_align) + .middle_of(state.ids.content_align) .font_size(self.fonts.cyri.scale(18)) .font_id(self.fonts.cyri.conrod_id) .color(TEXT_COLOR_3) - .set(ids.faction_test, ui); + .set(state.ids.faction_test, ui); } events diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index a6f5e608b1..9efd0c04d1 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -53,7 +53,7 @@ pub struct SessionState { auto_walk: bool, is_aiming: bool, target_entity: Option, - selected_entity: Option, + selected_entity: Option<(specs::Entity, std::time::Instant)>, } /// Represents an active game session (i.e., the one being played). @@ -533,7 +533,8 @@ impl PlayState for SessionState { }, Event::InputUpdate(GameInput::Select, state) => { if !state { - self.selected_entity = self.target_entity; + self.selected_entity = + self.target_entity.map(|e| (e, std::time::Instant::now())); } }, Event::AnalogGameInput(input) => match input { @@ -950,6 +951,24 @@ impl PlayState for SessionState { HudEvent::CraftRecipe(r) => { self.client.borrow_mut().craft_recipe(&r); }, + HudEvent::InviteMember(uid) => { + self.client.borrow_mut().send_group_invite(uid); + }, + HudEvent::AcceptInvite => { + self.client.borrow_mut().accept_group_invite(); + }, + HudEvent::RejectInvite => { + self.client.borrow_mut().reject_group_invite(); + }, + HudEvent::KickMember(uid) => { + self.client.borrow_mut().kick_from_group(uid); + }, + HudEvent::LeaveGroup => { + self.client.borrow_mut().leave_group(); + }, + HudEvent::AssignLeader(uid) => { + self.client.borrow_mut().assign_group_leader(uid); + }, } }