From e2b55e07063d1623a7d746b06ac27e720ef82867 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 11 Feb 2021 21:53:25 -0500 Subject: [PATCH] Implement enough of a trade UI that dragging & dropping items into it round-trips between clients. --- assets/voxygen/i18n/en/hud/trade.ron | 4 + client/src/lib.rs | 6 + common/src/trade.rs | 11 +- server/src/client.rs | 4 +- server/src/events/interaction.rs | 6 +- server/src/events/mod.rs | 5 +- server/src/events/trade.rs | 30 ++-- voxygen/src/hud/mod.rs | 26 ++- voxygen/src/hud/slots.rs | 30 ++++ voxygen/src/hud/trade.rs | 235 +++++++++++++++++++++------ voxygen/src/session.rs | 4 + 11 files changed, 284 insertions(+), 77 deletions(-) diff --git a/assets/voxygen/i18n/en/hud/trade.ron b/assets/voxygen/i18n/en/hud/trade.ron index b199a27a7f..e12fbf4f15 100644 --- a/assets/voxygen/i18n/en/hud/trade.ron +++ b/assets/voxygen/i18n/en/hud/trade.ron @@ -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.", }, diff --git a/client/src/lib.rs b/client/src/lib.rs index d92d20615d..1240c16760 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -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( diff --git a/common/src/trade.rs b/common/src/trade.rs index 7f92479e10..5a43dbee34 100644 --- a/common/src/trade.rs +++ b/common/src/trade.rs @@ -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; 2], + pub offers: [HashMap; 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 diff --git a/server/src/client.rs b/server/src/client.rs index bf2fc504b5..a80413c1ec 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -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(_) diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index b134b25324..b3e0f5e763 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -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, }; diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index b965a06ad3..042c7a5c03 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -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}; diff --git a/server/src/events/trade.rs b/server/src/events/trade.rs index ee2b135f85..1f49cb8dbc 100644 --- a/server/src/events/trade.rs +++ b/server/src/events/trade.rs @@ -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::(); 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()), + ) + }); + } } } } diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 11a8c1dc74..6eed2ccd97 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -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), + 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) => { diff --git a/voxygen/src/hud/slots.rs b/voxygen/src/hud/slots.rs index 5acfd790c6..1aacf0375c 100644 --- a/voxygen/src/hud/slots.rs +++ b/voxygen/src/hud/slots.rs @@ -21,6 +21,7 @@ pub enum SlotKind { Inventory(InventorySlot), Equip(EquipSlot), Hotbar(HotbarSlot), + Trade(TradeSlot), /* Spellbook(SpellbookSlot), TODO */ } @@ -63,6 +64,32 @@ impl SlotKey for EquipSlot { } } +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct TradeSlot { + pub index: usize, + pub quantity: u32, + pub invslot: Option, +} + +impl SlotKey for TradeSlot { + type ImageKey = ItemKey; + + fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option)> { + self.invslot + .and_then(|inv_id| InventorySlot(inv_id).image_key(source)) + } + + fn amount(&self, source: &Inventory) -> Option { + 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 for SlotKind { impl From for SlotKind { fn from(hotbar: HotbarSlot) -> Self { Self::Hotbar(hotbar) } } +impl From for SlotKind { + fn from(trade: TradeSlot) -> Self { Self::Trade(trade) } +} impl SumSlot for SlotKind {} diff --git a/voxygen/src/hud/trade.rs b/voxygen/src/hud/trade.rs index 3cd697eeb3..d9c599ab1d 100644 --- a/voxygen/src/hud/trade.rs +++ b/voxygen/src/hud/trade.rs @@ -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, + ) -> ::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<'_>, + ) -> ::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; 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::Event { - let widget::UpdateArgs { state, ui, .. } = args; + fn update(mut self, args: widget::UpdateArgs) -> 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 } diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index b10a7f1d1d..7e56999739 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -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;