Plumb trade requests through the group invite UI, such that they can be accepted/declined without impacting the counterparty's movement.

This commit is contained in:
Avi Weinstock 2021-02-10 19:59:14 -05:00
parent 250391656f
commit e9b811b62b
16 changed files with 367 additions and 257 deletions

View File

@ -5,6 +5,7 @@
string_map: {
"hud.group": "Group",
"hud.group.invite_to_join": "{name} invited you to their group!",
"hud.group.invite_to_trade": "{name} would like to trade with you.",
"hud.group.invite": "Invite",
"hud.group.kick": "Kick",
"hud.group.assign_leader": "Assign Leader",

View File

@ -20,7 +20,7 @@ use common::{
comp::{
self,
chat::{KillSource, KillType},
group,
group::{self, InviteKind},
skills::Skill,
slot::Slot,
ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip,
@ -69,6 +69,7 @@ const PING_ROLLING_AVERAGE_SECS: usize = 10;
pub enum Event {
Chat(comp::ChatMsg),
InviteComplete { target: Uid, answer: InviteAnswer, kind: InviteKind },
Disconnect,
DisconnectionNotification(u64),
InventoryUpdated(InventoryUpdateEvent),
@ -130,7 +131,7 @@ pub struct Client {
max_group_size: u32,
// Client has received an invite (inviter uid, time out instant)
group_invite: Option<(Uid, std::time::Instant, std::time::Duration)>,
group_invite: Option<(Uid, std::time::Instant, std::time::Duration, InviteKind)>,
group_leader: Option<Uid>,
// Note: potentially representable as a client only component
group_members: HashMap<Uid, group::Role>,
@ -636,18 +637,16 @@ impl Client {
}
}
pub fn is_dead(&self) -> bool {
self.state.ecs().read_storage::<comp::Health>().get(self.entity).map_or(false, |h| h.is_dead)
}
pub fn pick_up(&mut self, entity: EcsEntity) {
// Get the health component from the entity
if let Some(uid) = self.state.read_component_copied(entity) {
// If we're dead, exit before sending the message
if self
.state
.ecs()
.read_storage::<comp::Health>()
.get(self.entity)
.map_or(false, |h| h.is_dead)
{
if self.is_dead() {
return;
}
@ -659,13 +658,7 @@ impl Client {
pub fn npc_interact(&mut self, npc_entity: EcsEntity) {
// If we're dead, exit before sending message
if self
.state
.ecs()
.read_storage::<comp::Health>()
.get(self.entity)
.map_or(false, |h| h.is_dead)
{
if self.is_dead() {
return;
}
@ -674,6 +667,17 @@ impl Client {
}
}
pub fn initiate_trade(&mut self, counterparty: EcsEntity) {
// If we're dead, exit before sending message
if self.is_dead() {
return;
}
if let Some(uid) = self.state.read_component_copied(counterparty) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateTrade(uid)));
}
}
pub fn player_list(&self) -> &HashMap<Uid, PlayerInfo> { &self.player_list }
pub fn character_list(&self) -> &CharacterList { &self.character_list }
@ -737,7 +741,7 @@ impl Client {
pub fn max_group_size(&self) -> u32 { self.max_group_size }
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration)> {
pub fn group_invite(&self) -> Option<(Uid, std::time::Instant, std::time::Duration, InviteKind)> {
self.group_invite
}
@ -1094,7 +1098,7 @@ impl Client {
// Check if the group invite has timed out and remove if so
if self
.group_invite
.map_or(false, |(_, timeout, dur)| timeout.elapsed() > dur)
.map_or(false, |(_, timeout, dur, _)| timeout.elapsed() > dur)
{
self.group_invite = None;
}
@ -1468,30 +1472,22 @@ impl Client {
},
}
},
ServerGeneral::GroupInvite { inviter, timeout } => {
self.group_invite = Some((inviter, std::time::Instant::now(), timeout));
ServerGeneral::GroupInvite { inviter, timeout, kind } => {
self.group_invite = Some((inviter, std::time::Instant::now(), timeout, kind));
},
ServerGeneral::InvitePending(uid) => {
if !self.pending_invites.insert(uid) {
warn!("Received message about pending invite that was already pending");
}
},
ServerGeneral::InviteComplete { target, answer } => {
ServerGeneral::InviteComplete { target, answer, kind } => {
if !self.pending_invites.remove(&target) {
warn!(
"Received completed invite message for invite that was not in the list of \
pending invites"
)
}
// TODO: expose this as a new event variant instead of going
// through the chat
let msg = match answer {
// TODO: say who accepted/declined/timed out the invite
InviteAnswer::Accepted => "Invite accepted",
InviteAnswer::Declined => "Invite declined",
InviteAnswer::TimedOut => "Invite timed out",
};
frontend_events.push(Event::Chat(comp::ChatType::Meta.chat_msg(msg)));
frontend_events.push(Event::InviteComplete { target, answer, kind });
},
// Cleanup for when the client goes back to the `presence = None`
ServerGeneral::ExitInGameSuccess => {

View File

@ -3,7 +3,7 @@ use crate::sync;
use authc::AuthClientError;
use common::{
character::{self, CharacterItem},
comp,
comp::{self, group::InviteKind},
outcome::Outcome,
recipe::RecipeBook,
resources::TimeOfDay,
@ -80,6 +80,7 @@ pub enum ServerGeneral {
GroupInvite {
inviter: sync::Uid,
timeout: std::time::Duration,
kind: InviteKind,
},
/// Indicate to the client that their sent invite was not invalid and is
/// currently pending
@ -92,6 +93,7 @@ pub enum ServerGeneral {
InviteComplete {
target: sync::Uid,
answer: InviteAnswer,
kind: InviteKind,
},
/// Trigger cleanup for when the client goes back to the `Registered` state
/// from an ingame state

View File

@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use std::time::Duration;
use hashbrown::HashMap;
use vek::*;
/// Default duration before an input is considered 'held'.
@ -83,6 +84,7 @@ pub enum ControlEvent {
EnableLantern,
DisableLantern,
Interact(Uid),
InitiateTrade(Uid),
Mount(Uid),
Unmount,
InventoryManip(InventoryManip),
@ -318,3 +320,25 @@ pub struct Mounting(pub Uid);
impl Component for Mounting {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
/// A PendingTrade is associated with the entity that initiated the trade.
///
/// Items are not removed from the inventory during a PendingTrade: all the items are moved
/// atomically (if there's space and both parties agree) upon completion
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PendingTrade {
/// `counterparty` is the other entity that's being traded with
counterparty: Uid,
/// `self_offer` represents the items and quantities of the initiator's items being offered
self_offer: HashMap<InvSlotId, usize>,
/// `other_offer` represents the items and quantities of the counterparty's items being offered
other_offer: HashMap<InvSlotId, usize>,
/// `locked` is set when going from the first (mutable) screen to the second (readonly, review)
/// screen
locked: bool,
}
impl Component for PendingTrade {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}

View File

@ -28,14 +28,24 @@ impl Component for Group {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
pub struct Invite(pub specs::Entity);
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum InviteKind {
Group,
Trade,
}
pub struct Invite {
pub inviter: specs::Entity,
pub kind: InviteKind,
}
impl Component for Invite {
type Storage = IdvStorage<Self>;
}
// Pending invites that an entity currently has sent out
// (invited entity, instant when invite times out)
pub struct PendingInvites(pub Vec<(specs::Entity, std::time::Instant)>);
pub struct PendingInvites(pub Vec<(specs::Entity, InviteKind, std::time::Instant)>);
impl Component for PendingInvites {
type Storage = IdvStorage<Self>;
}

View File

@ -46,7 +46,7 @@ pub use chat::{
};
pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input,
InventoryManip, LoadoutManip, MountState, Mounting, SlotManip,
InventoryManip, LoadoutManip, MountState, Mounting, PendingTrade, SlotManip,
};
pub use energy::{Energy, EnergyChange, EnergySource};
pub use group::Group;

View File

@ -79,6 +79,7 @@ pub enum ServerEvent {
EnableLantern(EcsEntity),
DisableLantern(EcsEntity),
NpcInteract(EcsEntity, EcsEntity),
InitiateTrade(EcsEntity, EcsEntity),
Mount(EcsEntity, EcsEntity),
Unmount(EcsEntity),
Possess(Uid, Uid),

View File

@ -98,6 +98,13 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::NpcInteract(entity, npc_entity));
}
},
ControlEvent::InitiateTrade(counterparty_uid) => {
if let Some(counterparty_entity) =
uid_allocator.retrieve_entity_internal(counterparty_uid.id())
{
server_emitter.emit(ServerEvent::InitiateTrade(entity, counterparty_entity));
}
},
ControlEvent::InventoryManip(manip) => {
server_emitter.emit(ServerEvent::InventoryManip(entity, manip.into()));
},

View File

@ -2,7 +2,7 @@ use crate::{client::Client, Server};
use common::{
comp::{
self,
group::{self, Group, GroupManager, Invite, PendingInvites},
group::{self, Group, GroupManager, Invite, InviteKind, PendingInvites},
ChatType, GroupManip,
},
uid::Uid,
@ -20,169 +20,176 @@ const INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(31);
/// Reduced duration shown to the client to help alleviate latency issues
const PRESENTED_INVITE_TIMEOUT_DUR: Duration = Duration::from_secs(30);
// TODO: turn chat messages into enums
pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) {
pub fn handle_invite(server: &mut Server, inviter: specs::Entity, invitee_uid: Uid, kind: InviteKind) {
let max_group_size = server.settings().max_player_group_size;
let state = server.state_mut();
match manip {
GroupManip::Invite(uid) => {
let clients = state.ecs().read_storage::<Client>();
let invitee = match state.ecs().entity_from_uid(uid.into()) {
Some(t) => t,
None => {
// Inform of failure
if let Some(client) = clients.get(entity) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"Invite failed, target does not exist.",
));
}
return;
},
};
let uids = state.ecs().read_storage::<Uid>();
// 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;
}
// Disallow inviting entity that is already in your group
let groups = state.ecs().read_storage::<Group>();
let group_manager = state.ecs().read_resource::<GroupManager>();
let already_in_same_group = groups.get(entity).map_or(false, |group| {
group_manager
.group_info(*group)
.map_or(false, |g| g.leader == entity)
&& groups.get(invitee) == Some(group)
});
if already_in_same_group {
// Inform of failure
if let Some(client) = clients.get(entity) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"Invite failed, can't invite someone already in your group",
));
}
return;
}
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
// Check if group max size is already reached
// Adding the current number of pending invites
let group_size_limit_reached = state
.ecs()
.read_storage()
.get(entity)
.copied()
.and_then(|group| {
// If entity is currently the leader of a full group then they can't invite
// anyone else
group_manager
.group_info(group)
.filter(|i| i.leader == entity)
.map(|i| i.num_members)
})
.unwrap_or(1) as usize
+ pending_invites.get(entity).map_or(0, |p| p.0.len())
>= max_group_size as usize;
if group_size_limit_reached {
// Inform inviter that they have reached the group size limit
if let Some(client) = clients.get(entity) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"Invite failed, pending invites plus current group size have reached the \
group size limit"
.to_owned(),
));
}
return;
}
let agents = state.ecs().read_storage::<comp::Agent>();
let mut invites = state.ecs().write_storage::<Invite>();
if invites.contains(invitee) {
// Inform inviter that there is already an invite
if let Some(client) = clients.get(entity) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"This player already has a pending invite.",
));
}
return;
}
let mut invite_sent = false;
// Returns true if insertion was succesful
let mut send_invite = || {
match invites.insert(invitee, group::Invite(entity)) {
Err(err) => {
error!("Failed to insert Invite component: {:?}", err);
false
},
Ok(_) => {
match pending_invites.entry(entity) {
Ok(entry) => {
entry
.or_insert_with(|| PendingInvites(Vec::new()))
.0
.push((invitee, Instant::now() + INVITE_TIMEOUT_DUR));
invite_sent = true;
true
},
Err(err) => {
error!(
"Failed to get entry for pending invites component: {:?}",
err
);
// Cleanup
invites.remove(invitee);
false
},
}
},
}
};
// If client comp
if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(entity).copied())
{
if send_invite() {
client.send_fallible(ServerGeneral::GroupInvite {
inviter,
timeout: PRESENTED_INVITE_TIMEOUT_DUR,
});
}
} else if agents.contains(invitee) {
send_invite();
} else if let Some(client) = clients.get(entity) {
let clients = state.ecs().read_storage::<Client>();
let invitee = match state.ecs().entity_from_uid(invitee_uid.into()) {
Some(t) => t,
None => {
// Inform of failure
if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"Can't invite, not a player or npc",
"Invite failed, target does not exist.",
));
}
return;
},
};
// Notify inviter that the invite is pending
if invite_sent {
if let Some(client) = clients.get(entity) {
client.send_fallible(ServerGeneral::InvitePending(uid));
}
let uids = state.ecs().read_storage::<Uid>();
// Check if entity is trying to invite themselves
if uids
.get(inviter)
.map_or(false, |inviter_uid| *inviter_uid == invitee_uid)
{
warn!("Entity tried to invite themselves into a group/trade");
return;
}
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
if let InviteKind::Group = kind {
// Disallow inviting entity that is already in your group
let groups = state.ecs().read_storage::<Group>();
let group_manager = state.ecs().read_resource::<GroupManager>();
let already_in_same_group = groups.get(inviter).map_or(false, |group| {
group_manager
.group_info(*group)
.map_or(false, |g| g.leader == inviter)
&& groups.get(invitee) == Some(group)
});
if already_in_same_group {
// Inform of failure
if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"Invite failed, can't invite someone already in your group",
));
}
return;
}
// Check if group max size is already reached
// Adding the current number of pending invites
let group_size_limit_reached = state
.ecs()
.read_storage()
.get(inviter)
.copied()
.and_then(|group| {
// If entity is currently the leader of a full group then they can't invite
// anyone else
group_manager
.group_info(group)
.filter(|i| i.leader == inviter)
.map(|i| i.num_members)
})
.unwrap_or(1) as usize
+ pending_invites.get(inviter).map_or(0, |p| p.0.len())
>= max_group_size as usize;
if group_size_limit_reached {
// Inform inviter that they have reached the group size limit
if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"Invite failed, pending invites plus current group size have reached the \
group size limit"
.to_owned(),
));
}
return;
}
}
let agents = state.ecs().read_storage::<comp::Agent>();
let mut invites = state.ecs().write_storage::<Invite>();
if invites.contains(invitee) {
// Inform inviter that there is already an invite
if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"This player already has a pending invite.",
));
}
return;
}
let mut invite_sent = false;
// Returns true if insertion was succesful
let mut send_invite = || {
match invites.insert(invitee, group::Invite { inviter, kind }) {
Err(err) => {
error!("Failed to insert Invite component: {:?}", err);
false
},
Ok(_) => {
match pending_invites.entry(inviter) {
Ok(entry) => {
entry
.or_insert_with(|| PendingInvites(Vec::new()))
.0
.push((invitee, kind, Instant::now() + INVITE_TIMEOUT_DUR));
invite_sent = true;
true
},
Err(err) => {
error!(
"Failed to get entry for pending invites component: {:?}",
err
);
// Cleanup
invites.remove(invitee);
false
},
}
},
}
};
// If client comp
if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(inviter).copied())
{
if send_invite() {
client.send_fallible(ServerGeneral::GroupInvite {
inviter,
timeout: PRESENTED_INVITE_TIMEOUT_DUR,
kind,
});
}
} else if agents.contains(invitee) {
send_invite();
} else if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,
"Can't invite, not a player or npc",
));
}
// Notify inviter that the invite is pending
if invite_sent {
if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::InvitePending(invitee_uid));
}
}
}
// TODO: turn chat messages into enums
pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupManip) {
match manip {
GroupManip::Invite(uid) => {
handle_invite(server, entity, uid, InviteKind::Group);
},
GroupManip::Accept => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0;
if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| {
let Invite { inviter, kind } = invite;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
let pending = &mut pending_invites.get_mut(inviter)?.0;
// Check that inviter has a pending invite and remove it from the list
@ -193,7 +200,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
pending_invites.remove(inviter);
}
Some(inviter)
Some((inviter, kind))
}) {
if let (Some(client), Some(target)) =
(clients.get(inviter), uids.get(entity).copied())
@ -201,35 +208,39 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
client.send_fallible(ServerGeneral::InviteComplete {
target,
answer: InviteAnswer::Accepted,
kind,
});
}
let mut group_manager = state.ecs().write_resource::<GroupManager>();
group_manager.add_group_member(
inviter,
entity,
&state.ecs().entities(),
&mut state.ecs().write_storage(),
&state.ecs().read_storage(),
&uids,
|entity, group_change| {
clients
.get(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g)));
},
);
if let InviteKind::Group = kind {
let mut group_manager = state.ecs().write_resource::<GroupManager>();
group_manager.add_group_member(
inviter,
entity,
&state.ecs().entities(),
&mut state.ecs().write_storage(),
&state.ecs().read_storage(),
&uids,
|entity, group_change| {
clients
.get(entity)
.and_then(|c| {
group_change
.try_map(|e| uids.get(e).copied())
.map(|g| (g, c))
})
.map(|(g, c)| c.send(ServerGeneral::GroupUpdate(g)));
},
);
}
}
},
GroupManip::Decline => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut invites = state.ecs().write_storage::<Invite>();
if let Some(inviter) = invites.remove(entity).and_then(|invite| {
let inviter = invite.0;
if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| {
let Invite { inviter, kind } = invite;
let mut pending_invites = state.ecs().write_storage::<PendingInvites>();
let pending = &mut pending_invites.get_mut(inviter)?.0;
// Check that inviter has a pending invite and remove it from the list
@ -240,7 +251,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
pending_invites.remove(inviter);
}
Some(inviter)
Some((inviter, kind))
}) {
// Inform inviter of rejection
if let (Some(client), Some(target)) =
@ -249,11 +260,13 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
client.send_fallible(ServerGeneral::InviteComplete {
target,
answer: InviteAnswer::Declined,
kind,
});
}
}
},
GroupManip::Leave => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let mut group_manager = state.ecs().write_resource::<GroupManager>();
@ -276,6 +289,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
);
},
GroupManip::Kick(uid) => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let alignments = state.ecs().read_storage::<comp::Alignment>();
@ -379,6 +393,7 @@ pub fn handle_group(server: &mut Server, entity: specs::Entity, manip: GroupMani
}
},
GroupManip::AssignLeader(uid) => {
let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let target = match state.ecs().entity_from_uid(uid.into()) {

View File

@ -1,8 +1,8 @@
use specs::{world::WorldExt, Entity as EcsEntity};
use tracing::error;
use tracing::{error, warn};
use common::{
comp::{self, agent::AgentEvent, inventory::slot::EquipSlot, item, slot::Slot, Inventory, Pos},
comp::{self, agent::AgentEvent, group::InviteKind, inventory::slot::EquipSlot, item, slot::Slot, Inventory, Pos},
consts::MAX_MOUNT_RANGE,
uid::Uid,
};
@ -10,6 +10,7 @@ use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use crate::{
client::Client,
events::group_manip::handle_invite,
presence::{Presence, RegionSubscription},
Server,
};
@ -68,6 +69,14 @@ pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_en
}
}
pub fn handle_initiate_trade(server: &mut Server, interactor: EcsEntity, counterparty: EcsEntity) {
if let Some(uid) = server.state_mut().ecs().uid_from_entity(counterparty) {
handle_invite(server, interactor, uid, InviteKind::Trade);
} else {
warn!("Entity tried to trade with an entity that lacks an uid");
}
}
pub fn handle_mount(server: &mut Server, mounter: EcsEntity, mountee: EcsEntity) {
let state = server.state_mut();

View File

@ -13,7 +13,7 @@ use entity_manipulation::{
};
use group_manip::handle_group;
use interaction::{
handle_lantern, handle_mount, handle_npc_interaction, handle_possess, handle_unmount,
handle_lantern, handle_initiate_trade, handle_mount, handle_npc_interaction, handle_possess, handle_unmount,
};
use inventory_manip::handle_inventory;
use player::{handle_client_disconnect, handle_exit_ingame};
@ -103,6 +103,9 @@ impl Server {
ServerEvent::NpcInteract(interactor, target) => {
handle_npc_interaction(self, interactor, target)
},
ServerEvent::InitiateTrade(interactor, target) => {
handle_initiate_trade(self, interactor, target)
},
ServerEvent::Mount(mounter, mountee) => handle_mount(self, mounter, mountee),
ServerEvent::Unmount(mounter) => handle_unmount(self, mounter),
ServerEvent::Possess(possessor_uid, possesse_uid) => {

View File

@ -32,13 +32,13 @@ impl<'a> System<'a> for Sys {
let timed_out_invites = (&entities, &invites)
.join()
.filter_map(|(invitee, Invite(inviter))| {
.filter_map(|(invitee, Invite { inviter, kind })| {
// Retrieve timeout invite from pending invites
let pending = &mut pending_invites.get_mut(*inviter)?.0;
let index = pending.iter().position(|p| p.0 == invitee)?;
// Stop if not timed out
if pending[index].1 > now {
if pending[index].2 > now {
return None;
}
@ -57,6 +57,7 @@ impl<'a> System<'a> for Sys {
client.send_fallible(ServerGeneral::InviteComplete {
target,
answer: InviteAnswer::TimedOut,
kind: *kind,
});
}

View File

@ -16,7 +16,7 @@ use crate::{
use client::{self, Client};
use common::{
combat,
comp::{group::Role, BuffKind, Stats},
comp::{group::{InviteKind, Role}, BuffKind, Stats},
uid::{Uid, UidAllocator},
};
use common_net::sync::WorldSyncExt;
@ -219,7 +219,7 @@ impl<'a> Widget for Group<'a> {
.crop_kids()
.set(state.ids.bg, ui);
}
if let Some((_, timeout_start, timeout_dur)) = open_invite {
if let Some((_, timeout_start, timeout_dur, kind)) = open_invite {
// Group Menu button
Button::image(self.imgs.group_icon)
.w_h(49.0, 26.0)
@ -792,16 +792,26 @@ impl<'a> Widget for Group<'a> {
// into the maximum group size.
}
}
if let Some((invite_uid, _, _)) = open_invite {
if let Some((invite_uid, _, _, kind)) = open_invite {
self.show.group = true; // Auto open group menu
// TODO: add group name here too
// Invite text
let name = uid_to_name_text(invite_uid, &self.client);
let invite_text = self
.localized_strings
.get("hud.group.invite_to_join")
.replace("{name}", &name);
let invite_text = match kind {
InviteKind::Group => {
self
.localized_strings
.get("hud.group.invite_to_join")
.replace("{name}", &name)
},
InviteKind::Trade => {
self
.localized_strings
.get("hud.group.invite_to_trade")
.replace("{name}", &name)
},
};
Text::new(&invite_text)
.mid_top_with_margin_on(state.ids.bg, 5.0)
.font_size(12)

View File

@ -2810,10 +2810,6 @@ impl Hud {
self.show.toggle_bag();
true
},
GameInput::Trade if state => {
self.show.toggle_trade();
true
},
GameInput::Social if state => {
self.show.toggle_social();
true

View File

@ -6,9 +6,6 @@ use super::{
util::loadout_slot_text,
Show, CRITICAL_HP_COLOR, LOW_HP_COLOR, QUALITY_COMMON, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
};
use common::{
comp::{item::Quality,},
};
use crate::{
hud::get_quality_col,
i18n::Localization,
@ -19,6 +16,7 @@ use crate::{
},
};
use client::Client;
use common::comp::item::Quality;
use conrod_core::{
color,
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
@ -129,36 +127,28 @@ impl<'a> Widget for Trade<'a> {
// BG
Image::new(self.imgs.inv_bg_bag)
.w_h(424.0, 708.0)
.middle()
.color(Some(UI_MAIN))
.set(state.ids.bg, ui);
.w_h(424.0, 708.0)
.middle()
.color(Some(UI_MAIN))
.set(state.ids.bg, ui);
Image::new(self.imgs.inv_frame_bag)
.w_h(424.0, 708.0)
.middle_of(state.ids.bg)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.bg_frame, ui);
.w_h(424.0, 708.0)
.middle_of(state.ids.bg)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.bg_frame, ui);
// Title
Text::new(
&self
.localized_strings
.get("hud.trade.trade_window")
)
.mid_top_with_margin_on(state.ids.bg_frame, 9.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20))
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.trade_title_bg, ui);
Text::new(
&self
.localized_strings
.get("hud.trade.trade_window")
)
.top_left_with_margins_on(state.ids.trade_title_bg, 2.0, 2.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20))
.color(TEXT_COLOR)
.set(state.ids.trade_title, ui);
Text::new(&self.localized_strings.get("hud.trade.trade_window"))
.mid_top_with_margin_on(state.ids.bg_frame, 9.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20))
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.trade_title_bg, ui);
Text::new(&self.localized_strings.get("hud.trade.trade_window"))
.top_left_with_margins_on(state.ids.trade_title_bg, 2.0, 2.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20))
.color(TEXT_COLOR)
.set(state.ids.trade_title, ui);
event
}

View File

@ -9,7 +9,7 @@ use client::{self, Client};
use common::{
assets::AssetExt,
comp,
comp::{inventory::slot::Slot, ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel},
comp::{inventory::slot::Slot, group::InviteKind, ChatMsg, ChatType, InventoryUpdateEvent, Pos, Vel},
consts::{MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
outcome::Outcome,
span,
@ -20,7 +20,7 @@ use common::{
},
vol::ReadVol,
};
use common_net::msg::PresenceKind;
use common_net::msg::{server::InviteAnswer, PresenceKind};
use crate::{
audio::sfx::SfxEvent,
@ -121,6 +121,24 @@ impl SessionState {
client::Event::Chat(m) => {
self.hud.new_message(m);
},
client::Event::InviteComplete { target, answer, kind } => {
// TODO: i18n
let kind_str = match kind {
InviteKind::Group => "Group",
InviteKind::Trade => "Trade",
};
let target_name = match client.player_list().get(&target) {
Some(info) => info.player_alias.clone(),
None => "<unknown>".to_string(),
};
let answer_str = match answer {
InviteAnswer::Accepted => "accepted",
InviteAnswer::Declined => "declined",
InviteAnswer::TimedOut => "timed out",
};
let msg = format!("{} invite to {} {}", kind_str, target_name, answer_str);
self.hud.new_message(ChatType::Meta.chat_msg(msg));
}
client::Event::InventoryUpdated(inv_event) => {
let sfx_triggers = self.scene.sfx_mgr.triggers.read();
@ -524,6 +542,33 @@ impl PlayState for SessionState {
}
}
}
Event::InputUpdate(GameInput::Trade, state)
if state != self.key_state.collect =>
{
self.key_state.collect = state;
if state {
if let Some(interactable) = self.interactable {
let mut client = self.client.borrow_mut();
match interactable {
Interactable::Block(_, _) => {},
Interactable::Entity(entity) => {
if client
.state()
.ecs()
.read_storage::<comp::Item>()
.get(entity)
.is_some()
{
client.pick_up(entity);
} else {
client.initiate_trade(entity);
}
},
}
}
}
}
/*Event::InputUpdate(GameInput::Charge, state) => {
self.inputs.charge.set_state(state);
},*/