mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Diversify price calculation for items by using multiple categories per item.
This commit is contained in:
parent
dbc969251c
commit
f347b9de11
@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Improved site placement
|
- Improved site placement
|
||||||
- [Server] Kick clients who send messages on the wrong stream
|
- [Server] Kick clients who send messages on the wrong stream
|
||||||
|
- Reworked Merchant trade price calculation, Merchants offer more wares
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
@ -47,10 +47,9 @@ loot_tables: [
|
|||||||
// this is the amount of that good the most common item represents
|
// this is the amount of that good the most common item represents
|
||||||
// so basically this table balances the goods against each other (higher=less valuable)
|
// so basically this table balances the goods against each other (higher=less valuable)
|
||||||
good_scaling: [
|
good_scaling: [
|
||||||
(Potions, 0.005), // common.items.consumable.potion_minor
|
(Food, 0.0375), // common.items.food.mushroom
|
||||||
(Food, 0.15), // common.items.food.mushroom
|
|
||||||
(Coin, 1.0), // common.items.utility.coins
|
(Coin, 1.0), // common.items.utility.coins
|
||||||
(Armor, 0.5), // common.items.armor.misc.pants.worker_blue
|
(Armor, 0.025), // common.items.armor.misc.pants.worker_blue
|
||||||
(Tools, 0.5), // common.items.weapons.staff.starter_staff
|
(Tools, 0.015487), // common.items.weapons.staff.starter_staff
|
||||||
(Ingredients, 0.5), // common.items.crafting_ing.leather_scraps
|
(Ingredients, 0.034626), // common.items.crafting_ing.leather_scraps
|
||||||
])
|
])
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
assets::{self, AssetExt},
|
assets::{self, AssetExt},
|
||||||
|
comp::item::Item,
|
||||||
lottery::LootSpec,
|
lottery::LootSpec,
|
||||||
recipe::{default_recipe_book, RecipeInput},
|
recipe::{default_recipe_book, RecipeInput},
|
||||||
trade::Good,
|
trade::Good,
|
||||||
@ -9,66 +10,208 @@ use hashbrown::HashMap;
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use tracing::{info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
const PRICING_DEBUG: bool = false;
|
const PRICING_DEBUG: bool = false;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct TradePricing {
|
pub struct TradePricing {
|
||||||
// items of different good kinds
|
items: PriceEntries,
|
||||||
tools: Entries,
|
|
||||||
armor: Entries,
|
|
||||||
potions: Entries,
|
|
||||||
food: Entries,
|
|
||||||
ingredients: Entries,
|
|
||||||
other: Entries,
|
|
||||||
|
|
||||||
// good_scaling of coins
|
|
||||||
coin_scale: f32,
|
|
||||||
|
|
||||||
// get amount of material per item
|
|
||||||
material_cache: HashMap<String, (Good, f32)>,
|
|
||||||
equality_set: EqualitySet,
|
equality_set: EqualitySet,
|
||||||
}
|
}
|
||||||
|
|
||||||
// item asset specifier, probability, whether it's sellable by merchants
|
// combination logic:
|
||||||
type Entry = (String, f32, bool);
|
// price is the inverse of frequency
|
||||||
|
// you can use either equivalent A or B => add frequency
|
||||||
|
// you need both equivalent A and B => add price
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
/// Material equivalent for an item (price)
|
||||||
struct Entries {
|
#[derive(Default, Debug, Clone)]
|
||||||
entries: Vec<Entry>,
|
pub struct MaterialUse(Vec<(f32, Good)>);
|
||||||
|
|
||||||
|
impl std::ops::Mul<f32> for MaterialUse {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn mul(self, rhs: f32) -> Self::Output {
|
||||||
|
Self(self.0.iter().map(|v| (v.0 * rhs, v.1)).collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entries {
|
// used by the add variants
|
||||||
fn add(&mut self, eqset: &EqualitySet, item_name: &str, probability: f32, can_sell: bool) {
|
fn vector_add_eq(result: &mut Vec<(f32, Good)>, rhs: &[(f32, Good)]) {
|
||||||
let canonical_itemname = eqset.canonical(item_name);
|
for (amount, good) in rhs {
|
||||||
|
if result
|
||||||
let old = self
|
|
||||||
.entries
|
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|(name, _, _)| *name == *canonical_itemname);
|
.find(|(_amount2, good2)| *good == *good2)
|
||||||
|
.map(|elem| elem.0 += *amount)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
result.push((*amount, *good));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Increase probability if already in entries, or add new entry
|
impl std::ops::Add for MaterialUse {
|
||||||
if let Some((asset, ref mut old_probability, _)) = old {
|
type Output = Self;
|
||||||
if PRICING_DEBUG {
|
|
||||||
info!("Update {} {}+{}", asset, old_probability, probability);
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
|
let mut result = self;
|
||||||
|
vector_add_eq(&mut result.0, &rhs.0);
|
||||||
|
result
|
||||||
}
|
}
|
||||||
*old_probability += probability;
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for MaterialUse {
|
||||||
|
type Target = [(f32, Good)];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target { self.0.deref() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frequency
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct MaterialFrequency(Vec<(f32, Good)>);
|
||||||
|
|
||||||
|
// to compute price from frequency:
|
||||||
|
// price[i] = 1/frequency[i] * 1/sum(frequency) * 1/sum(1/frequency)
|
||||||
|
// scaling individual components so that ratio is inverted and the sum of all
|
||||||
|
// inverted elements is equivalent to inverse of the original sum
|
||||||
|
fn vector_invert(result: &mut Vec<(f32, Good)>) {
|
||||||
|
let mut oldsum: f32 = 0.0;
|
||||||
|
let mut newsum: f32 = 0.0;
|
||||||
|
for (value, _good) in result.iter_mut() {
|
||||||
|
oldsum += *value;
|
||||||
|
*value = 1.0 / *value;
|
||||||
|
newsum += *value;
|
||||||
|
}
|
||||||
|
let scale = 1.0 / (oldsum * newsum);
|
||||||
|
for (value, _good) in result.iter_mut() {
|
||||||
|
*value *= scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MaterialUse> for MaterialFrequency {
|
||||||
|
fn from(u: MaterialUse) -> Self {
|
||||||
|
let mut result = Self(u.0);
|
||||||
|
vector_invert(&mut result.0);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// identical computation
|
||||||
|
impl From<MaterialFrequency> for MaterialUse {
|
||||||
|
fn from(u: MaterialFrequency) -> Self {
|
||||||
|
let mut result = Self(u.0);
|
||||||
|
vector_invert(&mut result.0);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for MaterialFrequency {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
|
let mut result = self;
|
||||||
|
vector_add_eq(&mut result.0, &rhs.0);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::AddAssign for MaterialFrequency {
|
||||||
|
fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct PriceEntry {
|
||||||
|
// item asset specifier
|
||||||
|
name: String,
|
||||||
|
price: MaterialUse,
|
||||||
|
// sellable by merchants
|
||||||
|
sell: bool,
|
||||||
|
stackable: bool,
|
||||||
|
}
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct FreqEntry {
|
||||||
|
name: String,
|
||||||
|
freq: MaterialFrequency,
|
||||||
|
sell: bool,
|
||||||
|
stackable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct PriceEntries(Vec<PriceEntry>);
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct FreqEntries(Vec<FreqEntry>);
|
||||||
|
|
||||||
|
impl PriceEntries {
|
||||||
|
fn add_alternative(&mut self, b: PriceEntry) {
|
||||||
|
// alternatives are added in frequency (gets more frequent)
|
||||||
|
let already = self.0.iter_mut().find(|i| i.name == b.name);
|
||||||
|
if let Some(entry) = already {
|
||||||
|
let entry_freq: MaterialFrequency = std::mem::take(&mut entry.price).into();
|
||||||
|
let b_freq: MaterialFrequency = b.price.into();
|
||||||
|
let result = entry_freq + b_freq;
|
||||||
|
entry.price = result.into();
|
||||||
} else {
|
} else {
|
||||||
if PRICING_DEBUG {
|
self.0.push(b);
|
||||||
info!("New {} {}", item_name, probability);
|
|
||||||
}
|
}
|
||||||
self.entries
|
}
|
||||||
.push((canonical_itemname.to_owned(), probability, can_sell));
|
}
|
||||||
|
|
||||||
|
impl FreqEntries {
|
||||||
|
fn add(
|
||||||
|
&mut self,
|
||||||
|
eqset: &EqualitySet,
|
||||||
|
item_name: &str,
|
||||||
|
good: Good,
|
||||||
|
probability: f32,
|
||||||
|
can_sell: bool,
|
||||||
|
) {
|
||||||
|
let canonical_itemname = eqset.canonical(item_name);
|
||||||
|
let old = self
|
||||||
|
.0
|
||||||
|
.iter_mut()
|
||||||
|
.find(|elem| elem.name == *canonical_itemname);
|
||||||
|
let new_freq = MaterialFrequency(vec![(probability, good)]);
|
||||||
|
// Increase probability if already in entries, or add new entry
|
||||||
|
if let Some(FreqEntry {
|
||||||
|
name: asset,
|
||||||
|
freq: ref mut old_probability,
|
||||||
|
sell: old_can_sell,
|
||||||
|
stackable: _,
|
||||||
|
}) = old
|
||||||
|
{
|
||||||
|
if PRICING_DEBUG {
|
||||||
|
info!("Update {} {:?}+{:?}", asset, old_probability, probability);
|
||||||
|
}
|
||||||
|
if !can_sell && *old_can_sell {
|
||||||
|
*old_can_sell = false;
|
||||||
|
}
|
||||||
|
*old_probability += new_freq;
|
||||||
|
} else {
|
||||||
|
let item = Item::new_from_asset_expect(canonical_itemname);
|
||||||
|
let stackable = item.is_stackable();
|
||||||
|
let new_mat_prob: FreqEntry = FreqEntry {
|
||||||
|
name: canonical_itemname.to_owned(),
|
||||||
|
freq: new_freq,
|
||||||
|
sell: can_sell,
|
||||||
|
stackable,
|
||||||
|
};
|
||||||
|
if PRICING_DEBUG {
|
||||||
|
info!("New {:?}", new_mat_prob);
|
||||||
|
}
|
||||||
|
self.0.push(new_mat_prob);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the non-canonical item so that it'll show up in merchant inventories
|
// Add the non-canonical item so that it'll show up in merchant inventories
|
||||||
// It will have infinity as its price, but it's fine,
|
// It will have infinity as its price, but it's fine,
|
||||||
// because we determine all prices based on canonical value
|
// because we determine all prices based on canonical value
|
||||||
if canonical_itemname != item_name
|
if canonical_itemname != item_name && !self.0.iter().any(|elem| elem.name == *item_name) {
|
||||||
&& !self.entries.iter().any(|(name, _, _)| name == item_name)
|
self.0.push(FreqEntry {
|
||||||
{
|
name: item_name.to_owned(),
|
||||||
self.entries.push((item_name.to_owned(), 0.0, can_sell));
|
freq: Default::default(),
|
||||||
|
sell: can_sell,
|
||||||
|
stackable: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,27 +340,10 @@ impl assets::Compound for EqualitySet {
|
|||||||
struct RememberedRecipe {
|
struct RememberedRecipe {
|
||||||
output: String,
|
output: String,
|
||||||
amount: u32,
|
amount: u32,
|
||||||
material_cost: f32,
|
material_cost: Option<f32>,
|
||||||
input: Vec<(String, u32)>,
|
input: Vec<(String, u32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_and_normalize(entryvec: &mut [Entry], scale: f32) {
|
|
||||||
if !entryvec.is_empty() {
|
|
||||||
entryvec.sort_by(|a, b| {
|
|
||||||
a.1.partial_cmp(&b.1)
|
|
||||||
.unwrap_or(Ordering::Equal)
|
|
||||||
.then_with(|| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal))
|
|
||||||
});
|
|
||||||
if let Some((_, max_scale, _)) = entryvec.last() {
|
|
||||||
// most common item has frequency max_scale. avoid NaN
|
|
||||||
let rescale = scale / max_scale;
|
|
||||||
for i in entryvec.iter_mut() {
|
|
||||||
i.1 *= rescale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_scaling(contents: &AssetGuard<TradingPriceFile>, good: Good) -> f32 {
|
fn get_scaling(contents: &AssetGuard<TradingPriceFile>, good: Good) -> f32 {
|
||||||
contents
|
contents
|
||||||
.good_scaling
|
.good_scaling
|
||||||
@ -231,135 +357,109 @@ impl TradePricing {
|
|||||||
const CRAFTING_FACTOR: f32 = 0.95;
|
const CRAFTING_FACTOR: f32 = 0.95;
|
||||||
// increase price a bit compared to sum of ingredients
|
// increase price a bit compared to sum of ingredients
|
||||||
const INVEST_FACTOR: f32 = 0.33;
|
const INVEST_FACTOR: f32 = 0.33;
|
||||||
const UNAVAILABLE_PRICE: f32 = 1_000_000.0;
|
|
||||||
|
|
||||||
// add this much of a non-consumed crafting tool price
|
fn good_from_item(name: &str) -> Good {
|
||||||
|
|
||||||
fn get_list(&self, good: Good) -> &[Entry] {
|
|
||||||
match good {
|
|
||||||
Good::Armor => &self.armor.entries,
|
|
||||||
Good::Tools => &self.tools.entries,
|
|
||||||
Good::Potions => &self.potions.entries,
|
|
||||||
Good::Food => &self.food.entries,
|
|
||||||
Good::Ingredients => &self.ingredients.entries,
|
|
||||||
_ => &[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_mut(&mut self, good: Good) -> &mut [Entry] {
|
|
||||||
match good {
|
|
||||||
Good::Armor => &mut self.armor.entries,
|
|
||||||
Good::Tools => &mut self.tools.entries,
|
|
||||||
Good::Potions => &mut self.potions.entries,
|
|
||||||
Good::Food => &mut self.food.entries,
|
|
||||||
Good::Ingredients => &mut self.ingredients.entries,
|
|
||||||
_ => &mut [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_by_path(&self, name: &str) -> &[Entry] {
|
|
||||||
match name {
|
match name {
|
||||||
// Armor
|
_ if name.starts_with("common.items.armor.") => Good::Armor,
|
||||||
_ if name.starts_with("common.items.armor.") => &self.armor.entries,
|
|
||||||
// Tools
|
_ if name.starts_with("common.items.weapons.") => Good::Tools,
|
||||||
_ if name.starts_with("common.items.weapons.") => &self.tools.entries,
|
_ if name.starts_with("common.items.tool.") => Good::Tools,
|
||||||
_ if name.starts_with("common.items.tool.") => &self.tools.entries,
|
|
||||||
// Ingredients
|
_ if name.starts_with("common.items.crafting_ing.") => Good::Ingredients,
|
||||||
_ if name.starts_with("common.items.crafting_ing.") => &self.ingredients.entries,
|
_ if name.starts_with("common.items.mineral.") => Good::Ingredients,
|
||||||
_ if name.starts_with("common.items.mineral.") => &self.ingredients.entries,
|
_ if name.starts_with("common.items.flowers.") => Good::Ingredients,
|
||||||
_ if name.starts_with("common.items.flowers.") => &self.ingredients.entries,
|
|
||||||
// Potions
|
_ if name.starts_with("common.items.consumable.") => Good::Potions,
|
||||||
_ if name.starts_with("common.items.consumable.") => &self.potions.entries,
|
|
||||||
// Food
|
_ if name.starts_with("common.items.food.") => Good::Food,
|
||||||
_ if name.starts_with("common.items.food.") => &self.food.entries,
|
|
||||||
// Other
|
Self::COIN_ITEM => Good::Coin,
|
||||||
_ if name.starts_with("common.items.glider.") => &self.other.entries,
|
|
||||||
_ if name.starts_with("common.items.utility.") => &self.other.entries,
|
_ if name.starts_with("common.items.glider.") => Good::default(),
|
||||||
_ if name.starts_with("common.items.boss_drops.") => &self.other.entries,
|
_ if name.starts_with("common.items.utility.") => Good::default(),
|
||||||
_ if name.starts_with("common.items.crafting_tools.") => &self.other.entries,
|
_ if name.starts_with("common.items.boss_drops.") => Good::default(),
|
||||||
_ if name.starts_with("common.items.lantern.") => &self.other.entries,
|
_ if name.starts_with("common.items.crafting_tools.") => Good::default(),
|
||||||
|
_ if name.starts_with("common.items.lantern.") => Good::default(),
|
||||||
_ => {
|
_ => {
|
||||||
warn!("unknown loot item {}", name);
|
warn!("unknown loot item {}", name);
|
||||||
&self.other.entries
|
Good::default()
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_by_path_mut(&mut self, name: &str) -> &mut Entries {
|
|
||||||
match name {
|
|
||||||
// Armor
|
|
||||||
_ if name.starts_with("common.items.armor.") => &mut self.armor,
|
|
||||||
// Tools
|
|
||||||
_ if name.starts_with("common.items.weapons.") => &mut self.tools,
|
|
||||||
_ if name.starts_with("common.items.tool.") => &mut self.tools,
|
|
||||||
// Ingredients
|
|
||||||
_ if name.starts_with("common.items.crafting_ing.") => &mut self.ingredients,
|
|
||||||
_ if name.starts_with("common.items.mineral.") => &mut self.ingredients,
|
|
||||||
_ if name.starts_with("common.items.flowers.") => &mut self.ingredients,
|
|
||||||
// Potions
|
|
||||||
_ if name.starts_with("common.items.consumable.") => &mut self.potions,
|
|
||||||
// Food
|
|
||||||
_ if name.starts_with("common.items.food.") => &mut self.food,
|
|
||||||
// Other
|
|
||||||
_ if name.starts_with("common.items.glider.") => &mut self.other,
|
|
||||||
_ if name.starts_with("common.items.utility.") => &mut self.other,
|
|
||||||
_ if name.starts_with("common.items.boss_drops.") => &mut self.other,
|
|
||||||
_ if name.starts_with("common.items.crafting_tools.") => &mut self.other,
|
|
||||||
_ if name.starts_with("common.items.lantern.") => &mut self.other,
|
|
||||||
_ => {
|
|
||||||
warn!("unknown loot item {}", name);
|
|
||||||
&mut self.other
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// look up price (inverse frequency) of an item
|
// look up price (inverse frequency) of an item
|
||||||
fn price_lookup(&self, eqset: &EqualitySet, requested_name: &str) -> f32 {
|
fn price_lookup(&self, requested_name: &str) -> Option<&MaterialUse> {
|
||||||
let canonical_name = eqset.canonical(requested_name);
|
let canonical_name = self.equality_set.canonical(requested_name);
|
||||||
|
self.items
|
||||||
let goods = self.get_list_by_path(canonical_name);
|
.0
|
||||||
// even if we multiply by INVEST_FACTOR we need to remain
|
|
||||||
// above UNAVAILABLE_PRICE (add 1.0 to compensate rounding errors)
|
|
||||||
goods
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(name, _, _)| name == canonical_name)
|
.find(|e| e.name == canonical_name)
|
||||||
.map_or(
|
.map(|e| &e.price)
|
||||||
Self::UNAVAILABLE_PRICE / Self::INVEST_FACTOR + 1.0,
|
|
||||||
|(_, freq, _)| 1.0 / freq,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
|
||||||
fn calculate_material_cost(&self, r: &RememberedRecipe, eqset: &EqualitySet) -> f32 {
|
|
||||||
r.input
|
r.input
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, amount)| {
|
.map(|(name, amount)| {
|
||||||
self.price_lookup(eqset, name) * (*amount as f32).max(Self::INVEST_FACTOR)
|
self.price_lookup(name).map(|x| {
|
||||||
|
x.clone()
|
||||||
|
* (if *amount > 0 {
|
||||||
|
*amount as f32
|
||||||
|
} else {
|
||||||
|
Self::INVEST_FACTOR
|
||||||
})
|
})
|
||||||
.sum()
|
})
|
||||||
|
})
|
||||||
|
.try_fold(MaterialUse::default(), |acc, elem| Some(acc + elem?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_material_cost_sum(&self, r: &RememberedRecipe) -> Option<f32> {
|
||||||
|
self.calculate_material_cost(r)?
|
||||||
|
.iter()
|
||||||
|
.fold(None, |acc, elem| Some(acc.unwrap_or_default() + elem.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-look up prices and sort the vector by ascending material cost, return
|
// re-look up prices and sort the vector by ascending material cost, return
|
||||||
// whether first cost is finite
|
// whether first cost is finite
|
||||||
fn sort_by_price(&self, recipes: &mut Vec<RememberedRecipe>, eqset: &EqualitySet) -> bool {
|
fn sort_by_price(&self, recipes: &mut Vec<RememberedRecipe>) -> bool {
|
||||||
for recipe in recipes.iter_mut() {
|
for recipe in recipes.iter_mut() {
|
||||||
recipe.material_cost = self.calculate_material_cost(recipe, eqset);
|
recipe.material_cost = self.calculate_material_cost_sum(recipe);
|
||||||
}
|
}
|
||||||
recipes.sort_by(|a, b| a.material_cost.partial_cmp(&b.material_cost).unwrap());
|
// put None to the end
|
||||||
//info!(?recipes);
|
recipes.sort_by(|a, b| {
|
||||||
|
if a.material_cost.is_some() {
|
||||||
|
if b.material_cost.is_some() {
|
||||||
|
a.material_cost
|
||||||
|
.partial_cmp(&b.material_cost)
|
||||||
|
.unwrap_or(Ordering::Equal)
|
||||||
|
} else {
|
||||||
|
Ordering::Less
|
||||||
|
}
|
||||||
|
} else if b.material_cost.is_some() {
|
||||||
|
Ordering::Greater
|
||||||
|
} else {
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if PRICING_DEBUG {
|
||||||
|
tracing::debug!("{:?}", recipes);
|
||||||
|
}
|
||||||
|
//info!(? recipes);
|
||||||
recipes
|
recipes
|
||||||
.first()
|
.first()
|
||||||
.filter(|recipe| recipe.material_cost < Self::UNAVAILABLE_PRICE)
|
.filter(|recipe| recipe.material_cost.is_some())
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
// #[allow(clippy::cast_precision_loss)]
|
||||||
fn read() -> Self {
|
fn read() -> Self {
|
||||||
let mut result = Self::default();
|
let mut result = Self::default();
|
||||||
|
let mut freq = FreqEntries::default();
|
||||||
let price_config =
|
let price_config =
|
||||||
TradingPriceFile::load_expect("common.trading.item_price_calculation").read();
|
TradingPriceFile::load_expect("common.trading.item_price_calculation").read();
|
||||||
let eqset = EqualitySet::load_expect("common.trading.item_price_equality").read();
|
result.equality_set = EqualitySet::load_expect("common.trading.item_price_equality")
|
||||||
result.equality_set = eqset.clone();
|
.read()
|
||||||
|
.clone();
|
||||||
for table in &price_config.loot_tables {
|
for table in &price_config.loot_tables {
|
||||||
if PRICING_DEBUG {
|
if PRICING_DEBUG {
|
||||||
info!(?table);
|
info!(?table);
|
||||||
@ -367,14 +467,55 @@ impl TradePricing {
|
|||||||
let (frequency, can_sell, asset_path) = table;
|
let (frequency, can_sell, asset_path) = table;
|
||||||
let loot = ProbabilityFile::load_expect(asset_path);
|
let loot = ProbabilityFile::load_expect(asset_path);
|
||||||
for (p, item_asset, amount) in &loot.read().content {
|
for (p, item_asset, amount) in &loot.read().content {
|
||||||
result.get_list_by_path_mut(item_asset).add(
|
let good = Self::good_from_item(item_asset);
|
||||||
&eqset,
|
let scaling = get_scaling(&price_config, good);
|
||||||
|
freq.add(
|
||||||
|
&result.equality_set,
|
||||||
item_asset,
|
item_asset,
|
||||||
frequency * p * *amount,
|
good,
|
||||||
|
frequency * p * *amount * scaling,
|
||||||
*can_sell,
|
*can_sell,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
freq.add(
|
||||||
|
&result.equality_set,
|
||||||
|
Self::COIN_ITEM,
|
||||||
|
Good::Coin,
|
||||||
|
get_scaling(&price_config, Good::Coin),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// convert frequency to price
|
||||||
|
result.items.0.extend(freq.0.iter().map(|elem| {
|
||||||
|
if elem.freq.0.is_empty() {
|
||||||
|
// likely equality
|
||||||
|
let canonical_name = result.equality_set.canonical(&elem.name);
|
||||||
|
let can_freq = freq.0.iter().find(|i| i.name == canonical_name);
|
||||||
|
can_freq
|
||||||
|
.map(|e| PriceEntry {
|
||||||
|
name: elem.name.clone(),
|
||||||
|
price: MaterialUse::from(e.freq.clone()),
|
||||||
|
sell: elem.sell && e.sell,
|
||||||
|
stackable: elem.stackable,
|
||||||
|
})
|
||||||
|
.unwrap_or(PriceEntry {
|
||||||
|
name: elem.name.clone(),
|
||||||
|
price: MaterialUse::from(elem.freq.clone()),
|
||||||
|
sell: elem.sell,
|
||||||
|
stackable: elem.stackable,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
PriceEntry {
|
||||||
|
name: elem.name.clone(),
|
||||||
|
price: MaterialUse::from(elem.freq.clone()),
|
||||||
|
sell: elem.sell,
|
||||||
|
stackable: elem.stackable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
if PRICING_DEBUG {
|
||||||
|
tracing::debug!("{:?}", result.items.0);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply recipe book
|
// Apply recipe book
|
||||||
let book = default_recipe_book().read();
|
let book = default_recipe_book().read();
|
||||||
@ -384,7 +525,7 @@ impl TradePricing {
|
|||||||
ordered_recipes.push(RememberedRecipe {
|
ordered_recipes.push(RememberedRecipe {
|
||||||
output: asset_path.id().into(),
|
output: asset_path.id().into(),
|
||||||
amount,
|
amount,
|
||||||
material_cost: Self::UNAVAILABLE_PRICE,
|
material_cost: None,
|
||||||
input: recipe
|
input: recipe
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
@ -406,106 +547,130 @@ impl TradePricing {
|
|||||||
|
|
||||||
// re-evaluate prices based on crafting tables
|
// re-evaluate prices based on crafting tables
|
||||||
// (start with cheap ones to avoid changing material prices after evaluation)
|
// (start with cheap ones to avoid changing material prices after evaluation)
|
||||||
while result.sort_by_price(&mut ordered_recipes, &eqset) {
|
while result.sort_by_price(&mut ordered_recipes) {
|
||||||
ordered_recipes.retain(|recipe| {
|
ordered_recipes.retain(|recipe| {
|
||||||
if recipe.material_cost < 1e-5 {
|
if recipe.material_cost.map_or(false, |p| p < 1e-5) {
|
||||||
|
// don't handle recipes which have no raw materials
|
||||||
false
|
false
|
||||||
} else if recipe.material_cost < Self::UNAVAILABLE_PRICE {
|
} else if recipe.material_cost.is_some() {
|
||||||
let actual_cost = result.calculate_material_cost(recipe, &eqset);
|
let actual_cost = result.calculate_material_cost(recipe);
|
||||||
|
if let Some(usage) = actual_cost {
|
||||||
let output_tradeable = recipe.input.iter().all(|(input, _)| {
|
let output_tradeable = recipe.input.iter().all(|(input, _)| {
|
||||||
result
|
result
|
||||||
.get_list_by_path(input)
|
.items
|
||||||
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(item, _, _)| item == input)
|
.find(|item| item.name == *input)
|
||||||
.map_or(false, |(_, _, tradeable)| *tradeable)
|
.map_or(false, |item| item.sell)
|
||||||
});
|
});
|
||||||
result.get_list_by_path_mut(&recipe.output).add(
|
let item = Item::new_from_asset_expect(&recipe.output);
|
||||||
&eqset,
|
let stackable = item.is_stackable();
|
||||||
&recipe.output,
|
result.items.add_alternative(PriceEntry {
|
||||||
(recipe.amount as f32) / actual_cost * Self::CRAFTING_FACTOR,
|
name: recipe.output.clone(),
|
||||||
output_tradeable,
|
price: usage * (recipe.amount as f32) * Self::CRAFTING_FACTOR,
|
||||||
);
|
sell: output_tradeable,
|
||||||
|
stackable,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
error!("Recipe {:?} incomplete confusion", recipe);
|
||||||
|
}
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
|
// handle incomplete recipes later
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//info!(?ordered_recipes);
|
//info!(?ordered_recipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
let good_list = [
|
|
||||||
Good::Armor,
|
|
||||||
Good::Tools,
|
|
||||||
Good::Potions,
|
|
||||||
Good::Food,
|
|
||||||
Good::Ingredients,
|
|
||||||
];
|
|
||||||
|
|
||||||
for good in &good_list {
|
|
||||||
sort_and_normalize(
|
|
||||||
result.get_list_mut(*good),
|
|
||||||
get_scaling(&price_config, *good),
|
|
||||||
);
|
|
||||||
let mut materials = result
|
|
||||||
.get_list(*good)
|
|
||||||
.iter()
|
|
||||||
.map(|i| (i.0.clone(), (*good, 1.0 / i.1)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
result.material_cache.extend(materials.drain(..));
|
|
||||||
}
|
|
||||||
result.coin_scale = get_scaling(&price_config, Good::Coin);
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
// TODO: optimize repeated use
|
||||||
clippy::cast_possible_truncation,
|
fn random_items_impl(
|
||||||
clippy::cast_sign_loss,
|
&self,
|
||||||
clippy::cast_precision_loss
|
stockmap: &mut HashMap<Good, f32>,
|
||||||
)]
|
mut number: u32,
|
||||||
fn random_item_impl(&self, good: Good, amount: f32, selling: bool) -> Option<String> {
|
selling: bool,
|
||||||
if good == Good::Coin {
|
always_coin: bool,
|
||||||
Some(Self::COIN_ITEM.into())
|
limit: u32,
|
||||||
} else {
|
) -> Vec<(String, u32)> {
|
||||||
let table = self.get_list(good);
|
let mut candidates: Vec<&PriceEntry> = self
|
||||||
if table.is_empty()
|
.items
|
||||||
|| (selling && table.iter().filter(|(_, _, can_sell)| *can_sell).count() == 0)
|
.0
|
||||||
{
|
|
||||||
warn!("Good: {:?}, was unreachable.", good);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let upper = table.len();
|
|
||||||
let lower = table
|
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.filter(|i| {
|
||||||
.find(|i| i.1.1 * amount >= 1.0)
|
let excess = i
|
||||||
.map_or(upper - 1, |i| i.0);
|
.price
|
||||||
loop {
|
.iter()
|
||||||
let index =
|
.find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
|
||||||
(rand::random::<f32>() * ((upper - lower) as f32)).floor() as usize + lower;
|
excess.is_none()
|
||||||
if table.get(index).map_or(false, |i| !selling || i.2) {
|
&& (!selling || i.sell)
|
||||||
break table.get(index).map(|i| i.0.clone());
|
&& (!always_coin || i.name != Self::COIN_ITEM)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut result = Vec::new();
|
||||||
|
if always_coin && number > 0 {
|
||||||
|
let amount = stockmap.get(&Good::Coin).copied().unwrap_or_default() as u32;
|
||||||
|
if amount > 0 {
|
||||||
|
result.push((Self::COIN_ITEM.into(), amount));
|
||||||
|
number -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _ in 0..number {
|
||||||
|
candidates.retain(|i| {
|
||||||
|
let excess = i
|
||||||
|
.price
|
||||||
|
.iter()
|
||||||
|
.find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
|
||||||
|
excess.is_none()
|
||||||
|
});
|
||||||
|
if candidates.is_empty() {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
let index = (rand::random::<f32>() * candidates.len() as f32).floor() as usize;
|
||||||
|
let result2 = candidates[index];
|
||||||
#[must_use]
|
let amount: u32 = if result2.stackable {
|
||||||
pub fn random_item(good: Good, amount: f32, selling: bool) -> Option<String> {
|
let max_amount = result2
|
||||||
TRADE_PRICING.random_item_impl(good, amount, selling)
|
.price
|
||||||
}
|
.iter()
|
||||||
|
.map(|e| {
|
||||||
#[must_use]
|
stockmap
|
||||||
pub fn get_material(item: &str) -> (Good, f32) {
|
.get_mut(&e.1)
|
||||||
if item == Self::COIN_ITEM {
|
.map(|stock| *stock / e.0.max(0.001))
|
||||||
(Good::Coin, 1.0)
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.fold(f32::INFINITY, f32::min)
|
||||||
|
.min(limit as f32);
|
||||||
|
(rand::random::<f32>() * (max_amount - 1.0)).floor() as u32 + 1
|
||||||
} else {
|
} else {
|
||||||
let item = TRADE_PRICING.equality_set.canonical(item);
|
1
|
||||||
|
};
|
||||||
TRADE_PRICING.material_cache.get(item).copied().map_or(
|
for i in result2.price.iter() {
|
||||||
(Good::Terrain(crate::terrain::BiomeKind::Void), 0.0),
|
stockmap.get_mut(&i.1).map(|v| *v -= i.0 * (amount as f32));
|
||||||
|(a, b)| (a, b * TRADE_PRICING.coin_scale),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
result.push((result2.name.clone(), amount));
|
||||||
|
// avoid duplicates
|
||||||
|
candidates.remove(index);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_materials_impl(&self, item: &str) -> Option<&MaterialUse> { self.price_lookup(item) }
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn random_items(
|
||||||
|
stock: &mut HashMap<Good, f32>,
|
||||||
|
number: u32,
|
||||||
|
selling: bool,
|
||||||
|
always_coin: bool,
|
||||||
|
limit: u32,
|
||||||
|
) -> Vec<(String, u32)> {
|
||||||
|
TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_materials(item: &str) -> Option<&MaterialUse> {
|
||||||
|
TRADE_PRICING.get_materials_impl(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -513,69 +678,32 @@ impl TradePricing {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn print_sorted(&self) {
|
fn print_sorted(&self) {
|
||||||
use crate::comp::item::{armor, tool, Item, ItemKind};
|
use crate::comp::item::{armor, tool, ItemKind};
|
||||||
|
|
||||||
// we pass the item and the inverse of the price to the closure
|
|
||||||
fn printvec<F>(good_kind: &str, entries: &[(String, f32, bool)], f: F, unit: &str)
|
|
||||||
where
|
|
||||||
F: Fn(&Item, f32) -> String,
|
|
||||||
{
|
|
||||||
for (item_id, p, can_sell) in entries.iter() {
|
|
||||||
let it = Item::new_from_asset_expect(item_id);
|
|
||||||
let price = 1.0 / p;
|
|
||||||
println!(
|
|
||||||
"{}, {}, {:>4.2}, {}, {:?}, {}, {},",
|
|
||||||
item_id,
|
|
||||||
if *can_sell { "yes" } else { "no" },
|
|
||||||
price,
|
|
||||||
good_kind,
|
|
||||||
it.quality,
|
|
||||||
f(&it, *p),
|
|
||||||
unit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
|
println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
|
||||||
|
|
||||||
printvec(
|
fn more_information(i: &Item, p: f32) -> (String, &'static str) {
|
||||||
"Armor",
|
|
||||||
&self.armor.entries,
|
|
||||||
|i, p| {
|
|
||||||
if let ItemKind::Armor(a) = &i.kind {
|
if let ItemKind::Armor(a) = &i.kind {
|
||||||
|
(
|
||||||
match a.protection() {
|
match a.protection() {
|
||||||
Some(armor::Protection::Invincible) => "Invincible".into(),
|
Some(armor::Protection::Invincible) => "Invincible".into(),
|
||||||
Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
|
Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
|
||||||
None => "0.0".into(),
|
None => "0.0".into(),
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("{:?}", i.kind)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"prot/val",
|
"prot/val",
|
||||||
);
|
)
|
||||||
printvec(
|
} else if let ItemKind::Tool(t) = &i.kind {
|
||||||
"Tools",
|
(
|
||||||
&self.tools.entries,
|
|
||||||
|i, p| {
|
|
||||||
if let ItemKind::Tool(t) = &i.kind {
|
|
||||||
match &t.stats {
|
match &t.stats {
|
||||||
tool::StatKind::Direct(d) => {
|
tool::StatKind::Direct(d) => {
|
||||||
format!("{:.4}", d.power * d.speed * p)
|
format!("{:.4}", d.power * d.speed * p)
|
||||||
},
|
},
|
||||||
tool::StatKind::Modular => "Modular".into(),
|
tool::StatKind::Modular => "Modular".into(),
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("{:?}", i.kind)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"dps/val",
|
"dps/val",
|
||||||
);
|
)
|
||||||
printvec(
|
} else if let ItemKind::Consumable { kind: _, effects } = &i.kind {
|
||||||
"Potions",
|
(
|
||||||
&self.potions.entries,
|
|
||||||
|i, p| {
|
|
||||||
if let ItemKind::Consumable { kind: _, effects } = &i.kind {
|
|
||||||
effects
|
effects
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| {
|
.map(|e| {
|
||||||
@ -586,43 +714,53 @@ impl TradePricing {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(" ")
|
.join(" "),
|
||||||
} else {
|
|
||||||
format!("{:?}", i.kind)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"str/val",
|
"str/val",
|
||||||
);
|
)
|
||||||
printvec(
|
} else {
|
||||||
"Food",
|
(Default::default(), "")
|
||||||
&self.food.entries,
|
}
|
||||||
|i, p| {
|
}
|
||||||
if let ItemKind::Consumable { kind: _, effects } = &i.kind {
|
let mut sorted: Vec<(f32, &PriceEntry)> = self
|
||||||
effects
|
.items
|
||||||
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| {
|
.map(|e| (e.price.iter().map(|i| i.0.to_owned()).sum(), e))
|
||||||
if let crate::effect::Effect::Buff(b) = e {
|
.collect();
|
||||||
format!("{:.2}", b.data.strength * p)
|
sorted.sort_by(|(p, e), (p2, e2)| {
|
||||||
} else {
|
p2.partial_cmp(p)
|
||||||
format!("{:?}", e)
|
.unwrap_or(Ordering::Equal)
|
||||||
}
|
.then(e.name.cmp(&e2.name))
|
||||||
})
|
});
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" ")
|
for (
|
||||||
} else {
|
pricesum,
|
||||||
format!("{:?}", i.kind)
|
PriceEntry {
|
||||||
}
|
name: item_id,
|
||||||
|
price: mat_use,
|
||||||
|
sell: can_sell,
|
||||||
|
stackable: _,
|
||||||
},
|
},
|
||||||
"str/val",
|
) in sorted.iter()
|
||||||
|
{
|
||||||
|
let it = Item::new_from_asset_expect(item_id);
|
||||||
|
//let price = mat_use.iter().map(|(amount, _good)| *amount).sum::<f32>();
|
||||||
|
let prob = 1.0 / pricesum;
|
||||||
|
let (info, unit) = more_information(&it, prob);
|
||||||
|
let materials = mat_use
|
||||||
|
.iter()
|
||||||
|
.fold(String::new(), |agg, i| agg + &format!("{:?}.", i.1));
|
||||||
|
println!(
|
||||||
|
"{}, {}, {:>4.2}, {}, {:?}, {}, {},",
|
||||||
|
&item_id,
|
||||||
|
if *can_sell { "yes" } else { "no" },
|
||||||
|
pricesum,
|
||||||
|
materials,
|
||||||
|
it.quality,
|
||||||
|
info,
|
||||||
|
unit,
|
||||||
);
|
);
|
||||||
printvec(
|
}
|
||||||
"Ingredients",
|
|
||||||
&self.ingredients.entries,
|
|
||||||
|_i, _p| String::new(),
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
printvec("Other", &self.other.entries, |_i, _p| String::new(), "");
|
|
||||||
println!("{}, yes, {}, Coin, ,,,", Self::COIN_ITEM, self.coin_scale);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -684,10 +822,19 @@ mod tests {
|
|||||||
init();
|
init();
|
||||||
info!("init");
|
info!("init");
|
||||||
|
|
||||||
for _ in 0..5 {
|
let mut stock: hashbrown::HashMap<Good, f32> = vec![
|
||||||
if let Some(item_id) = TradePricing::random_item(Good::Armor, 5.0, false) {
|
(Good::Ingredients, 50.0),
|
||||||
info!("Armor 5 {}", item_id);
|
(Good::Tools, 10.0),
|
||||||
}
|
(Good::Armor, 10.0),
|
||||||
|
//(Good::Ores, 20.0),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let loadout = TradePricing::random_items(&mut stock, 20, false, false, 999);
|
||||||
|
for i in loadout.iter() {
|
||||||
|
info!("Random item {}*{}", i.0, i.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,11 +367,18 @@ impl SitePrices {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|ri| {
|
.and_then(|ri| {
|
||||||
ri.inventory.get(slot).map(|item| {
|
ri.inventory.get(slot).map(|item| {
|
||||||
let (material, factor) = TradePricing::get_material(&item.name);
|
if let Some(vec) = TradePricing::get_materials(&item.name) {
|
||||||
self.values.get(&material).cloned().unwrap_or_default()
|
vec.iter()
|
||||||
* factor
|
.map(|(amount2, material)| {
|
||||||
|
self.values.get(material).copied().unwrap_or_default()
|
||||||
|
* *amount2
|
||||||
|
* (if reduce { material.trade_margin() } else { 1.0 })
|
||||||
|
})
|
||||||
|
.sum::<f32>()
|
||||||
* (*amount as f32)
|
* (*amount as f32)
|
||||||
* if reduce { material.trade_margin() } else { 1.0 }
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
@ -3730,17 +3730,26 @@ impl Hud {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
if let Some(item) = inventory.get(slot) {
|
if let Some(item) = inventory.get(slot) {
|
||||||
let (material, factor) =
|
if let Some(materials) =
|
||||||
TradePricing::get_material(item.item_definition_id());
|
TradePricing::get_materials(item.item_definition_id())
|
||||||
let mut unit_price = prices
|
{
|
||||||
|
let unit_price: f32 = materials
|
||||||
|
.iter()
|
||||||
|
.map(|e| {
|
||||||
|
prices
|
||||||
.values
|
.values
|
||||||
.get(&material)
|
.get(&e.1)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
* factor;
|
* e.0
|
||||||
if ours {
|
* (if ours {
|
||||||
unit_price *= material.trade_margin();
|
e.1.trade_margin()
|
||||||
}
|
} else {
|
||||||
|
1.0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
|
||||||
let mut float_delta = if ours ^ remove {
|
let mut float_delta = if ours ^ remove {
|
||||||
(balance1 - balance0) / unit_price
|
(balance1 - balance0) / unit_price
|
||||||
} else {
|
} else {
|
||||||
@ -3754,6 +3763,7 @@ impl Hud {
|
|||||||
*quantity = float_delta.max(0.0) as u32;
|
*quantity = float_delta.max(0.0) as u32;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match slot {
|
match slot {
|
||||||
Inventory(i) => {
|
Inventory(i) => {
|
||||||
|
@ -22,13 +22,25 @@ pub fn price_desc(
|
|||||||
i18n: &Localization,
|
i18n: &Localization,
|
||||||
) -> Option<(String, String, f32)> {
|
) -> Option<(String, String, f32)> {
|
||||||
if let Some(prices) = prices {
|
if let Some(prices) = prices {
|
||||||
let (material, factor) = TradePricing::get_material(item_definition_id);
|
if let Some(materials) = TradePricing::get_materials(item_definition_id) {
|
||||||
let coinprice = prices.values.get(&Good::Coin).cloned().unwrap_or(1.0);
|
let coinprice = prices.values.get(&Good::Coin).cloned().unwrap_or(1.0);
|
||||||
let buyprice = prices.values.get(&material).cloned().unwrap_or_default() * factor;
|
let buyprice: f32 = materials
|
||||||
let sellprice = buyprice * material.trade_margin();
|
.iter()
|
||||||
|
.map(|e| prices.values.get(&e.1).cloned().unwrap_or_default() * e.0)
|
||||||
|
.sum();
|
||||||
|
let sellprice: f32 = materials
|
||||||
|
.iter()
|
||||||
|
.map(|e| {
|
||||||
|
prices.values.get(&e.1).cloned().unwrap_or_default() * e.0 * e.1.trade_margin()
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
|
||||||
let deal_goodness = prices.values.get(&material).cloned().unwrap_or(0.0)
|
let deal_goodness: f32 = materials
|
||||||
/ prices.values.get(&Good::Coin).cloned().unwrap_or(1.0);
|
.iter()
|
||||||
|
.map(|e| prices.values.get(&e.1).cloned().unwrap_or(0.0))
|
||||||
|
.sum::<f32>()
|
||||||
|
/ prices.values.get(&Good::Coin).cloned().unwrap_or(1.0)
|
||||||
|
/ (materials.len() as f32);
|
||||||
let deal_goodness = deal_goodness.log(2.0);
|
let deal_goodness = deal_goodness.log(2.0);
|
||||||
let buy_string = format!(
|
let buy_string = format!(
|
||||||
"{} : {:0.1} {}",
|
"{} : {:0.1} {}",
|
||||||
@ -53,6 +65,9 @@ pub fn price_desc(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kind_text<'a>(kind: &ItemKind, i18n: &'a Localization) -> Cow<'a, str> {
|
pub fn kind_text<'a>(kind: &ItemKind, i18n: &'a Localization) -> Cow<'a, str> {
|
||||||
|
@ -35,12 +35,7 @@ use fxhash::FxHasher64;
|
|||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{collections::VecDeque, f32, hash::BuildHasherDefault};
|
||||||
cmp::{self, min},
|
|
||||||
collections::VecDeque,
|
|
||||||
f32,
|
|
||||||
hash::BuildHasherDefault,
|
|
||||||
};
|
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -1019,236 +1014,96 @@ pub fn merchant_loadout(
|
|||||||
) -> LoadoutBuilder {
|
) -> LoadoutBuilder {
|
||||||
let rng = &mut rand::thread_rng();
|
let rng = &mut rand::thread_rng();
|
||||||
|
|
||||||
// Fill backpack with ingredients and coins
|
let mut backpack = Item::new_from_asset_expect("common.items.armor.misc.back.backpack");
|
||||||
let backpack = ingredient_backpack(economy, rng);
|
let mut bag1 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
|
||||||
|
let mut bag2 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
|
||||||
// Fill bags with stuff
|
let mut bag3 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
|
||||||
let (food_bag, potion_bag) = consumable_bags(economy, rng);
|
let mut bag4 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
|
||||||
let weapon_bag = weapon_bag(economy);
|
let slots = backpack.slots().len() + 4 * bag1.slots().len();
|
||||||
let armor_bag = armor_bag(economy);
|
let mut stockmap: HashMap<Good, f32> = economy
|
||||||
|
.map(|e| e.unconsumed_stock.clone())
|
||||||
loadout_builder
|
.unwrap_or_default();
|
||||||
.with_asset_expect("common.loadout.village.merchant", rng)
|
// modify stock for better gameplay
|
||||||
.back(Some(backpack))
|
|
||||||
.bag(ArmorSlot::Bag1, Some(food_bag))
|
|
||||||
.bag(ArmorSlot::Bag2, Some(potion_bag))
|
|
||||||
.bag(ArmorSlot::Bag3, Some(weapon_bag))
|
|
||||||
.bag(ArmorSlot::Bag4, Some(armor_bag))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sort_bag(bag: &mut Item) {
|
|
||||||
bag.slots_mut().sort_by(|a, b| match (a, b) {
|
|
||||||
(Some(a), Some(b)) => a.quality().cmp(&b.quality()),
|
|
||||||
(None, Some(_)) => cmp::Ordering::Greater,
|
|
||||||
(Some(_), None) => cmp::Ordering::Less,
|
|
||||||
(None, None) => cmp::Ordering::Equal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn armor_bag(economy: Option<&trade::SiteInformation>) -> Item {
|
|
||||||
#![warn(clippy::pedantic)]
|
|
||||||
|
|
||||||
let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
|
|
||||||
|
|
||||||
let armor_items = economy
|
|
||||||
.and_then(|e| e.unconsumed_stock.get(&Good::Armor))
|
|
||||||
.copied();
|
|
||||||
|
|
||||||
// If we have some uncomsumed armor, stock
|
|
||||||
if let Some(armor_good) = armor_items {
|
|
||||||
if armor_good < f32::EPSILON {
|
|
||||||
return bag;
|
|
||||||
}
|
|
||||||
for slot in bag.slots_mut() {
|
|
||||||
let amount = armor_good / 10.0;
|
|
||||||
if let Some(item_id) = TradePricing::random_item(Good::Armor, amount, true) {
|
|
||||||
*slot = Some(Item::new_from_asset_expect(&item_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort_bag(&mut bag);
|
|
||||||
|
|
||||||
bag
|
|
||||||
}
|
|
||||||
|
|
||||||
fn weapon_bag(economy: Option<&trade::SiteInformation>) -> Item {
|
|
||||||
#![warn(clippy::pedantic)]
|
|
||||||
|
|
||||||
let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
|
|
||||||
|
|
||||||
let weapons = economy
|
|
||||||
.and_then(|e| e.unconsumed_stock.get(&Good::Tools))
|
|
||||||
.copied();
|
|
||||||
|
|
||||||
// If we have some uncomsumed weapons, stock
|
|
||||||
if let Some(weapon_good) = weapons {
|
|
||||||
if weapon_good < f32::EPSILON {
|
|
||||||
return bag;
|
|
||||||
}
|
|
||||||
for slot in bag.slots_mut() {
|
|
||||||
let amount = weapon_good / 10.0;
|
|
||||||
if let Some(item_id) = TradePricing::random_item(Good::Tools, amount, true) {
|
|
||||||
*slot = Some(Item::new_from_asset_expect(&item_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort_bag(&mut bag);
|
|
||||||
|
|
||||||
bag
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ingredient_backpack(economy: Option<&trade::SiteInformation>, rng: &mut impl Rng) -> Item {
|
|
||||||
#![warn(clippy::pedantic)]
|
|
||||||
|
|
||||||
let mut bag = Item::new_from_asset_expect("common.items.armor.merchant.back");
|
|
||||||
let slots = bag.slots_mut();
|
|
||||||
|
|
||||||
// It's safe to truncate here, because coins clamped to 3000 max
|
|
||||||
// also we don't really want negative values here
|
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
||||||
let coins = economy
|
|
||||||
.and_then(|e| e.unconsumed_stock.get(&Good::Coin))
|
|
||||||
.copied()
|
|
||||||
.map(|cs| cs.min(rand::thread_rng().gen_range(1000.0..3000.0)) as u32);
|
|
||||||
let ingredients = economy
|
|
||||||
.and_then(|e| e.unconsumed_stock.get(&Good::Ingredients))
|
|
||||||
.copied();
|
|
||||||
|
|
||||||
// `to_skip` is ideologicaly boolean flag either to start from 0-th or 1-th slot
|
|
||||||
let mut to_skip = 0;
|
|
||||||
if let Some(coins) = coins.filter(|c| *c > 0) {
|
|
||||||
let mut coin_item = Item::new_from_asset_expect("common.items.utility.coins");
|
|
||||||
coin_item
|
|
||||||
.set_amount(coins)
|
|
||||||
.expect("coins should be stackable");
|
|
||||||
|
|
||||||
if let Some(slot) = slots.first_mut() {
|
|
||||||
*slot = Some(coin_item);
|
|
||||||
} else {
|
|
||||||
common_base::dev_panic!("Merchant backpack doesn't have slots");
|
|
||||||
}
|
|
||||||
// as we've placed coins, start from second slot
|
|
||||||
to_skip = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have some uncomsumed ingredients, trade
|
|
||||||
if let Some(ingredients_good) = ingredients {
|
|
||||||
let mut supply = ingredients_good / 10.0;
|
|
||||||
let tries = slots.len() - to_skip;
|
|
||||||
let good_map = gather_merged_goods(Good::Ingredients, &mut supply, tries, rng);
|
|
||||||
|
|
||||||
for (i, item) in good_map.into_iter().enumerate() {
|
|
||||||
// As we have at most (slots.len() - to_skip) items
|
|
||||||
// index won't panic
|
|
||||||
slots[i + to_skip] = Some(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort_bag(&mut bag);
|
|
||||||
|
|
||||||
bag
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consumable_bags(economy: Option<&trade::SiteInformation>, rng: &mut impl Rng) -> (Item, Item) {
|
|
||||||
#![warn(clippy::pedantic)]
|
|
||||||
|
|
||||||
let (mut bag3, mut bag4) = (
|
|
||||||
Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack"),
|
|
||||||
Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: currently econsim spends all its food on population, resulting in none
|
// TODO: currently econsim spends all its food on population, resulting in none
|
||||||
// for the players to buy; the `.max` is temporary to ensure that there's some
|
// for the players to buy; the `.max` is temporary to ensure that there's some
|
||||||
// food for sale at every site, to be used until we have some solution like NPC
|
// food for sale at every site, to be used until we have some solution like NPC
|
||||||
// houses as a limit on econsim population growth
|
// houses as a limit on econsim population growth
|
||||||
let mut food = economy
|
stockmap
|
||||||
.and_then(|e| e.unconsumed_stock.get(&Good::Food))
|
.entry(Good::Food)
|
||||||
.copied()
|
.and_modify(|e| *e = e.max(10_000.0))
|
||||||
.map_or(Some(10_000.0), |food| Some(food.max(10_000.0)));
|
.or_insert(10_000.0);
|
||||||
// Reduce amount of potions so merchants do not oversupply potions.
|
// Reduce amount of potions so merchants do not oversupply potions.
|
||||||
// TODO: Maybe remove when merchants and their inventories are rtsim?
|
// TODO: Maybe remove when merchants and their inventories are rtsim?
|
||||||
let mut potions = economy
|
// Note: Likely without effect now that potions are counted as food
|
||||||
.and_then(|e| e.unconsumed_stock.get(&Good::Potions))
|
stockmap
|
||||||
.copied()
|
.entry(Good::Potions)
|
||||||
.map(|potions| potions.powf(0.25));
|
.and_modify(|e| *e = e.powf(0.25));
|
||||||
|
// It's safe to truncate here, because coins clamped to 3000 max
|
||||||
|
// also we don't really want negative values here
|
||||||
|
stockmap
|
||||||
|
.entry(Good::Coin)
|
||||||
|
.and_modify(|e| *e = e.min(rng.gen_range(1000.0..3000.0)));
|
||||||
|
// assume roughly 10 merchants sharing a town's stock (other logic for coins)
|
||||||
|
stockmap
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(good, _amount)| **good != Good::Coin)
|
||||||
|
.for_each(|(_good, amount)| *amount *= 0.1);
|
||||||
|
// Fill bags with stuff according to unclaimed stock
|
||||||
|
let mut wares: Vec<Item> =
|
||||||
|
TradePricing::random_items(&mut stockmap, slots as u32, true, true, 16)
|
||||||
|
.iter()
|
||||||
|
.map(|(n, a)| {
|
||||||
|
let mut i = Item::new_from_asset_expect(n);
|
||||||
|
i.set_amount(*a)
|
||||||
|
.map_err(|_| tracing::error!("merchant loadout amount failure"))
|
||||||
|
.ok();
|
||||||
|
i
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
sort_wares(&mut wares);
|
||||||
|
transfer(&mut wares, &mut backpack);
|
||||||
|
transfer(&mut wares, &mut bag1);
|
||||||
|
transfer(&mut wares, &mut bag2);
|
||||||
|
transfer(&mut wares, &mut bag3);
|
||||||
|
transfer(&mut wares, &mut bag4);
|
||||||
|
|
||||||
let goods = [
|
loadout_builder
|
||||||
(Good::Food, &mut food, &mut bag3),
|
.with_asset_expect("common.loadout.village.merchant", rng)
|
||||||
(Good::Potions, &mut potions, &mut bag4),
|
.back(Some(backpack))
|
||||||
];
|
.bag(ArmorSlot::Bag1, Some(bag1))
|
||||||
|
.bag(ArmorSlot::Bag2, Some(bag2))
|
||||||
for (good_kind, goods, bag) in goods {
|
.bag(ArmorSlot::Bag3, Some(bag3))
|
||||||
// Try to get goods as many times as we have slots
|
.bag(ArmorSlot::Bag4, Some(bag4))
|
||||||
let mut supply = goods.unwrap_or(0.0) / 10.0;
|
|
||||||
let tries = bag.slots().len();
|
|
||||||
let good_map = gather_merged_goods(good_kind, &mut supply, tries, rng);
|
|
||||||
|
|
||||||
// Place them to the bags
|
|
||||||
let slots = bag.slots_mut();
|
|
||||||
for (i, item) in good_map.into_iter().enumerate() {
|
|
||||||
// As we have at most `slots.len()` items
|
|
||||||
// index won't panic
|
|
||||||
slots[i] = Some(item);
|
|
||||||
}
|
|
||||||
sort_bag(bag);
|
|
||||||
}
|
|
||||||
(bag3, bag4)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns vector of `tries` to gather given `good_kind` with given `supply`
|
fn sort_wares(bag: &mut Vec<Item>) {
|
||||||
/// Merges items if possible
|
use common::comp::item::TagExampleInfo;
|
||||||
#[warn(clippy::pedantic)]
|
|
||||||
fn gather_merged_goods(
|
|
||||||
good_kind: Good,
|
|
||||||
supply: &mut f32,
|
|
||||||
tries: usize,
|
|
||||||
rng: &mut impl Rng,
|
|
||||||
) -> Vec<Item> {
|
|
||||||
let mut good_map: Vec<Item> = Vec::new();
|
|
||||||
// NOTE: Conversion to and from f32 works fine
|
|
||||||
// because we make sure the
|
|
||||||
// number we're converting is 1..100.
|
|
||||||
#[allow(
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_sign_loss,
|
|
||||||
clippy::cast_precision_loss
|
|
||||||
)]
|
|
||||||
for _ in 0..tries {
|
|
||||||
if let Some(item_id) = TradePricing::random_item(good_kind, *supply, true) {
|
|
||||||
if *supply > 0.0 {
|
|
||||||
// clamp it with 1..n items per slot
|
|
||||||
// where n < stack_size, n < 16, n < supply
|
|
||||||
let mut new_item = Item::new_from_asset_expect(&item_id);
|
|
||||||
let supported_amount = new_item.max_amount();
|
|
||||||
let max = min(*supply as u32, min(16, supported_amount));
|
|
||||||
let n = if new_item.is_stackable() {
|
|
||||||
rng.gen_range(1..cmp::max(2, max))
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to merge with item we already have
|
bag.sort_by(|a, b| {
|
||||||
let old_item = good_map.iter_mut().find(|old_item| {
|
a.quality()
|
||||||
old_item.item_definition_id() == new_item.item_definition_id()
|
.cmp(&b.quality())
|
||||||
|
// sort by kind
|
||||||
|
.then(
|
||||||
|
Ord::cmp(
|
||||||
|
a.tags.first().map_or("", |tag| tag.name()),
|
||||||
|
b.tags.first().map_or("", |tag| tag.name()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// sort by name
|
||||||
|
.then(Ord::cmp(a.name(), b.name()))
|
||||||
});
|
});
|
||||||
let mut updated = false;
|
}
|
||||||
if let Some(item) = old_item {
|
|
||||||
if item.set_amount(item.amount() + n).is_ok() {
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push new pair if can't merge
|
fn transfer(wares: &mut Vec<Item>, bag: &mut Item) {
|
||||||
if !updated {
|
let capacity = bag.slots().len();
|
||||||
let _ = new_item.set_amount(n);
|
for (s, w) in bag
|
||||||
good_map.push(new_item);
|
.slots_mut()
|
||||||
|
.iter_mut()
|
||||||
|
.zip(wares.drain(0..wares.len().min(capacity)))
|
||||||
|
{
|
||||||
|
*s = Some(w);
|
||||||
}
|
}
|
||||||
// Don't forget to cut supply
|
|
||||||
*supply -= n as f32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
good_map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
Loading…
Reference in New Issue
Block a user