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.
- 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

View File

@ -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<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)]
pub struct ReducedInventoryItem {
pub name: String,

View File

@ -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

View File

@ -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,

View File

@ -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::<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);
@ -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);

View File

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

View File

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

View File

@ -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<K> {
SplitDragged(K, K),
// Clicked while selected
Used(K),
// {Shift,Ctrl}-clicked
Request { slot: K, auto_quantity: bool },
}
// Handles interactions with slots
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
if input.clicks().right().next().is_some() {
match self.state {