Allow fast stacking into and out of a trade with {ctrl,shift} click.

Shift click goes 1 at a time, Ctrl click automatically balances the trade w.r.t. that quantity.
This commit is contained in:
Avi Weinstock 2021-03-30 18:37:38 -04:00
parent aafdc209d3
commit 0122dca3c3
8 changed files with 210 additions and 36 deletions

View File

@ -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. - Trades now display item prices in tooltips.
- Admin designated build areas - Admin designated build areas
- Indicator text to collectable terrain sprites - 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 ### Changed

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
comp::inventory::{slot::InvSlotId, Inventory}, comp::inventory::{slot::InvSlotId, trade_pricing::TradePricing, Inventory},
terrain::BiomeKind, terrain::BiomeKind,
uid::Uid, uid::Uid,
}; };
@ -349,6 +349,35 @@ pub struct SitePrices {
pub values: HashMap<Good, f32>, pub values: HashMap<Good, f32>,
} }
impl SitePrices {
pub fn balance(
&self,
offers: &[HashMap<InvSlotId, u32>; 2],
inventories: &[Option<ReducedInventory>; 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)] #[derive(Clone, Debug, Default)]
pub struct ReducedInventoryItem { pub struct ReducedInventoryItem {
pub name: String, pub name: String,

View File

@ -7,7 +7,7 @@ use common::{
compass::{Direction, Distance}, compass::{Direction, Distance},
dialogue::{MoodContext, MoodState, Subject}, dialogue::{MoodContext, MoodState, Subject},
group, group,
inventory::{item::ItemTag, slot::EquipSlot, trade_pricing::TradePricing}, inventory::{item::ItemTag, slot::EquipSlot},
invite::{InviteKind, InviteResponse}, invite::{InviteKind, InviteResponse},
item::{ item::{
tool::{ToolKind, UniqueKind}, tool::{ToolKind, UniqueKind},
@ -1150,33 +1150,9 @@ impl<'a> AgentData<'a> {
let (tradeid, pending, prices, inventories) = *boxval; let (tradeid, pending, prices, inventories) = *boxval;
if agent.trading { if agent.trading {
let who: usize = if agent.trading_issuer { 0 } else { 1 }; let who: usize = if agent.trading_issuer { 0 } else { 1 };
let balance = |who: usize, reduce: bool| { let balance0: f32 =
pending.offers[who] prices.balance(&pending.offers, &inventories, 1 - who, true);
.iter() let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false);
.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);
tracing::debug!("UpdatePendingTrade({}, {})", balance0, balance1); tracing::debug!("UpdatePendingTrade({}, {})", balance0, balance1);
if balance0 >= balance1 { if balance0 >= balance1 {
// If the trade is favourable to us, only send an accept message if we're // If the trade is favourable to us, only send an accept message if we're

View File

@ -30,6 +30,7 @@ use conrod_core::{
}; };
use crate::hud::slots::SlotKind; use crate::hud::slots::SlotKind;
use specs::Entity as EcsEntity;
use std::sync::Arc; use std::sync::Arc;
use vek::Vec2; use vek::Vec2;
@ -78,6 +79,7 @@ pub struct InventoryScroller<'a> {
on_right: bool, on_right: bool,
item_tooltip: &'a ItemTooltip<'a>, item_tooltip: &'a ItemTooltip<'a>,
playername: String, playername: String,
entity: EcsEntity,
is_us: bool, is_us: bool,
inventory: &'a Inventory, inventory: &'a Inventory,
bg_ids: &'a BackgroundIds, bg_ids: &'a BackgroundIds,
@ -99,6 +101,7 @@ impl<'a> InventoryScroller<'a> {
on_right: bool, on_right: bool,
item_tooltip: &'a ItemTooltip<'a>, item_tooltip: &'a ItemTooltip<'a>,
playername: String, playername: String,
entity: EcsEntity,
is_us: bool, is_us: bool,
inventory: &'a Inventory, inventory: &'a Inventory,
bg_ids: &'a BackgroundIds, bg_ids: &'a BackgroundIds,
@ -118,6 +121,7 @@ impl<'a> InventoryScroller<'a> {
on_right, on_right,
item_tooltip, item_tooltip,
playername, playername,
entity,
is_us, is_us,
inventory, inventory,
bg_ids, bg_ids,
@ -274,6 +278,7 @@ impl<'a> InventoryScroller<'a> {
InventorySlot { InventorySlot {
slot: pos, slot: pos,
ours: self.is_us, ours: self.is_us,
entity: self.entity,
}, },
[40.0; 2], [40.0; 2],
) )
@ -616,6 +621,7 @@ impl<'a> Widget for Bag<'a> {
true, true,
&item_tooltip, &item_tooltip,
self.stats.name.to_string(), self.stats.name.to_string(),
self.client.entity(),
true, true,
&inventory, &inventory,
&state.bg_ids, &state.bg_ids,

View File

@ -66,19 +66,23 @@ use common::{
combat, combat,
comp::{ comp::{
self, self,
inventory::trade_pricing::TradePricing,
item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality}, item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality},
skills::{Skill, SkillGroupKind}, skills::{Skill, SkillGroupKind},
BuffKind, Item, BuffKind, Item,
}, },
outcome::Outcome, outcome::Outcome,
terrain::TerrainChunk, terrain::TerrainChunk,
trade::TradeAction, trade::{ReducedInventory, TradeAction},
uid::Uid, uid::Uid,
util::srgba_to_linear, util::srgba_to_linear,
vol::RectRasterableVol, vol::RectRasterableVol,
}; };
use common_base::span; 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::{ use conrod_core::{
text::cursor::Index, text::cursor::Index,
widget::{self, Button, Image, Text}, widget::{self, Button, Image, Text},
@ -2901,11 +2905,13 @@ impl Hud {
} }
// Maintain slot manager // 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 comp::slot::Slot;
use slots::{InventorySlot, SlotKind::*}; use slots::{InventorySlot, SlotKind::*};
let to_slot = |slot_kind| match slot_kind { 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, Inventory(InventorySlot { ours: false, .. }) => None,
Equip(e) => Some(Slot::Equip(e)), Equip(e) => Some(Slot::Equip(e)),
Hotbar(_) => None, Hotbar(_) => None,
@ -2920,8 +2926,12 @@ impl Hud {
slot_b: b, slot_b: b,
bypass_dialog: false, bypass_dialog: false,
}); });
} else if let (Inventory(InventorySlot { slot, ours: true }), Hotbar(h)) = } else if let (
(a, b) Inventory(InventorySlot {
slot, ours: true, ..
}),
Hotbar(h),
) = (a, b)
{ {
self.hotbar.add_inventory_link(h, slot); self.hotbar.add_inventory_link(h, slot);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned()))); 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::<common::comp::Inventory>();
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); self.hotbar.maintain_ability3(client);
@ -3096,6 +3227,7 @@ impl Hud {
if let Some(slots::SlotKind::Inventory(InventorySlot { if let Some(slots::SlotKind::Inventory(InventorySlot {
slot: i, slot: i,
ours: true, ours: true,
..
})) = slot_manager.selected() })) = slot_manager.selected()
{ {
hotbar.add_inventory_link(slot, i); hotbar.add_inventory_link(slot, i);

View File

@ -31,6 +31,7 @@ pub type SlotManager = slot::SlotManager<SlotKind>;
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct InventorySlot { pub struct InventorySlot {
pub slot: InvSlotId, pub slot: InvSlotId,
pub entity: EcsEntity,
pub ours: bool, pub ours: bool,
} }
@ -85,6 +86,7 @@ impl SlotKey<Inventory, ItemImgs> for TradeSlot {
InventorySlot { InventorySlot {
slot: inv_id, slot: inv_id,
ours: self.ours, ours: self.ours,
entity: self.entity,
} }
.image_key(source) .image_key(source)
}) })
@ -96,6 +98,7 @@ impl SlotKey<Inventory, ItemImgs> for TradeSlot {
InventorySlot { InventorySlot {
slot: inv_id, slot: inv_id,
ours: self.ours, ours: self.ours,
entity: self.entity,
} }
.amount(source) .amount(source)
}) })

View File

@ -305,6 +305,7 @@ impl<'a> Trade<'a> {
false, false,
&item_tooltip, &item_tooltip,
name, name,
entity,
false, false,
&inventory, &inventory,
&state.bg_ids, &state.bg_ids,

View File

@ -2,7 +2,7 @@
use crate::hud::animate_by_pulse; use crate::hud::animate_by_pulse;
use conrod_core::{ use conrod_core::{
builder_methods, image, builder_methods, image,
input::state::mouse, input::{keyboard::ModifierKey, state::mouse},
text::font, text::font,
widget::{self, Image, Text}, widget::{self, Image, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
@ -122,6 +122,8 @@ pub enum Event<K> {
SplitDragged(K, K), SplitDragged(K, K),
// Clicked while selected // Clicked while selected
Used(K), Used(K),
// {Shift,Ctrl}-clicked
Request { slot: K, auto_quantity: bool },
} }
// Handles interactions with slots // Handles interactions with slots
pub struct SlotManager<S: SumSlot> { pub struct SlotManager<S: SumSlot> {
@ -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 // Use on right click if not dragging
if input.clicks().right().next().is_some() { if input.clicks().right().next().is_some() {
match self.state { match self.state {