Get SitePricing information to clients, and use it to display coin-denominated prices in voxygen on tooltips during a trade. Also boost merchant spawn rate slightly.

This commit is contained in:
Avi Weinstock 2021-03-25 00:35:33 -04:00
parent 28952f6d7b
commit 8d90548331
13 changed files with 131 additions and 45 deletions

View File

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

View File

@ -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<Uid>,
// The pending trade the client is involved in, and it's id
pending_trade: Option<(TradeId, PendingTrade)>,
pending_trade: Option<(TradeId, PendingTrade, Option<SitePrices>)>,
_network: Network,
participant: Option<Participant>,
@ -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<Uid> { &self.pending_invites }
pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade)> { &self.pending_trade }
pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade, Option<SitePrices>)> {
&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 })
}

View File

@ -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<SitePrices>),
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()

View File

@ -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<Site> but that is from the world crate
pub type SiteId = u64;
@ -329,7 +344,7 @@ pub struct SiteInformation {
pub unconsumed_stock: HashMap<Good, f32>,
}
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SitePrices {
pub values: HashMap<Good, f32>,
}

View File

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

View File

@ -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::<Client>();
let uids = state.ecs().read_storage::<Uid>();
let agents = state.ecs().read_storage::<Agent>();
let mut invites = state.ecs().write_storage::<Invite>();
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::<Trades>();
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)));
}
},
}

View File

@ -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<specs::Entity>; 2] = [None, None];
let mut inventories: [Option<ReducedInventory>; 2] = [None, None];
let mut prices = None;
let agents = server.state.ecs().read_storage::<Agent>();
// 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::<Inventory>()
.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::<Agent>(),
@ -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(),
))),
);

View File

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

View File

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

View File

@ -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<SitePrices>,
ours: bool,
) -> <Self as Widget>::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<SitePrices>,
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);

View File

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

View File

@ -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<Arc<Colors>> { self.colors }
pub fn get_site_prices(&self, agent: &Agent) -> Option<SitePrices> {
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 {

View File

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