diff --git a/CHANGELOG.md b/CHANGELOG.md index dea078354c..a1ac007634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Trades now display item prices in tooltips. - Admin designated build areas - Indicator text to collectable terrain sprites +- You can now autorequest exact change by ctrl-clicking in a trade, and can quick-add individual items with shift-click. ### Changed diff --git a/common/src/trade.rs b/common/src/trade.rs index 249702c174..33aa6dae32 100644 --- a/common/src/trade.rs +++ b/common/src/trade.rs @@ -1,5 +1,5 @@ use crate::{ - comp::inventory::{slot::InvSlotId, Inventory}, + comp::inventory::{slot::InvSlotId, trade_pricing::TradePricing, Inventory}, terrain::BiomeKind, uid::Uid, }; @@ -349,6 +349,35 @@ pub struct SitePrices { pub values: HashMap, } +impl SitePrices { + pub fn balance( + &self, + offers: &[HashMap; 2], + inventories: &[Option; 2], + who: usize, + reduce: bool, + ) -> f32 { + offers[who] + .iter() + .map(|(slot, amount)| { + inventories[who] + .as_ref() + .map(|ri| { + ri.inventory.get(slot).map(|item| { + let (material, factor) = TradePricing::get_material(&item.name); + self.values.get(&material).cloned().unwrap_or_default() + * factor + * (*amount as f32) + * if reduce { material.trade_margin() } else { 1.0 } + }) + }) + .flatten() + .unwrap_or_default() + }) + .sum() + } +} + #[derive(Clone, Debug, Default)] pub struct ReducedInventoryItem { pub name: String, diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 89f917b3f8..b4777ae6a4 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -7,7 +7,7 @@ use common::{ compass::{Direction, Distance}, dialogue::{MoodContext, MoodState, Subject}, group, - inventory::{item::ItemTag, slot::EquipSlot, trade_pricing::TradePricing}, + inventory::{item::ItemTag, slot::EquipSlot}, invite::{InviteKind, InviteResponse}, item::{ tool::{ToolKind, UniqueKind}, @@ -1150,33 +1150,9 @@ impl<'a> AgentData<'a> { let (tradeid, pending, prices, inventories) = *boxval; if agent.trading { let who: usize = if agent.trading_issuer { 0 } else { 1 }; - let balance = |who: usize, reduce: bool| { - pending.offers[who] - .iter() - .map(|(slot, amount)| { - inventories[who] - .as_ref() - .map(|ri| { - ri.inventory.get(slot).map(|item| { - let (material, factor) = - TradePricing::get_material(&item.name); - prices - .values - .get(&material) - .cloned() - .unwrap_or_default() - * factor - * (*amount as f32) - * if reduce { material.trade_margin() } else { 1.0 } - }) - }) - .flatten() - .unwrap_or_default() - }) - .sum() - }; - let balance0: f32 = balance(1 - who, true); - let balance1: f32 = balance(who, false); + let balance0: f32 = + prices.balance(&pending.offers, &inventories, 1 - who, true); + let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); tracing::debug!("UpdatePendingTrade({}, {})", balance0, balance1); if balance0 >= balance1 { // If the trade is favourable to us, only send an accept message if we're diff --git a/voxygen/src/hud/bag.rs b/voxygen/src/hud/bag.rs index 703ec6b0b8..fd7bd1834c 100644 --- a/voxygen/src/hud/bag.rs +++ b/voxygen/src/hud/bag.rs @@ -30,6 +30,7 @@ use conrod_core::{ }; use crate::hud::slots::SlotKind; +use specs::Entity as EcsEntity; use std::sync::Arc; use vek::Vec2; @@ -78,6 +79,7 @@ pub struct InventoryScroller<'a> { on_right: bool, item_tooltip: &'a ItemTooltip<'a>, playername: String, + entity: EcsEntity, is_us: bool, inventory: &'a Inventory, bg_ids: &'a BackgroundIds, @@ -99,6 +101,7 @@ impl<'a> InventoryScroller<'a> { on_right: bool, item_tooltip: &'a ItemTooltip<'a>, playername: String, + entity: EcsEntity, is_us: bool, inventory: &'a Inventory, bg_ids: &'a BackgroundIds, @@ -118,6 +121,7 @@ impl<'a> InventoryScroller<'a> { on_right, item_tooltip, playername, + entity, is_us, inventory, bg_ids, @@ -274,6 +278,7 @@ impl<'a> InventoryScroller<'a> { InventorySlot { slot: pos, ours: self.is_us, + entity: self.entity, }, [40.0; 2], ) @@ -616,6 +621,7 @@ impl<'a> Widget for Bag<'a> { true, &item_tooltip, self.stats.name.to_string(), + self.client.entity(), true, &inventory, &state.bg_ids, diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 2360fed373..413ee7b185 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -66,19 +66,23 @@ use common::{ combat, comp::{ self, + inventory::trade_pricing::TradePricing, item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality}, skills::{Skill, SkillGroupKind}, BuffKind, Item, }, outcome::Outcome, terrain::TerrainChunk, - trade::TradeAction, + trade::{ReducedInventory, TradeAction}, uid::Uid, util::srgba_to_linear, vol::RectRasterableVol, }; use common_base::span; -use common_net::msg::{world_msg::SiteId, Notification, PresenceKind}; +use common_net::{ + msg::{world_msg::SiteId, Notification, PresenceKind}, + sync::WorldSyncExt, +}; use conrod_core::{ text::cursor::Index, widget::{self, Button, Image, Text}, @@ -2901,11 +2905,13 @@ impl Hud { } // Maintain slot manager - for event in self.slot_manager.maintain(ui_widgets) { + 'slot_events: for event in self.slot_manager.maintain(ui_widgets) { use comp::slot::Slot; use slots::{InventorySlot, SlotKind::*}; let to_slot = |slot_kind| match slot_kind { - Inventory(InventorySlot { slot, ours: true }) => Some(Slot::Inventory(slot)), + Inventory(InventorySlot { + slot, ours: true, .. + }) => Some(Slot::Inventory(slot)), Inventory(InventorySlot { ours: false, .. }) => None, Equip(e) => Some(Slot::Equip(e)), Hotbar(_) => None, @@ -2920,8 +2926,12 @@ impl Hud { slot_b: b, bypass_dialog: false, }); - } else if let (Inventory(InventorySlot { slot, ours: true }), Hotbar(h)) = - (a, b) + } else if let ( + Inventory(InventorySlot { + slot, ours: true, .. + }), + Hotbar(h), + ) = (a, b) { self.hotbar.add_inventory_link(h, slot); events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); @@ -3042,6 +3052,127 @@ impl Hud { }); } }, + slot::Event::Request { + slot, + auto_quantity, + } => { + if let Some((_, trade, prices)) = client.pending_trade() { + let ecs = client.state().ecs(); + let inventories = ecs.read_component::(); + let get_inventory = |uid: Uid| { + if let Some(entity) = ecs.entity_from_uid(uid.0) { + inventories.get(entity) + } else { + None + } + }; + let mut r_inventories = [None, None]; + for (i, party) in trade.parties.iter().enumerate() { + match get_inventory(*party) { + Some(inventory) => { + r_inventories[i] = Some(ReducedInventory::from(inventory)) + }, + None => continue 'slot_events, + }; + } + let who = match ecs + .uid_from_entity(client.entity()) + .and_then(|uid| trade.which_party(uid)) + { + Some(who) => who, + None => continue 'slot_events, + }; + let do_auto_quantity = + |inventory: &common::comp::Inventory, + slot, + ours, + remove, + quantity: &mut u32| { + if let Some(prices) = prices { + let balance0 = + prices.balance(&trade.offers, &r_inventories, who, true); + let balance1 = prices.balance( + &trade.offers, + &r_inventories, + 1 - who, + false, + ); + if let Some(item) = inventory.get(slot) { + let (material, factor) = + TradePricing::get_material(item.item_definition_id()); + let mut unit_price = prices + .values + .get(&material) + .cloned() + .unwrap_or_default() + * factor; + if ours { + unit_price *= material.trade_margin(); + } + let mut float_delta = if ours ^ remove { + (balance1 - balance0) / unit_price + } else { + (balance0 - balance1) / unit_price + }; + if ours ^ remove { + float_delta = float_delta.ceil(); + } else { + float_delta = float_delta.floor(); + } + *quantity = float_delta.max(0.0) as u32; + } + } + }; + match slot { + Inventory(i) => { + if let Some(inventory) = inventories.get(i.entity) { + let mut quantity = 1; + if auto_quantity { + do_auto_quantity( + inventory, + i.slot, + i.ours, + false, + &mut quantity, + ); + let inv_quantity = i.amount(inventory).unwrap_or(1); + quantity = quantity.min(inv_quantity); + } + + events.push(Event::TradeAction(TradeAction::AddItem { + item: i.slot, + quantity, + ours: i.ours, + })); + } + }, + Trade(t) => { + if let Some(inventory) = inventories.get(t.entity) { + if let Some(invslot) = t.invslot { + let mut quantity = 1; + if auto_quantity { + do_auto_quantity( + inventory, + invslot, + t.ours, + true, + &mut quantity, + ); + let inv_quantity = t.amount(inventory).unwrap_or(1); + quantity = quantity.min(inv_quantity); + } + events.push(Event::TradeAction(TradeAction::RemoveItem { + item: invslot, + quantity, + ours: t.ours, + })); + } + } + }, + _ => {}, + } + } + }, } } self.hotbar.maintain_ability3(client); @@ -3096,6 +3227,7 @@ impl Hud { if let Some(slots::SlotKind::Inventory(InventorySlot { slot: i, ours: true, + .. })) = slot_manager.selected() { hotbar.add_inventory_link(slot, i); diff --git a/voxygen/src/hud/slots.rs b/voxygen/src/hud/slots.rs index 37353ab5b9..fe28b7e1a1 100644 --- a/voxygen/src/hud/slots.rs +++ b/voxygen/src/hud/slots.rs @@ -31,6 +31,7 @@ pub type SlotManager = slot::SlotManager; #[derive(Clone, Copy, Debug, PartialEq)] pub struct InventorySlot { pub slot: InvSlotId, + pub entity: EcsEntity, pub ours: bool, } @@ -85,6 +86,7 @@ impl SlotKey for TradeSlot { InventorySlot { slot: inv_id, ours: self.ours, + entity: self.entity, } .image_key(source) }) @@ -96,6 +98,7 @@ impl SlotKey for TradeSlot { InventorySlot { slot: inv_id, ours: self.ours, + entity: self.entity, } .amount(source) }) diff --git a/voxygen/src/hud/trade.rs b/voxygen/src/hud/trade.rs index 04af1189bc..3a506f5911 100644 --- a/voxygen/src/hud/trade.rs +++ b/voxygen/src/hud/trade.rs @@ -305,6 +305,7 @@ impl<'a> Trade<'a> { false, &item_tooltip, name, + entity, false, &inventory, &state.bg_ids, diff --git a/voxygen/src/ui/widgets/slot.rs b/voxygen/src/ui/widgets/slot.rs index 2a936c6712..055655a3ac 100644 --- a/voxygen/src/ui/widgets/slot.rs +++ b/voxygen/src/ui/widgets/slot.rs @@ -2,7 +2,7 @@ use crate::hud::animate_by_pulse; use conrod_core::{ builder_methods, image, - input::state::mouse, + input::{keyboard::ModifierKey, state::mouse}, text::font, widget::{self, Image, Text}, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, @@ -122,6 +122,8 @@ pub enum Event { SplitDragged(K, K), // Clicked while selected Used(K), + // {Shift,Ctrl}-clicked + Request { slot: K, auto_quantity: bool }, } // Handles interactions with slots pub struct SlotManager { @@ -369,6 +371,30 @@ where }; } + // Translate ctrl-clicks to stack-requests and shift-clicks to + // individual-requests + if let Some(click) = input.clicks().left().next() { + if !matches!(self.state, ManagerState::Dragging(_, _, _, _)) { + match click.modifiers { + ModifierKey::CTRL => { + self.events.push(Event::Request { + slot, + auto_quantity: true, + }); + self.state = ManagerState::Idle; + }, + ModifierKey::SHIFT => { + self.events.push(Event::Request { + slot, + auto_quantity: false, + }); + self.state = ManagerState::Idle; + }, + _ => {}, + } + } + } + // Use on right click if not dragging if input.clicks().right().next().is_some() { match self.state {