diff --git a/CHANGELOG.md b/CHANGELOG.md index a10ea27e91..36a1ad083b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pickaxes (can be used to collect gems and mine weak rock) - You can now jump out of rolls for a slight jump boost - Dungeons now have multiple kinds of stairs. +- Trades now display item prices in tooltips. ### Changed diff --git a/client/src/lib.rs b/client/src/lib.rs index 4066c36bc4..2ce9b5f54d 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -35,7 +35,7 @@ use common::{ recipe::RecipeBook, resources::PlayerEntity, terrain::{block::Block, neighbors, BiomeKind, SitesKind, TerrainChunk, TerrainChunkSize}, - trade::{PendingTrade, TradeAction, TradeId, TradeResult}, + trade::{PendingTrade, SitePrices, TradeAction, TradeId, TradeResult}, uid::{Uid, UidAllocator}, vol::RectVolSize, }; @@ -156,7 +156,7 @@ pub struct Client { // Pending invites that this client has sent out pending_invites: HashSet, // The pending trade the client is involved in, and it's id - pending_trade: Option<(TradeId, PendingTrade)>, + pending_trade: Option<(TradeId, PendingTrade, Option)>, _network: Network, participant: Option, @@ -694,7 +694,7 @@ impl Client { } pub fn perform_trade_action(&mut self, action: TradeAction) { - if let Some((id, _)) = self.pending_trade { + if let Some((id, _, _)) = self.pending_trade { if let TradeAction::Decline = action { self.pending_trade.take(); } @@ -833,7 +833,9 @@ impl Client { pub fn pending_invites(&self) -> &HashSet { &self.pending_invites } - pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade)> { &self.pending_trade } + pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade, Option)> { + &self.pending_trade + } pub fn send_invite(&mut self, invitee: Uid, kind: InviteKind) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateInvite( @@ -1637,12 +1639,12 @@ impl Client { impulse, }); }, - ServerGeneral::UpdatePendingTrade(id, trade) => { + ServerGeneral::UpdatePendingTrade(id, trade, pricing) => { tracing::trace!("UpdatePendingTrade {:?} {:?}", id, trade); - self.pending_trade = Some((id, trade)); + self.pending_trade = Some((id, trade, pricing)); }, ServerGeneral::FinishedTrade(result) => { - if let Some((_, trade)) = self.pending_trade.take() { + if let Some((_, trade, _)) = self.pending_trade.take() { self.update_available_recipes(); frontend_events.push(Event::TradeComplete { result, trade }) } diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index b497bcd9c3..176869cf94 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -8,7 +8,7 @@ use common::{ recipe::RecipeBook, resources::TimeOfDay, terrain::{Block, TerrainChunk}, - trade::{PendingTrade, TradeId, TradeResult}, + trade::{PendingTrade, SitePrices, TradeId, TradeResult}, uid::Uid, uuid::Uuid, }; @@ -126,7 +126,7 @@ pub enum ServerGeneral { Disconnect(DisconnectReason), /// Send a popup notification such as "Waypoint Saved" Notification(Notification), - UpdatePendingTrade(TradeId, PendingTrade), + UpdatePendingTrade(TradeId, PendingTrade, Option), FinishedTrade(TradeResult), /// Economic information about sites SiteEconomy(EconomyInfo), @@ -237,7 +237,7 @@ impl ServerMsg { | ServerGeneral::SetViewDistance(_) | ServerGeneral::Outcomes(_) | ServerGeneral::Knockback(_) - | ServerGeneral::UpdatePendingTrade(_, _) + | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) | ServerGeneral::SiteEconomy(_) => { c_type == ClientType::Game && presence.is_some() diff --git a/common/src/trade.rs b/common/src/trade.rs index a5ec16a065..249702c174 100644 --- a/common/src/trade.rs +++ b/common/src/trade.rs @@ -320,6 +320,21 @@ impl Default for Good { } } +impl Good { + /// The discounting factor applied when selling goods back to a merchant + pub fn trade_margin(&self) -> f32 { + match self { + Good::Tools | Good::Armor => 0.5, + Good::Food | Good::Potions | Good::Ingredients => 0.75, + Good::Coin => 1.0, + // Certain abstract goods (like Territory) shouldn't be attached to concrete items; + // give a sale price of 0 if the player is trying to sell a concrete item that somehow + // has one of these categories + _ => 0.0, + } + } +} + // ideally this would be a real Id but that is from the world crate pub type SiteId = u64; @@ -329,7 +344,7 @@ pub struct SiteInformation { pub unconsumed_stock: HashMap, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct SitePrices { pub values: HashMap, } diff --git a/server/src/client.rs b/server/src/client.rs index 3abbf49c9a..2db4c6d43c 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -91,7 +91,7 @@ impl Client { | ServerGeneral::SiteEconomy(_) | ServerGeneral::Outcomes(_) | ServerGeneral::Knockback(_) - | ServerGeneral::UpdatePendingTrade(_, _) + | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) => { self.in_game_stream.lock().unwrap().send(g) }, @@ -162,7 +162,7 @@ impl Client { | ServerGeneral::Outcomes(_) | ServerGeneral::Knockback(_) | ServerGeneral::SiteEconomy(_) - | ServerGeneral::UpdatePendingTrade(_, _) + | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) => { PreparedMsg::new(2, &g, &self.in_game_stream) }, diff --git a/server/src/events/invite.rs b/server/src/events/invite.rs index d7dac789b4..b67a3b0d89 100644 --- a/server/src/events/invite.rs +++ b/server/src/events/invite.rs @@ -3,7 +3,7 @@ use crate::{client::Client, Server}; use common::{ comp::{ self, - agent::AgentEvent, + agent::{Agent, AgentEvent}, group::GroupManager, invite::{Invite, InviteKind, InviteResponse, PendingInvites}, ChatType, @@ -162,9 +162,11 @@ pub fn handle_invite_response( } pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) { + let index = server.index.clone(); let state = server.state_mut(); let clients = state.ecs().read_storage::(); let uids = state.ecs().read_storage::(); + let agents = state.ecs().read_storage::(); let mut invites = state.ecs().write_storage::(); if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| { let Invite { inviter, kind } = invite; @@ -216,12 +218,20 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) { let mut trades = state.ecs().write_resource::(); let id = trades.begin_trade(inviter_uid, invitee_uid); let trade = trades.trades[&id].clone(); - clients + let pricing = agents .get(inviter) - .map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade.clone()))); + .and_then(|a| index.get_site_prices(a)) + .or_else(|| agents.get(entity).and_then(|a| index.get_site_prices(a))); + clients.get(inviter).map(|c| { + c.send(ServerGeneral::UpdatePendingTrade( + id, + trade.clone(), + pricing.clone(), + )) + }); clients .get(entity) - .map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade))); + .map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade, pricing))); } }, } diff --git a/server/src/events/trade.rs b/server/src/events/trade.rs index ef91cfb9fe..ae39974cc6 100644 --- a/server/src/events/trade.rs +++ b/server/src/events/trade.rs @@ -33,19 +33,18 @@ fn notify_agent_prices( event: AgentEvent, ) { if let Some(agent) = agents.get_mut(entity) { + let prices = index.get_site_prices(agent); if let AgentEvent::UpdatePendingTrade(boxval) = event { // Box<(tid, pend, _, inventories)>) = event { - let prices = agent - .trade_for_site - .map(|i| index.sites.recreate_id(i)) - .flatten() - .map(|i| index.sites.get(i)) - .map(|s| s.economy.get_site_prices()) - .unwrap_or_default(); agent .inbox .push_front(AgentEvent::UpdatePendingTrade(Box::new(( - boxval.0, boxval.1, prices, boxval.3, + // Prefer using this Agent's price data, but use the counterparty's price data + // if we don't have price data + boxval.0, + boxval.1, + prices.unwrap_or(boxval.2), + boxval.3, )))); } } @@ -103,6 +102,8 @@ pub fn handle_process_trade_action( } else { let mut entities: [Option; 2] = [None, None]; let mut inventories: [Option; 2] = [None, None]; + let mut prices = None; + let agents = server.state.ecs().read_storage::(); // sadly there is no map and collect on arrays for i in 0..2 { // parties.len()) { @@ -114,13 +115,23 @@ pub fn handle_process_trade_action( .read_component::() .get(e) .map(|i| ReducedInventory::from(i)); + // Get price info from the first Agent in the trade (currently, an + // Agent will never initiate a trade with another agent though) + prices = prices.or_else(|| { + agents.get(e).and_then(|a| server.index.get_site_prices(a)) + }); } } + drop(agents); for party in entities.iter() { if let Some(e) = *party { server.notify_client( e, - ServerGeneral::UpdatePendingTrade(trade_id, entry.get().clone()), + ServerGeneral::UpdatePendingTrade( + trade_id, + entry.get().clone(), + prices.clone(), + ), ); notify_agent_prices( server.state.ecs().write_storage::(), @@ -129,7 +140,7 @@ pub fn handle_process_trade_action( AgentEvent::UpdatePendingTrade(Box::new(( trade_id, entry.get().clone(), - Default::default(), + prices.clone().unwrap_or_default(), inventories.clone(), ))), ); diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 3f4c7b1009..a2c3e2952f 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -22,7 +22,7 @@ use common::{ rtsim::{Memory, MemoryItem, RtSimEntity, RtSimEvent}, terrain::{Block, TerrainGrid}, time::DayPeriod, - trade::{Good, TradeAction, TradePhase, TradeResult}, + trade::{TradeAction, TradePhase, TradeResult}, uid::{Uid, UidAllocator}, util::Dir, vol::ReadVol, @@ -947,14 +947,6 @@ impl<'a> AgentData<'a> { // This needs revisiting when agents can initiate trades (e.g. to offer // mercenary contracts as quests) const WHO: usize = 1; - fn trade_margin(g: Good) -> f32 { - match g { - Good::Tools | Good::Armor => 0.5, - Good::Food | Good::Potions | Good::Ingredients => 0.75, - Good::Coin => 1.0, - _ => 0.0, // what is this? - } - } let balance = |who: usize, reduce: bool| { pending.offers[who] .iter() @@ -972,7 +964,7 @@ impl<'a> AgentData<'a> { .unwrap_or_default() * factor * (*amount as f32) - * if reduce { trade_margin(material) } else { 1.0 } + * if reduce { material.trade_margin() } else { 1.0 } }) }) .flatten() diff --git a/voxygen/src/hud/bag.rs b/voxygen/src/hud/bag.rs index 9a71b5e5fe..ec551dbcc8 100644 --- a/voxygen/src/hud/bag.rs +++ b/voxygen/src/hud/bag.rs @@ -64,6 +64,7 @@ pub struct InventoryScrollerState { #[derive(WidgetCommon)] pub struct InventoryScroller<'a> { + client: &'a Client, imgs: &'a Imgs, item_imgs: &'a ItemImgs, fonts: &'a Fonts, @@ -87,6 +88,7 @@ pub struct InventoryScroller<'a> { impl<'a> InventoryScroller<'a> { #[allow(clippy::too_many_arguments)] pub fn new( + client: &'a Client, imgs: &'a Imgs, item_imgs: &'a ItemImgs, fonts: &'a Fonts, @@ -105,6 +107,7 @@ impl<'a> InventoryScroller<'a> { bg_ids: &'a BackgroundIds, ) -> Self { InventoryScroller { + client, imgs, item_imgs, fonts, @@ -302,6 +305,11 @@ impl<'a> InventoryScroller<'a> { Quality::Artifact => self.imgs.inv_slot_orange, _ => self.imgs.inv_slot_red, }; + let mut desc = desc.to_string(); + if let Some((_, _, prices)) = self.client.pending_trade() { + super::util::append_price_desc(&mut desc, prices, item.item_definition_id()); + } + slot_widget .filled_slot(quality_col_img) .with_tooltip( @@ -571,6 +579,7 @@ impl<'a> Widget for Bag<'a> { .desc_text_color(TEXT_COLOR); InventoryScroller::new( + self.client, self.imgs, self.item_imgs, self.fonts, diff --git a/voxygen/src/hud/trade.rs b/voxygen/src/hud/trade.rs index e656911356..cc75d28698 100644 --- a/voxygen/src/hud/trade.rs +++ b/voxygen/src/hud/trade.rs @@ -20,7 +20,7 @@ use common::{ inventory::item::{MaterialStatManifest, Quality}, Inventory, }, - trade::{PendingTrade, TradeAction, TradePhase}, + trade::{PendingTrade, SitePrices, TradeAction, TradePhase}, }; use common_net::sync::WorldSyncExt; use conrod_core::{ @@ -156,6 +156,7 @@ impl<'a> Trade<'a> { state: &mut ConrodState<'_, State>, ui: &mut UiCell<'_>, trade: &'a PendingTrade, + prices: &'a Option, ours: bool, ) -> ::Event { let inventories = self.client.inventories(); @@ -233,7 +234,17 @@ impl<'a> Trade<'a> { .collect(); if matches!(trade.phase(), TradePhase::Mutate) { - self.phase1_itemwidget(state, ui, inventory, who, ours, entity, name, &tradeslots); + self.phase1_itemwidget( + state, + ui, + inventory, + who, + ours, + entity, + name, + prices, + &tradeslots, + ); } else { self.phase2_itemwidget(state, ui, inventory, who, ours, entity, &tradeslots); } @@ -250,6 +261,7 @@ impl<'a> Trade<'a> { ours: bool, entity: EcsEntity, name: String, + prices: &'a Option, tradeslots: &[TradeSlot], ) { let item_tooltip = Tooltip::new({ @@ -272,6 +284,7 @@ impl<'a> Trade<'a> { if !ours { InventoryScroller::new( + self.client, self.imgs, self.item_imgs, self.fonts, @@ -353,6 +366,8 @@ impl<'a> Trade<'a> { Quality::Artifact => self.imgs.inv_slot_orange, _ => self.imgs.inv_slot_red, }; + let mut desc = desc.to_string(); + super::util::append_price_desc(&mut desc, prices, item.item_definition_id()); slot_widget .filled_slot(quality_col_img) .with_tooltip( @@ -496,8 +511,8 @@ impl<'a> Widget for Trade<'a> { let widget::UpdateArgs { mut state, ui, .. } = args; let mut event = None; - let trade = match self.client.pending_trade() { - Some((_, trade)) => trade, + let (trade, prices) = match self.client.pending_trade() { + Some((_, trade, prices)) => (trade, prices), None => return Some(TradeAction::Decline), }; @@ -523,8 +538,12 @@ impl<'a> Widget for Trade<'a> { self.title(&mut state, ui); self.phase_indicator(&mut state, ui, &trade); - event = self.item_pane(&mut state, ui, &trade, false).or(event); - event = self.item_pane(&mut state, ui, &trade, true).or(event); + event = self + .item_pane(&mut state, ui, &trade, &prices, false) + .or(event); + event = self + .item_pane(&mut state, ui, &trade, &prices, true) + .or(event); event = self .accept_decline_buttons(&mut state, ui, &trade) .or(event); diff --git a/voxygen/src/hud/util.rs b/voxygen/src/hud/util.rs index bf5d915cab..d4e48ada12 100644 --- a/voxygen/src/hud/util.rs +++ b/voxygen/src/hud/util.rs @@ -1,5 +1,6 @@ use common::{ comp::{ + inventory::trade_pricing::TradePricing, item::{ armor::{Armor, ArmorKind, Protection}, tool::{Hands, StatKind, Stats, Tool, ToolKind}, @@ -8,6 +9,7 @@ use common::{ BuffKind, }, effect::Effect, + trade::{Good, SitePrices}, }; use std::{borrow::Cow, fmt::Write}; @@ -64,6 +66,20 @@ pub fn item_text<'a>( (item.name(), desc) } +pub fn append_price_desc(desc: &mut String, prices: &Option, item_definition_id: &str) { + if let Some(prices) = prices { + let (material, factor) = TradePricing::get_material(item_definition_id); + let coinprice = prices.values.get(&Good::Coin).cloned().unwrap_or(1.0); + let buyprice = prices.values.get(&material).cloned().unwrap_or_default() * factor; + let sellprice = buyprice * material.trade_margin(); + *desc += &format!( + "\n\nBuy price: {:0.1} coins\nSell price: {:0.1} coins", + buyprice / coinprice, + sellprice / coinprice + ); + } +} + // TODO: localization fn modular_component_desc( mc: &ModularComponent, diff --git a/world/src/index.rs b/world/src/index.rs index c88e0f8859..feaef98cf0 100644 --- a/world/src/index.rs +++ b/world/src/index.rs @@ -4,7 +4,9 @@ use crate::{ }; use common::{ assets::{AssetExt, AssetHandle}, + comp::Agent, store::Store, + trade::SitePrices, }; use core::ops::Deref; use noise::{Seedable, SuperSimplex}; @@ -69,6 +71,15 @@ impl Index { } pub fn colors(&self) -> AssetHandle> { self.colors } + + pub fn get_site_prices(&self, agent: &Agent) -> Option { + agent + .trade_for_site + .map(|i| self.sites.recreate_id(i)) + .flatten() + .map(|i| self.sites.get(i)) + .map(|s| s.economy.get_site_prices()) + } } impl IndexOwned { diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index 0c297c3fa7..0cecda5ac6 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -944,7 +944,7 @@ impl Settlement { .do_if(!is_dummy, |e| e.with_automatic_name()) .do_if(is_dummy, |e| e.with_name("Training Dummy")) .do_if(is_human && dynamic_rng.gen(), |entity| { - match dynamic_rng.gen_range(0..5) { + match dynamic_rng.gen_range(0..6) { 0 => entity .with_main_tool(Item::new_from_asset_expect( "common.items.weapons.sword.iron-4", @@ -955,7 +955,7 @@ impl Settlement { .with_skillset_config( common::skillset_builder::SkillSetConfig::Guard, ), - 1 => entity + 1 | 2 => entity .with_main_tool(Item::new_from_asset_expect( "common.items.weapons.bow.eldwood-0", ))