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
|
||||
- [Server] Kick clients who send messages on the wrong stream
|
||||
- Reworked Merchant trade price calculation, Merchants offer more wares
|
||||
|
||||
### Removed
|
||||
|
||||
|
@ -47,10 +47,9 @@ loot_tables: [
|
||||
// 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)
|
||||
good_scaling: [
|
||||
(Potions, 0.005), // common.items.consumable.potion_minor
|
||||
(Food, 0.15), // common.items.food.mushroom
|
||||
(Food, 0.0375), // common.items.food.mushroom
|
||||
(Coin, 1.0), // common.items.utility.coins
|
||||
(Armor, 0.5), // common.items.armor.misc.pants.worker_blue
|
||||
(Tools, 0.5), // common.items.weapons.staff.starter_staff
|
||||
(Ingredients, 0.5), // common.items.crafting_ing.leather_scraps
|
||||
(Armor, 0.025), // common.items.armor.misc.pants.worker_blue
|
||||
(Tools, 0.015487), // common.items.weapons.staff.starter_staff
|
||||
(Ingredients, 0.034626), // common.items.crafting_ing.leather_scraps
|
||||
])
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
assets::{self, AssetExt},
|
||||
comp::item::Item,
|
||||
lottery::LootSpec,
|
||||
recipe::{default_recipe_book, RecipeInput},
|
||||
trade::Good,
|
||||
@ -9,66 +10,208 @@ use hashbrown::HashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use std::cmp::Ordering;
|
||||
use tracing::{info, warn};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
const PRICING_DEBUG: bool = false;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TradePricing {
|
||||
// items of different good kinds
|
||||
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)>,
|
||||
items: PriceEntries,
|
||||
equality_set: EqualitySet,
|
||||
}
|
||||
|
||||
// item asset specifier, probability, whether it's sellable by merchants
|
||||
type Entry = (String, f32, bool);
|
||||
// combination logic:
|
||||
// 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)]
|
||||
struct Entries {
|
||||
entries: Vec<Entry>,
|
||||
/// Material equivalent for an item (price)
|
||||
#[derive(Default, Debug, Clone)]
|
||||
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 {
|
||||
fn add(&mut self, eqset: &EqualitySet, item_name: &str, probability: f32, can_sell: bool) {
|
||||
let canonical_itemname = eqset.canonical(item_name);
|
||||
|
||||
let old = self
|
||||
.entries
|
||||
// used by the add variants
|
||||
fn vector_add_eq(result: &mut Vec<(f32, Good)>, rhs: &[(f32, Good)]) {
|
||||
for (amount, good) in rhs {
|
||||
if result
|
||||
.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
|
||||
if let Some((asset, ref mut old_probability, _)) = old {
|
||||
if PRICING_DEBUG {
|
||||
info!("Update {} {}+{}", asset, old_probability, probability);
|
||||
impl std::ops::Add for MaterialUse {
|
||||
type Output = Self;
|
||||
|
||||
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 {
|
||||
if PRICING_DEBUG {
|
||||
info!("New {} {}", item_name, probability);
|
||||
self.0.push(b);
|
||||
}
|
||||
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
|
||||
// It will have infinity as its price, but it's fine,
|
||||
// because we determine all prices based on canonical value
|
||||
if canonical_itemname != item_name
|
||||
&& !self.entries.iter().any(|(name, _, _)| name == item_name)
|
||||
{
|
||||
self.entries.push((item_name.to_owned(), 0.0, can_sell));
|
||||
if canonical_itemname != item_name && !self.0.iter().any(|elem| elem.name == *item_name) {
|
||||
self.0.push(FreqEntry {
|
||||
name: item_name.to_owned(),
|
||||
freq: Default::default(),
|
||||
sell: can_sell,
|
||||
stackable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,27 +340,10 @@ impl assets::Compound for EqualitySet {
|
||||
struct RememberedRecipe {
|
||||
output: String,
|
||||
amount: u32,
|
||||
material_cost: f32,
|
||||
material_cost: Option<f32>,
|
||||
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 {
|
||||
contents
|
||||
.good_scaling
|
||||
@ -231,135 +357,109 @@ impl TradePricing {
|
||||
const CRAFTING_FACTOR: f32 = 0.95;
|
||||
// increase price a bit compared to sum of ingredients
|
||||
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 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] {
|
||||
fn good_from_item(name: &str) -> Good {
|
||||
match name {
|
||||
// Armor
|
||||
_ if name.starts_with("common.items.armor.") => &self.armor.entries,
|
||||
// Tools
|
||||
_ if name.starts_with("common.items.weapons.") => &self.tools.entries,
|
||||
_ if name.starts_with("common.items.tool.") => &self.tools.entries,
|
||||
// Ingredients
|
||||
_ if name.starts_with("common.items.crafting_ing.") => &self.ingredients.entries,
|
||||
_ if name.starts_with("common.items.mineral.") => &self.ingredients.entries,
|
||||
_ if name.starts_with("common.items.flowers.") => &self.ingredients.entries,
|
||||
// Potions
|
||||
_ if name.starts_with("common.items.consumable.") => &self.potions.entries,
|
||||
// Food
|
||||
_ if name.starts_with("common.items.food.") => &self.food.entries,
|
||||
// Other
|
||||
_ 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.boss_drops.") => &self.other.entries,
|
||||
_ if name.starts_with("common.items.crafting_tools.") => &self.other.entries,
|
||||
_ if name.starts_with("common.items.lantern.") => &self.other.entries,
|
||||
_ if name.starts_with("common.items.armor.") => Good::Armor,
|
||||
|
||||
_ if name.starts_with("common.items.weapons.") => Good::Tools,
|
||||
_ if name.starts_with("common.items.tool.") => Good::Tools,
|
||||
|
||||
_ if name.starts_with("common.items.crafting_ing.") => Good::Ingredients,
|
||||
_ if name.starts_with("common.items.mineral.") => Good::Ingredients,
|
||||
_ if name.starts_with("common.items.flowers.") => Good::Ingredients,
|
||||
|
||||
_ if name.starts_with("common.items.consumable.") => Good::Potions,
|
||||
|
||||
_ if name.starts_with("common.items.food.") => Good::Food,
|
||||
|
||||
Self::COIN_ITEM => Good::Coin,
|
||||
|
||||
_ if name.starts_with("common.items.glider.") => Good::default(),
|
||||
_ if name.starts_with("common.items.utility.") => Good::default(),
|
||||
_ if name.starts_with("common.items.boss_drops.") => Good::default(),
|
||||
_ if name.starts_with("common.items.crafting_tools.") => Good::default(),
|
||||
_ if name.starts_with("common.items.lantern.") => Good::default(),
|
||||
_ => {
|
||||
warn!("unknown loot item {}", name);
|
||||
&self.other.entries
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Good::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// look up price (inverse frequency) of an item
|
||||
fn price_lookup(&self, eqset: &EqualitySet, requested_name: &str) -> f32 {
|
||||
let canonical_name = eqset.canonical(requested_name);
|
||||
|
||||
let goods = self.get_list_by_path(canonical_name);
|
||||
// even if we multiply by INVEST_FACTOR we need to remain
|
||||
// above UNAVAILABLE_PRICE (add 1.0 to compensate rounding errors)
|
||||
goods
|
||||
fn price_lookup(&self, requested_name: &str) -> Option<&MaterialUse> {
|
||||
let canonical_name = self.equality_set.canonical(requested_name);
|
||||
self.items
|
||||
.0
|
||||
.iter()
|
||||
.find(|(name, _, _)| name == canonical_name)
|
||||
.map_or(
|
||||
Self::UNAVAILABLE_PRICE / Self::INVEST_FACTOR + 1.0,
|
||||
|(_, freq, _)| 1.0 / freq,
|
||||
)
|
||||
.find(|e| e.name == canonical_name)
|
||||
.map(|e| &e.price)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn calculate_material_cost(&self, r: &RememberedRecipe, eqset: &EqualitySet) -> f32 {
|
||||
fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
|
||||
r.input
|
||||
.iter()
|
||||
.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
|
||||
// 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() {
|
||||
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());
|
||||
//info!(?recipes);
|
||||
// put None to the end
|
||||
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
|
||||
.first()
|
||||
.filter(|recipe| recipe.material_cost < Self::UNAVAILABLE_PRICE)
|
||||
.filter(|recipe| recipe.material_cost.is_some())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
// #[allow(clippy::cast_precision_loss)]
|
||||
fn read() -> Self {
|
||||
let mut result = Self::default();
|
||||
let mut freq = FreqEntries::default();
|
||||
let price_config =
|
||||
TradingPriceFile::load_expect("common.trading.item_price_calculation").read();
|
||||
let eqset = EqualitySet::load_expect("common.trading.item_price_equality").read();
|
||||
result.equality_set = eqset.clone();
|
||||
result.equality_set = EqualitySet::load_expect("common.trading.item_price_equality")
|
||||
.read()
|
||||
.clone();
|
||||
for table in &price_config.loot_tables {
|
||||
if PRICING_DEBUG {
|
||||
info!(?table);
|
||||
@ -367,14 +467,55 @@ impl TradePricing {
|
||||
let (frequency, can_sell, asset_path) = table;
|
||||
let loot = ProbabilityFile::load_expect(asset_path);
|
||||
for (p, item_asset, amount) in &loot.read().content {
|
||||
result.get_list_by_path_mut(item_asset).add(
|
||||
&eqset,
|
||||
let good = Self::good_from_item(item_asset);
|
||||
let scaling = get_scaling(&price_config, good);
|
||||
freq.add(
|
||||
&result.equality_set,
|
||||
item_asset,
|
||||
frequency * p * *amount,
|
||||
good,
|
||||
frequency * p * *amount * scaling,
|
||||
*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
|
||||
let book = default_recipe_book().read();
|
||||
@ -384,7 +525,7 @@ impl TradePricing {
|
||||
ordered_recipes.push(RememberedRecipe {
|
||||
output: asset_path.id().into(),
|
||||
amount,
|
||||
material_cost: Self::UNAVAILABLE_PRICE,
|
||||
material_cost: None,
|
||||
input: recipe
|
||||
.inputs
|
||||
.iter()
|
||||
@ -406,106 +547,130 @@ impl TradePricing {
|
||||
|
||||
// re-evaluate prices based on crafting tables
|
||||
// (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| {
|
||||
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
|
||||
} else if recipe.material_cost < Self::UNAVAILABLE_PRICE {
|
||||
let actual_cost = result.calculate_material_cost(recipe, &eqset);
|
||||
} else if recipe.material_cost.is_some() {
|
||||
let actual_cost = result.calculate_material_cost(recipe);
|
||||
if let Some(usage) = actual_cost {
|
||||
let output_tradeable = recipe.input.iter().all(|(input, _)| {
|
||||
result
|
||||
.get_list_by_path(input)
|
||||
.items
|
||||
.0
|
||||
.iter()
|
||||
.find(|(item, _, _)| item == input)
|
||||
.map_or(false, |(_, _, tradeable)| *tradeable)
|
||||
.find(|item| item.name == *input)
|
||||
.map_or(false, |item| item.sell)
|
||||
});
|
||||
result.get_list_by_path_mut(&recipe.output).add(
|
||||
&eqset,
|
||||
&recipe.output,
|
||||
(recipe.amount as f32) / actual_cost * Self::CRAFTING_FACTOR,
|
||||
output_tradeable,
|
||||
);
|
||||
let item = Item::new_from_asset_expect(&recipe.output);
|
||||
let stackable = item.is_stackable();
|
||||
result.items.add_alternative(PriceEntry {
|
||||
name: recipe.output.clone(),
|
||||
price: usage * (recipe.amount as f32) * Self::CRAFTING_FACTOR,
|
||||
sell: output_tradeable,
|
||||
stackable,
|
||||
});
|
||||
} else {
|
||||
error!("Recipe {:?} incomplete confusion", recipe);
|
||||
}
|
||||
false
|
||||
} else {
|
||||
// handle incomplete recipes later
|
||||
true
|
||||
}
|
||||
});
|
||||
//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
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss
|
||||
)]
|
||||
fn random_item_impl(&self, good: Good, amount: f32, selling: bool) -> Option<String> {
|
||||
if good == Good::Coin {
|
||||
Some(Self::COIN_ITEM.into())
|
||||
} else {
|
||||
let table = self.get_list(good);
|
||||
if table.is_empty()
|
||||
|| (selling && table.iter().filter(|(_, _, can_sell)| *can_sell).count() == 0)
|
||||
{
|
||||
warn!("Good: {:?}, was unreachable.", good);
|
||||
return None;
|
||||
}
|
||||
let upper = table.len();
|
||||
let lower = table
|
||||
// TODO: optimize repeated use
|
||||
fn random_items_impl(
|
||||
&self,
|
||||
stockmap: &mut HashMap<Good, f32>,
|
||||
mut number: u32,
|
||||
selling: bool,
|
||||
always_coin: bool,
|
||||
limit: u32,
|
||||
) -> Vec<(String, u32)> {
|
||||
let mut candidates: Vec<&PriceEntry> = self
|
||||
.items
|
||||
.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|i| i.1.1 * amount >= 1.0)
|
||||
.map_or(upper - 1, |i| i.0);
|
||||
loop {
|
||||
let index =
|
||||
(rand::random::<f32>() * ((upper - lower) as f32)).floor() as usize + lower;
|
||||
if table.get(index).map_or(false, |i| !selling || i.2) {
|
||||
break table.get(index).map(|i| i.0.clone());
|
||||
.filter(|i| {
|
||||
let excess = i
|
||||
.price
|
||||
.iter()
|
||||
.find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
|
||||
excess.is_none()
|
||||
&& (!selling || i.sell)
|
||||
&& (!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;
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn random_item(good: Good, amount: f32, selling: bool) -> Option<String> {
|
||||
TRADE_PRICING.random_item_impl(good, amount, selling)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_material(item: &str) -> (Good, f32) {
|
||||
if item == Self::COIN_ITEM {
|
||||
(Good::Coin, 1.0)
|
||||
let index = (rand::random::<f32>() * candidates.len() as f32).floor() as usize;
|
||||
let result2 = candidates[index];
|
||||
let amount: u32 = if result2.stackable {
|
||||
let max_amount = result2
|
||||
.price
|
||||
.iter()
|
||||
.map(|e| {
|
||||
stockmap
|
||||
.get_mut(&e.1)
|
||||
.map(|stock| *stock / e.0.max(0.001))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.fold(f32::INFINITY, f32::min)
|
||||
.min(limit as f32);
|
||||
(rand::random::<f32>() * (max_amount - 1.0)).floor() as u32 + 1
|
||||
} else {
|
||||
let item = TRADE_PRICING.equality_set.canonical(item);
|
||||
|
||||
TRADE_PRICING.material_cache.get(item).copied().map_or(
|
||||
(Good::Terrain(crate::terrain::BiomeKind::Void), 0.0),
|
||||
|(a, b)| (a, b * TRADE_PRICING.coin_scale),
|
||||
)
|
||||
1
|
||||
};
|
||||
for i in result2.price.iter() {
|
||||
stockmap.get_mut(&i.1).map(|v| *v -= i.0 * (amount as f32));
|
||||
}
|
||||
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)]
|
||||
@ -513,69 +678,32 @@ impl TradePricing {
|
||||
|
||||
#[cfg(test)]
|
||||
fn print_sorted(&self) {
|
||||
use crate::comp::item::{armor, tool, Item, 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
use crate::comp::item::{armor, tool, ItemKind};
|
||||
|
||||
println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
|
||||
|
||||
printvec(
|
||||
"Armor",
|
||||
&self.armor.entries,
|
||||
|i, p| {
|
||||
fn more_information(i: &Item, p: f32) -> (String, &'static str) {
|
||||
if let ItemKind::Armor(a) = &i.kind {
|
||||
(
|
||||
match a.protection() {
|
||||
Some(armor::Protection::Invincible) => "Invincible".into(),
|
||||
Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
|
||||
None => "0.0".into(),
|
||||
}
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
},
|
||||
"prot/val",
|
||||
);
|
||||
printvec(
|
||||
"Tools",
|
||||
&self.tools.entries,
|
||||
|i, p| {
|
||||
if let ItemKind::Tool(t) = &i.kind {
|
||||
)
|
||||
} else if let ItemKind::Tool(t) = &i.kind {
|
||||
(
|
||||
match &t.stats {
|
||||
tool::StatKind::Direct(d) => {
|
||||
format!("{:.4}", d.power * d.speed * p)
|
||||
},
|
||||
tool::StatKind::Modular => "Modular".into(),
|
||||
}
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
},
|
||||
"dps/val",
|
||||
);
|
||||
printvec(
|
||||
"Potions",
|
||||
&self.potions.entries,
|
||||
|i, p| {
|
||||
if let ItemKind::Consumable { kind: _, effects } = &i.kind {
|
||||
)
|
||||
} else if let ItemKind::Consumable { kind: _, effects } = &i.kind {
|
||||
(
|
||||
effects
|
||||
.iter()
|
||||
.map(|e| {
|
||||
@ -586,43 +714,53 @@ impl TradePricing {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
},
|
||||
.join(" "),
|
||||
"str/val",
|
||||
);
|
||||
printvec(
|
||||
"Food",
|
||||
&self.food.entries,
|
||||
|i, p| {
|
||||
if let ItemKind::Consumable { kind: _, effects } = &i.kind {
|
||||
effects
|
||||
)
|
||||
} else {
|
||||
(Default::default(), "")
|
||||
}
|
||||
}
|
||||
let mut sorted: Vec<(f32, &PriceEntry)> = self
|
||||
.items
|
||||
.0
|
||||
.iter()
|
||||
.map(|e| {
|
||||
if let crate::effect::Effect::Buff(b) = e {
|
||||
format!("{:.2}", b.data.strength * p)
|
||||
} else {
|
||||
format!("{:?}", e)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
.map(|e| (e.price.iter().map(|i| i.0.to_owned()).sum(), e))
|
||||
.collect();
|
||||
sorted.sort_by(|(p, e), (p2, e2)| {
|
||||
p2.partial_cmp(p)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
.then(e.name.cmp(&e2.name))
|
||||
});
|
||||
|
||||
for (
|
||||
pricesum,
|
||||
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();
|
||||
info!("init");
|
||||
|
||||
for _ in 0..5 {
|
||||
if let Some(item_id) = TradePricing::random_item(Good::Armor, 5.0, false) {
|
||||
info!("Armor 5 {}", item_id);
|
||||
}
|
||||
let mut stock: hashbrown::HashMap<Good, f32> = vec![
|
||||
(Good::Ingredients, 50.0),
|
||||
(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()
|
||||
.and_then(|ri| {
|
||||
ri.inventory.get(slot).map(|item| {
|
||||
let (material, factor) = TradePricing::get_material(&item.name);
|
||||
self.values.get(&material).cloned().unwrap_or_default()
|
||||
* factor
|
||||
if let Some(vec) = TradePricing::get_materials(&item.name) {
|
||||
vec.iter()
|
||||
.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)
|
||||
* if reduce { material.trade_margin() } else { 1.0 }
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
|
@ -3730,17 +3730,26 @@ impl Hud {
|
||||
false,
|
||||
);
|
||||
if let Some(item) = inventory.get(slot) {
|
||||
let (material, factor) =
|
||||
TradePricing::get_material(item.item_definition_id());
|
||||
let mut unit_price = prices
|
||||
if let Some(materials) =
|
||||
TradePricing::get_materials(item.item_definition_id())
|
||||
{
|
||||
let unit_price: f32 = materials
|
||||
.iter()
|
||||
.map(|e| {
|
||||
prices
|
||||
.values
|
||||
.get(&material)
|
||||
.get(&e.1)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
* factor;
|
||||
if ours {
|
||||
unit_price *= material.trade_margin();
|
||||
}
|
||||
* e.0
|
||||
* (if ours {
|
||||
e.1.trade_margin()
|
||||
} else {
|
||||
1.0
|
||||
})
|
||||
})
|
||||
.sum();
|
||||
|
||||
let mut float_delta = if ours ^ remove {
|
||||
(balance1 - balance0) / unit_price
|
||||
} else {
|
||||
@ -3754,6 +3763,7 @@ impl Hud {
|
||||
*quantity = float_delta.max(0.0) as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match slot {
|
||||
Inventory(i) => {
|
||||
|
@ -22,13 +22,25 @@ pub fn price_desc(
|
||||
i18n: &Localization,
|
||||
) -> Option<(String, String, f32)> {
|
||||
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 buyprice = prices.values.get(&material).cloned().unwrap_or_default() * factor;
|
||||
let sellprice = buyprice * material.trade_margin();
|
||||
let buyprice: f32 = materials
|
||||
.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)
|
||||
/ prices.values.get(&Good::Coin).cloned().unwrap_or(1.0);
|
||||
let deal_goodness: f32 = materials
|
||||
.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 buy_string = format!(
|
||||
"{} : {:0.1} {}",
|
||||
@ -53,6 +65,9 @@ pub fn price_desc(
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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 rand::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
cmp::{self, min},
|
||||
collections::VecDeque,
|
||||
f32,
|
||||
hash::BuildHasherDefault,
|
||||
};
|
||||
use std::{collections::VecDeque, f32, hash::BuildHasherDefault};
|
||||
use vek::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -1019,236 +1014,96 @@ pub fn merchant_loadout(
|
||||
) -> LoadoutBuilder {
|
||||
let rng = &mut rand::thread_rng();
|
||||
|
||||
// Fill backpack with ingredients and coins
|
||||
let backpack = ingredient_backpack(economy, rng);
|
||||
|
||||
// Fill bags with stuff
|
||||
let (food_bag, potion_bag) = consumable_bags(economy, rng);
|
||||
let weapon_bag = weapon_bag(economy);
|
||||
let armor_bag = armor_bag(economy);
|
||||
|
||||
loadout_builder
|
||||
.with_asset_expect("common.loadout.village.merchant", rng)
|
||||
.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"),
|
||||
);
|
||||
let mut backpack = Item::new_from_asset_expect("common.items.armor.misc.back.backpack");
|
||||
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");
|
||||
let mut bag3 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
|
||||
let mut bag4 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
|
||||
let slots = backpack.slots().len() + 4 * bag1.slots().len();
|
||||
let mut stockmap: HashMap<Good, f32> = economy
|
||||
.map(|e| e.unconsumed_stock.clone())
|
||||
.unwrap_or_default();
|
||||
// modify stock for better gameplay
|
||||
|
||||
// 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
|
||||
// food for sale at every site, to be used until we have some solution like NPC
|
||||
// houses as a limit on econsim population growth
|
||||
let mut food = economy
|
||||
.and_then(|e| e.unconsumed_stock.get(&Good::Food))
|
||||
.copied()
|
||||
.map_or(Some(10_000.0), |food| Some(food.max(10_000.0)));
|
||||
stockmap
|
||||
.entry(Good::Food)
|
||||
.and_modify(|e| *e = e.max(10_000.0))
|
||||
.or_insert(10_000.0);
|
||||
// Reduce amount of potions so merchants do not oversupply potions.
|
||||
// TODO: Maybe remove when merchants and their inventories are rtsim?
|
||||
let mut potions = economy
|
||||
.and_then(|e| e.unconsumed_stock.get(&Good::Potions))
|
||||
.copied()
|
||||
.map(|potions| potions.powf(0.25));
|
||||
// Note: Likely without effect now that potions are counted as food
|
||||
stockmap
|
||||
.entry(Good::Potions)
|
||||
.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 = [
|
||||
(Good::Food, &mut food, &mut bag3),
|
||||
(Good::Potions, &mut potions, &mut bag4),
|
||||
];
|
||||
|
||||
for (good_kind, goods, bag) in goods {
|
||||
// Try to get goods as many times as we have slots
|
||||
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)
|
||||
loadout_builder
|
||||
.with_asset_expect("common.loadout.village.merchant", rng)
|
||||
.back(Some(backpack))
|
||||
.bag(ArmorSlot::Bag1, Some(bag1))
|
||||
.bag(ArmorSlot::Bag2, Some(bag2))
|
||||
.bag(ArmorSlot::Bag3, Some(bag3))
|
||||
.bag(ArmorSlot::Bag4, Some(bag4))
|
||||
}
|
||||
|
||||
/// Returns vector of `tries` to gather given `good_kind` with given `supply`
|
||||
/// Merges items if possible
|
||||
#[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
|
||||
};
|
||||
fn sort_wares(bag: &mut Vec<Item>) {
|
||||
use common::comp::item::TagExampleInfo;
|
||||
|
||||
// Try to merge with item we already have
|
||||
let old_item = good_map.iter_mut().find(|old_item| {
|
||||
old_item.item_definition_id() == new_item.item_definition_id()
|
||||
bag.sort_by(|a, b| {
|
||||
a.quality()
|
||||
.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
|
||||
if !updated {
|
||||
let _ = new_item.set_amount(n);
|
||||
good_map.push(new_item);
|
||||
fn transfer(wares: &mut Vec<Item>, bag: &mut Item) {
|
||||
let capacity = bag.slots().len();
|
||||
for (s, w) in bag
|
||||
.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)]
|
||||
|
Loading…
Reference in New Issue
Block a user