Implement enough of a trade UI that dragging & dropping items into it round-trips between clients.

This commit is contained in:
Avi Weinstock 2021-02-11 21:53:25 -05:00
parent aeb2398fc6
commit e2b55e0706
11 changed files with 284 additions and 77 deletions

View File

@ -4,6 +4,10 @@
(
string_map: {
"hud.trade.trade_window": "Trade window",
"hud.trade.phase1_description": "Drag the items you want to trade into your area.",
"hud.trade.phase2_description": "The trade is now locked to give you time to review it.",
/// Phase3 should only be visible for a few milliseconds if everything is working properly, but is included for completeness
"hud.trade.phase3_description": "Trade is finalized.",
},

View File

@ -646,6 +646,12 @@ impl Client {
}
}
pub fn trade_action(&mut self, msg: TradeActionMsg) {
if let Some((id, _)) = self.pending_trade {
self.send_msg(ClientGeneral::UpdatePendingTrade(id, msg));
}
}
pub fn decline_trade(&mut self) {
if let Some((id, _)) = self.pending_trade.take() {
self.send_msg(ClientGeneral::UpdatePendingTrade(

View File

@ -1,7 +1,4 @@
use crate::{
comp::inventory::slot::InvSlotId,
uid::Uid,
};
use crate::{comp::inventory::slot::InvSlotId, uid::Uid};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use tracing::warn;
@ -11,8 +8,8 @@ use tracing::warn;
/// accepting on behalf of)
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum TradeActionMsg {
AddItem { item: InvSlotId, quantity: usize },
RemoveItem { item: InvSlotId, quantity: usize },
AddItem { item: InvSlotId, quantity: u32 },
RemoveItem { item: InvSlotId, quantity: u32 },
Phase1Accept,
Phase2Accept,
Decline,
@ -28,7 +25,7 @@ pub struct PendingTrade {
pub parties: [Uid; 2],
/// `offers[i]` represents the items and quantities of the party i's items
/// being offered
pub offers: [HashMap<InvSlotId, usize>; 2],
pub offers: [HashMap<InvSlotId, u32>; 2],
/// phase1_accepts indicate that the parties wish to proceed to review
pub phase1_accepts: [bool; 2],
/// phase2_accepts indicate that the parties have reviewed the trade and

View File

@ -170,9 +170,7 @@ impl Client {
| ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_)
| ServerGeneral::UpdatePendingTrade(_, _)
| ServerGeneral::DeclinedTrade => {
PreparedMsg::new(2, &g, &self.in_game_stream)
},
| ServerGeneral::DeclinedTrade => PreparedMsg::new(2, &g, &self.in_game_stream),
// Always possible
ServerGeneral::PlayerListUpdate(_)
| ServerGeneral::ChatMsg(_)

View File

@ -2,10 +2,7 @@ use specs::{world::WorldExt, Entity as EcsEntity};
use tracing::{error, warn};
use common::{
comp::{
self, agent::AgentEvent, group::InviteKind, inventory::slot::EquipSlot, item, slot::Slot,
Inventory, Pos,
},
comp::{self, agent::AgentEvent, inventory::slot::EquipSlot, item, slot::Slot, Inventory, Pos},
consts::MAX_MOUNT_RANGE,
uid::Uid,
};
@ -13,7 +10,6 @@ use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use crate::{
client::Client,
events::group_manip::handle_invite,
presence::{Presence, RegionSubscription},
Server,
};

View File

@ -1,8 +1,8 @@
use crate::{state_ext::StateExt, Server};
use common::{
event::{EventBus, ServerEvent},
trade::{Trades, TradeActionMsg},
span,
trade::{TradeActionMsg, Trades},
};
use entity_creation::{
handle_beam, handle_create_npc, handle_create_waypoint, handle_initialize_character,
@ -14,8 +14,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_mount, handle_npc_interaction, handle_possess, handle_unmount,
};
use inventory_manip::handle_inventory;
use player::{handle_client_disconnect, handle_exit_ingame};

View File

@ -1,20 +1,13 @@
use crate::{
Server,
comp::inventory::slot::InvSlotId,
events::group_manip::handle_invite,
};
use crate::{comp::inventory::slot::InvSlotId, events::group_manip::handle_invite, Server};
use common::{
comp::{
group::InviteKind,
},
trade::{Trades, TradeActionMsg, PendingTrade},
comp::group::InviteKind,
trade::{PendingTrade, TradeActionMsg, Trades},
uid::Uid,
};
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use specs::{world::WorldExt, Entity as EcsEntity};
use tracing::warn;
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);
@ -23,7 +16,12 @@ pub fn handle_initiate_trade(server: &mut Server, interactor: EcsEntity, counter
}
}
pub fn handle_process_trade_action(server: &mut Server, entity: EcsEntity, trade_id: usize, msg: TradeActionMsg) {
pub fn handle_process_trade_action(
server: &mut Server,
entity: EcsEntity,
trade_id: usize,
msg: TradeActionMsg,
) {
if let Some(uid) = server.state.ecs().uid_from_entity(entity) {
let mut trades = server.state.ecs().write_resource::<Trades>();
if let TradeActionMsg::Decline = msg {
@ -36,6 +34,16 @@ pub fn handle_process_trade_action(server: &mut Server, entity: EcsEntity, trade
if let Some(trade) = trades.trades.get(&trade_id) {
if trade.should_commit() {
// TODO: inventory manip
} else {
// send the updated state to both parties
for party in trade.parties.iter() {
server.state.ecs().entity_from_uid(party.0).map(|e| {
server.notify_client(
e,
ServerGeneral::UpdatePendingTrade(trade_id, trade.clone()),
)
});
}
}
}
}

View File

@ -44,6 +44,7 @@ use prompt_dialog::PromptDialog;
use serde::{Deserialize, Serialize};
use settings_window::{SettingsTab, SettingsWindow};
use skillbar::Skillbar;
use slots::{InventorySlot, TradeSlot};
use social::{Social, SocialTab};
use trade::Trade;
@ -54,7 +55,7 @@ use crate::{
render::{Consts, Globals, RenderMode, Renderer},
scene::camera::{self, Camera},
settings::Fps,
ui::{fonts::Fonts, img_ids::Rotations, slot, Graphic, Ingameable, ScaleMode, Ui},
ui::{fonts::Fonts, img_ids::Rotations, slot, slot::SlotKey, Graphic, Ingameable, ScaleMode, Ui},
window::{Event as WinEvent, FullScreenSettings, GameInput},
GlobalState,
};
@ -71,6 +72,7 @@ use common::{
outcome::Outcome,
span,
terrain::TerrainChunk,
trade::TradeActionMsg,
uid::Uid,
util::srgba_to_linear,
vol::RectRasterableVol,
@ -397,6 +399,7 @@ pub enum Event {
DropSlot(comp::slot::Slot),
DeclineTrade,
ChangeHotbarState(Box<HotbarState>),
TradeAction(TradeActionMsg),
Ability3(bool),
Logout,
Quit,
@ -876,7 +879,9 @@ impl Hud {
let entities = ecs.entities();
let me = client.entity();
if (client.pending_trade().is_some() && !self.show.trade) || (client.pending_trade().is_none() && self.show.trade) {
if (client.pending_trade().is_some() && !self.show.trade)
|| (client.pending_trade().is_none() && self.show.trade)
{
self.show.toggle_trade();
}
@ -2610,6 +2615,7 @@ impl Hud {
Inventory(i) => Some(Slot::Inventory(i.0)),
Equip(e) => Some(Slot::Equip(e)),
Hotbar(_) => None,
Trade(_) => None,
};
match event {
slot::Event::Dragged(a, b) => {
@ -2626,6 +2632,22 @@ impl Hud {
} else if let (Hotbar(a), Hotbar(b)) = (a, b) {
self.hotbar.swap(a, b);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
} else if let (Inventory(i), Trade(_)) = (a, b) {
if let Some(inventory) = inventories.get(entity) {
events.push(Event::TradeAction(TradeActionMsg::AddItem {
item: i.0,
quantity: i.amount(inventory).unwrap_or(0),
}));
}
} else if let (Trade(t), Inventory(_)) = (a, b) {
if let Some(inventory) = inventories.get(entity) {
if let Some(invslot) = t.invslot {
events.push(Event::TradeAction(TradeActionMsg::RemoveItem {
item: invslot,
quantity: t.amount(inventory).unwrap_or(0),
}));
}
}
}
},
slot::Event::Dropped(from) => {

View File

@ -21,6 +21,7 @@ pub enum SlotKind {
Inventory(InventorySlot),
Equip(EquipSlot),
Hotbar(HotbarSlot),
Trade(TradeSlot),
/* Spellbook(SpellbookSlot), TODO */
}
@ -63,6 +64,32 @@ impl SlotKey<Inventory, ItemImgs> for EquipSlot {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TradeSlot {
pub index: usize,
pub quantity: u32,
pub invslot: Option<InvSlotId>,
}
impl SlotKey<Inventory, ItemImgs> for TradeSlot {
type ImageKey = ItemKey;
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
self.invslot
.and_then(|inv_id| InventorySlot(inv_id).image_key(source))
}
fn amount(&self, source: &Inventory) -> Option<u32> {
self.invslot
.and_then(|inv_id| InventorySlot(inv_id).amount(source))
.map(|x| x.min(self.quantity))
}
fn image_id(key: &Self::ImageKey, source: &ItemImgs) -> image::Id {
source.img_id_or_not_found_img(key.clone())
}
}
#[derive(Clone, PartialEq)]
pub enum HotbarImage {
Item(ItemKey),
@ -159,5 +186,8 @@ impl From<EquipSlot> for SlotKind {
impl From<HotbarSlot> for SlotKind {
fn from(hotbar: HotbarSlot) -> Self { Self::Hotbar(hotbar) }
}
impl From<TradeSlot> for SlotKind {
fn from(trade: TradeSlot) -> Self { Self::Trade(trade) }
}
impl SumSlot for SlotKind {}

View File

@ -2,12 +2,12 @@ use super::{
cr_color,
img_ids::{Imgs, ImgsRot},
item_imgs::ItemImgs,
slots::{InventorySlot, SlotManager},
slots::{InventorySlot, SlotManager, TradeSlot},
util::loadout_slot_text,
Show, CRITICAL_HP_COLOR, LOW_HP_COLOR, QUALITY_COMMON, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
};
use crate::{
hud::get_quality_col,
hud::{get_quality_col, slots::SlotKind},
i18n::Localization,
ui::{
fonts::Fonts,
@ -16,12 +16,15 @@ use crate::{
},
};
use client::Client;
use common::comp::item::Quality;
use common::{comp::item::Quality, trade::PendingTrade};
use common_net::sync::WorldSyncExt;
use conrod_core::{
color,
widget::{self, Button, Image, Rectangle, Scrollbar, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
widget::{self, Button, Image, Rectangle, Scrollbar, State as ConrodState, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon,
};
use inline_tweak::tweak;
use vek::*;
pub struct State {
ids: Ids,
@ -38,6 +41,10 @@ widget_ids! {
bg_frame,
trade_title_bg,
trade_title,
inv_alignment[],
inv_slots[],
offer_headers[],
phase_indicator,
}
}
@ -83,6 +90,160 @@ impl<'a> Trade<'a> {
}
}
impl<'a> Trade<'a> {
fn background(&mut self, state: &mut ConrodState<'_, State>, ui: &mut UiCell<'_>) {
Image::new(self.imgs.inv_bg_bag)
.w_h(tweak!(424.0), 708.0)
.middle()
.color(Some(UI_MAIN))
.set(state.ids.bg, ui);
Image::new(self.imgs.inv_frame_bag)
.w_h(tweak!(424.0), 708.0)
.middle_of(state.ids.bg)
.color(Some(UI_HIGHLIGHT_0))
.set(state.ids.bg_frame, ui);
}
fn title(&mut self, state: &mut ConrodState<'_, State>, ui: &mut UiCell<'_>) {
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);
}
fn phase_indicator(
&mut self,
state: &mut ConrodState<'_, State>,
ui: &mut UiCell<'_>,
trade: &'a PendingTrade,
) {
let phase_text = if trade.in_phase1() {
self.localized_strings.get("hud.trade.phase1_description")
} else if trade.in_phase2() {
self.localized_strings.get("hud.trade.phase2_description")
} else {
self.localized_strings.get("hud.trade.phase3_description")
};
Text::new(&phase_text)
.mid_top_with_margin_on(state.ids.bg, 70.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20))
.color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
.set(state.ids.phase_indicator, ui);
}
fn item_pane(
&mut self,
state: &mut ConrodState<'_, State>,
ui: &mut UiCell<'_>,
trade: &'a PendingTrade,
who: usize,
) -> <Self as Widget>::Event {
let inventories = self.client.inventories();
let uid = trade.parties[who];
let entity = self.client.state().ecs().entity_from_uid(uid.0)?;
let inventory = inventories.get(entity)?;
let mut slot_maker = SlotMaker {
empty_slot: self.imgs.inv_slot,
filled_slot: self.imgs.inv_slot,
selected_slot: self.imgs.inv_slot_sel,
background_color: Some(UI_MAIN),
content_size: ContentSize {
width_height_ratio: 1.0,
max_fraction: 0.75,
},
selected_content_scale: 1.067,
amount_font: self.fonts.cyri.conrod_id,
amount_margins: Vec2::new(-4.0, 0.0),
amount_font_size: self.fonts.cyri.scale(12),
amount_text_color: TEXT_COLOR,
content_source: inventory,
image_source: self.item_imgs,
slot_manager: Some(self.slot_manager),
};
// Alignment for Grid
let mut alignment = Rectangle::fill_with([200.0, 600.0], color::TRANSPARENT);
if who % 2 == 0 {
alignment =
alignment.top_left_with_margins_on(state.ids.bg, tweak!(160.0), tweak!(46.5));
} else {
alignment = alignment.right_from(state.ids.inv_alignment[0], 0.0);
}
alignment
.scroll_kids_vertically()
.set(state.ids.inv_alignment[who], ui);
Text::new(&format!("Player {}'s offer", who))
.up_from(state.ids.inv_alignment[who], 20.0)
.font_id(self.fonts.cyri.conrod_id)
.font_size(self.fonts.cyri.scale(20))
.color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
.set(state.ids.offer_headers[who], ui);
const MAX_TRADE_SLOTS: usize = 16;
if state.ids.inv_slots.len() < 2 * MAX_TRADE_SLOTS {
state.update(|s| {
s.ids
.inv_slots
.resize(2 * MAX_TRADE_SLOTS, &mut ui.widget_id_generator());
});
}
let mut invslots: Vec<_> = trade.offers[who].iter().map(|(k, v)| (k.clone(), v.clone())).collect();
invslots.sort();
let tradeslots: Vec<_> = invslots.into_iter().enumerate().map(|(index, (k, quantity))| TradeSlot { index, quantity, invslot: Some(k) }).collect();
for i in 0..MAX_TRADE_SLOTS {
let x = i % 4;
let y = i / 4;
let slot = tradeslots.get(i).cloned().unwrap_or(TradeSlot { index: i, quantity: 0, invslot: None, });
// Slot
let mut slot_widget = slot_maker
.fabricate(
slot.clone(),
[40.0; 2],
)
.top_left_with_margins_on(
state.ids.inv_alignment[who],
0.0 + y as f64 * (40.0),
0.0 + x as f64 * (40.0),
);
slot_widget.set(state.ids.inv_slots[i + who * MAX_TRADE_SLOTS], ui);
}
None
}
fn close_button(
&mut self,
state: &mut ConrodState<'_, State>,
ui: &mut UiCell<'_>,
) -> <Self as Widget>::Event {
if Button::image(self.imgs.close_btn)
.w_h(tweak!(24.0), 25.0)
.hover_image(self.imgs.close_btn_hover)
.press_image(self.imgs.close_btn_press)
.top_right_with_margins_on(state.ids.bg, 0.0, 0.0)
.set(state.ids.trade_close, ui)
.was_clicked()
{
Some(Event::Close)
} else {
None
}
}
}
impl<'a> Widget for Trade<'a> {
type Event = Option<Event>;
type State = State;
@ -96,17 +257,26 @@ impl<'a> Widget for Trade<'a> {
fn style(&self) -> Self::Style {}
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { state, ui, .. } = args;
fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs { mut state, ui, .. } = args;
let mut event = None;
let inventories = self.client.inventories();
let inventory = match inventories.get(self.client.entity()) {
Some(l) => l,
None => return None,
let trade = match self.client.pending_trade() {
Some((_, trade)) => trade,
None => return Some(Event::Close),
};
if state.ids.inv_alignment.len() < 2 {
state.update(|s| {
s.ids.inv_alignment.resize(2, &mut ui.widget_id_generator());
});
}
if state.ids.offer_headers.len() < 2 {
state.update(|s| {
s.ids.offer_headers.resize(2, &mut ui.widget_id_generator());
});
}
let trade_tooltip = Tooltip::new({
// Edge images [t, b, r, l]
// Corner images [tr, tl, br, bl]
@ -125,41 +295,14 @@ impl<'a> Widget for Trade<'a> {
.font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR);
// BG
Image::new(self.imgs.inv_bg_bag)
.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);
// 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);
self.background(&mut state, ui);
self.title(&mut state, ui);
self.phase_indicator(&mut state, ui, &trade);
event = self.item_pane(&mut state, ui, &trade, 0).or(event);
event = self.item_pane(&mut state, ui, &trade, 1).or(event);
// Close button
if Button::image(self.imgs.close_btn)
.w_h(24.0, 25.0)
.hover_image(self.imgs.close_btn_hover)
.press_image(self.imgs.close_btn_press)
.top_right_with_margins_on(state.ids.bg, 0.0, 0.0)
.set(state.ids.trade_close, ui)
.was_clicked()
{
event = Some(Event::Close);
}
event = self.close_button(&mut state, ui).or(event);
event
}

View File

@ -1144,6 +1144,10 @@ impl PlayState for SessionState {
info!("Event! -> ChangedHotbarState")
},
HudEvent::TradeAction(msg) => {
let mut client = self.client.borrow_mut();
client.trade_action(msg);
},
HudEvent::Ability3(state) => self.inputs.ability3.set_state(state),
HudEvent::ChangeFOV(new_fov) => {
global_state.settings.graphics.fov = new_fov;