Merge branch 'christof/econfix' into 'master'

Small economy cleanup

See merge request veloren/veloren!3164
This commit is contained in:
Dominik Broński 2022-02-03 21:48:01 +00:00
commit 19c764dde6
2 changed files with 227 additions and 120 deletions

View File

@ -2,32 +2,32 @@
loot_tables: [ loot_tables: [
// balance the loot tables against each other (higher= more common= smaller price) // balance the loot tables against each other (higher= more common= smaller price)
// Weapons // Weapons
(32.0, true, "common.loot_tables.weapons.starter"), (192.0, true, "common.loot_tables.weapons.starter"),
(0.025, false, "common.loot_tables.weapons.cultist"), (0.075, false, "common.loot_tables.weapons.cultist"),
(0.025, false, "common.loot_tables.weapons.cave"), (0.075, false, "common.loot_tables.weapons.cave"),
(0.02, false, "common.loot_tables.weapons.legendary"), (0.04, false, "common.loot_tables.weapons.legendary"),
// Weapons sets // Weapons sets
(16.0, true, "common.loot_tables.weapons.tier-0"), (80.0, true, "common.loot_tables.weapons.tier-0"),
(8.0, true, "common.loot_tables.weapons.tier-1"), (48.0, true, "common.loot_tables.weapons.tier-1"),
(1.0, true, "common.loot_tables.weapons.tier-2"), (6.0, true, "common.loot_tables.weapons.tier-2"),
(0.125, true, "common.loot_tables.weapons.tier-3"), (0.75, true, "common.loot_tables.weapons.tier-3"),
(0.0625, false, "common.loot_tables.weapons.tier-4"), (0.375, false, "common.loot_tables.weapons.tier-4"),
(0.03, false, "common.loot_tables.weapons.tier-5"), (0.18, false, "common.loot_tables.weapons.tier-5"),
// Non-craftable Armor // Non-craftable Armor
(20.0, true, "common.loot_tables.armor.cloth"), (640, true, "common.loot_tables.armor.cloth"),
(1.0, true, "common.loot_tables.armor.twigs"), (6.0, true, "common.loot_tables.armor.twigs"),
(1.0, true, "common.loot_tables.armor.twigsflowers"), (6.0, true, "common.loot_tables.armor.twigsflowers"),
(1.0, true, "common.loot_tables.armor.twigsleaves"), (6.0, true, "common.loot_tables.armor.twigsleaves"),
(0.01, false, "common.trading.jewellery"), (0.02, false, "common.trading.jewellery"),
// Ingredients // Ingredients
(1.0, true, "common.trading.sellable_materials"), (72.5, true, "common.trading.sellable_materials"),
(1.0, false, "common.trading.unsellable_materials"), (1.7205, false, "common.trading.unsellable_materials"),
// Food Ingredients // Food Ingredients
(1.0, true, "common.trading.food"), (20.375, true, "common.trading.food"),
// Potions // Potions
// //
@ -39,9 +39,9 @@ loot_tables: [
// and economy. // and economy.
// //
// Collections // Collections
(0.00001, false, "common.trading.collection"), (0.00026, false, "common.trading.collection"),
// Manual balance // Manual balance
(1.0, false, "common.trading.balance"), (81.0, false, "common.trading.balance"),
], ],
// this is the amount of that good the most common item represents // this is the amount of that good the most common item represents

View File

@ -1,9 +1,6 @@
#![warn(clippy::pedantic)]
//#![warn(clippy::nursery)]
use crate::{ use crate::{
assets::{self, AssetExt}, assets::{self, AssetExt},
lottery::{LootSpec, Lottery}, lottery::LootSpec,
recipe::{default_recipe_book, RecipeInput}, recipe::{default_recipe_book, RecipeInput},
trade::Good, trade::Good,
}; };
@ -11,6 +8,7 @@ use assets::AssetGuard;
use hashbrown::HashMap; use hashbrown::HashMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::Deserialize; use serde::Deserialize;
use std::cmp::Ordering;
use tracing::{info, warn}; use tracing::{info, warn};
const PRICING_DEBUG: bool = false; const PRICING_DEBUG: bool = false;
@ -27,7 +25,6 @@ pub struct TradePricing {
// good_scaling of coins // good_scaling of coins
coin_scale: f32, coin_scale: f32,
// rng: ChaChaRng,
// get amount of material per item // get amount of material per item
material_cache: HashMap<String, (Good, f32)>, material_cache: HashMap<String, (Good, f32)>,
@ -81,8 +78,11 @@ lazy_static! {
} }
#[derive(Clone)] #[derive(Clone)]
struct ProbabilityFile { /// A collection of items with probabilty (normalized to one), created
pub content: Vec<(f32, String)>, /// hierarchically from `LootSpec`s
/// (probability, item id, average amount)
pub struct ProbabilityFile {
pub content: Vec<(f32, String, f32)>,
} }
impl assets::Asset for ProbabilityFile { impl assets::Asset for ProbabilityFile {
@ -94,22 +94,25 @@ impl assets::Asset for ProbabilityFile {
impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile { impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
fn from(content: Vec<(f32, LootSpec<String>)>) -> Self { fn from(content: Vec<(f32, LootSpec<String>)>) -> Self {
let rescale = if content.is_empty() {
1.0
} else {
1.0 / content.iter().map(|e| e.0).sum::<f32>()
};
Self { Self {
content: content content: content
.into_iter() .into_iter()
.flat_map(|(p0, loot)| match loot { .flat_map(|(p0, loot)| match loot {
LootSpec::Item(asset) => vec![(p0, asset)].into_iter(), LootSpec::Item(asset) => vec![(p0 * rescale, asset, 1.0)].into_iter(),
LootSpec::ItemQuantity(asset, a, b) => { LootSpec::ItemQuantity(asset, a, b) => {
vec![(p0 * (a + b) as f32 / 2.0, asset)].into_iter() vec![(p0 * rescale, asset, (a + b) as f32 * 0.5)].into_iter()
}, },
LootSpec::LootTable(table_asset) => { LootSpec::LootTable(table_asset) => {
let total = Lottery::<LootSpec<String>>::load_expect(&table_asset) let unscaled = &Self::load_expect(&table_asset).read().content;
.read() let scale = p0 * rescale;
.total(); unscaled
Self::load_expect_cloned(&table_asset) .iter()
.content .map(|(p1, asset, amount)| (*p1 * scale, asset.clone(), *amount))
.into_iter()
.map(|(p1, asset)| (p0 * p1 / total, asset))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into_iter() .into_iter()
}, },
@ -171,7 +174,7 @@ impl assets::Compound for EqualitySet {
EqualitySpec::LootTable(table) => { EqualitySpec::LootTable(table) => {
let acc = &ProbabilityFile::load_expect(table).read().content; let acc = &ProbabilityFile::load_expect(table).read().content;
acc.iter().map(|(_p, item)| item).cloned().collect() acc.iter().map(|(_p, item, _)| item).cloned().collect()
}, },
EqualitySpec::Set(xs) => xs.clone(), EqualitySpec::Set(xs) => xs.clone(),
}; };
@ -200,7 +203,11 @@ struct RememberedRecipe {
fn sort_and_normalize(entryvec: &mut [Entry], scale: f32) { fn sort_and_normalize(entryvec: &mut [Entry], scale: f32) {
if !entryvec.is_empty() { if !entryvec.is_empty() {
entryvec.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 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() { if let Some((_, max_scale, _)) = entryvec.last() {
// most common item has frequency max_scale. avoid NaN // most common item has frequency max_scale. avoid NaN
let rescale = scale / max_scale; let rescale = scale / max_scale;
@ -359,11 +366,11 @@ 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) in &loot.read().content { for (p, item_asset, amount) in &loot.read().content {
result.get_list_by_path_mut(item_asset).add( result.get_list_by_path_mut(item_asset).add(
&eqset, &eqset,
item_asset, item_asset,
frequency * p, frequency * p * *amount,
*can_sell, *can_sell,
); );
} }
@ -475,7 +482,6 @@ impl TradePricing {
loop { loop {
let index = let index =
(rand::random::<f32>() * ((upper - lower) as f32)).floor() as usize + lower; (rand::random::<f32>() * ((upper - lower) as f32)).floor() as usize + lower;
//.gen_range(lower..upper);
if table.get(index).map_or(false, |i| !selling || i.2) { if table.get(index).map_or(false, |i| !selling || i.2) {
break table.get(index).map(|i| i.0.clone()); break table.get(index).map(|i| i.0.clone());
} }
@ -510,124 +516,225 @@ impl TradePricing {
use crate::comp::item::{armor, tool, Item, ItemKind}; use crate::comp::item::{armor, tool, Item, ItemKind};
// we pass the item and the inverse of the price to the closure // 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) fn printvec<F>(good_kind: &str, entries: &[(String, f32, bool)], f: F, unit: &str)
where where
F: Fn(&Item, f32) -> String, F: Fn(&Item, f32) -> String,
{ {
println!("\n======{:^15}======", good_kind);
for (item_id, p, can_sell) in entries.iter() { for (item_id, p, can_sell) in entries.iter() {
let it = Item::new_from_asset_expect(item_id); let it = Item::new_from_asset_expect(item_id);
let price = 1.0 / p; let price = 1.0 / p;
println!( println!(
"<{}> {}\n{:>4.2} {:?} {}", "{}, {}, {:>4.2}, {}, {:?}, {}, {},",
item_id, item_id,
if *can_sell { "+" } else { "-" }, if *can_sell { "yes" } else { "no" },
price, price,
good_kind,
it.quality, it.quality,
f(&it, *p) f(&it, *p),
unit,
); );
} }
} }
printvec("Armor", &self.armor.entries, |i, p| { println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
if let ItemKind::Armor(a) = &i.kind {
match a.protection() { printvec(
Some(armor::Protection::Invincible) => "Invincible".into(), "Armor",
Some(armor::Protection::Normal(x)) => format!("{:.4} prot/val", x * p), &self.armor.entries,
None => "0.0 prot/val".into(), |i, p| {
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)
} }
} else { },
format!("{:?}", i.kind) "prot/val",
} );
}); printvec(
printvec("Tools", &self.tools.entries, |i, p| { "Tools",
if let ItemKind::Tool(t) = &i.kind { &self.tools.entries,
match &t.stats { |i, p| {
tool::StatKind::Direct(d) => { if let ItemKind::Tool(t) = &i.kind {
format!("{:.4} dps/val", d.power * d.speed * p) match &t.stats {
}, tool::StatKind::Direct(d) => {
tool::StatKind::Modular => "Modular".into(), format!("{:.4}", d.power * d.speed * p)
},
tool::StatKind::Modular => "Modular".into(),
}
} else {
format!("{:?}", i.kind)
} }
} else { },
format!("{:?}", i.kind) "dps/val",
} );
}); printvec(
printvec("Potions", &self.potions.entries, |i, p| { "Potions",
if let ItemKind::Consumable { kind: _, effects } = &i.kind { &self.potions.entries,
effects |i, p| {
.iter() if let ItemKind::Consumable { kind: _, effects } = &i.kind {
.map(|e| { effects
if let crate::effect::Effect::Buff(b) = e { .iter()
format!("{:.2} str/val", b.data.strength * p) .map(|e| {
} else { if let crate::effect::Effect::Buff(b) = e {
format!("{:?}", e) format!("{:.2}", b.data.strength * p)
} } else {
}) format!("{:?}", e)
.collect::<Vec<String>>() }
.join(" ") })
} else { .collect::<Vec<String>>()
format!("{:?}", i.kind) .join(" ")
} } else {
}); format!("{:?}", i.kind)
printvec("Food", &self.food.entries, |i, p| { }
if let ItemKind::Consumable { kind: _, effects } = &i.kind { },
effects "str/val",
.iter() );
.map(|e| { printvec(
if let crate::effect::Effect::Buff(b) = e { "Food",
format!("{:.2} str/val", b.data.strength * p) &self.food.entries,
} else { |i, p| {
format!("{:?}", e) if let ItemKind::Consumable { kind: _, effects } = &i.kind {
} effects
}) .iter()
.collect::<Vec<String>>() .map(|e| {
.join(" ") if let crate::effect::Effect::Buff(b) = e {
} else { format!("{:.2}", b.data.strength * p)
format!("{:?}", i.kind) } else {
} format!("{:?}", e)
}); }
printvec("Ingredients", &self.ingredients.entries, |i, _p| { })
if let ItemKind::Ingredient { kind } = &i.kind { .collect::<Vec<String>>()
kind.clone() .join(" ")
} else { } else {
format!("{:?}", i.kind) format!("{:?}", i.kind)
} }
}); },
printvec("Other", &self.other.entries, |i, _p| { "str/val",
format!("{:?}", i.kind) );
}); printvec(
println!("<{}>\n{}", Self::COIN_ITEM, self.coin_scale); "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);
} }
} }
/// hierarchically combine and scale this loot table
#[must_use]
pub fn expand_loot_table(loot_table: &str) -> Vec<(f32, String, f32)> {
ProbabilityFile::from(vec![(1.0, LootSpec::LootTable(loot_table.into()))]).content
}
// if you want to take a look at the calculated values run: // if you want to take a look at the calculated values run:
// cd common && cargo test trade_pricing -- --nocapture // cd common && cargo test trade_pricing -- --nocapture
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{comp::inventory::trade_pricing::TradePricing, trade::Good}; use crate::{
use tracing::{info, Level}; comp::inventory::trade_pricing::{expand_loot_table, ProbabilityFile, TradePricing},
use tracing_subscriber::{ lottery::LootSpec,
filter::{EnvFilter, LevelFilter}, trade::Good,
FmtSubscriber,
}; };
use tracing::{info, Level};
use tracing_subscriber::{filter::EnvFilter, FmtSubscriber};
fn init() { fn init() {
FmtSubscriber::builder() FmtSubscriber::builder()
.with_max_level(Level::ERROR) .with_max_level(Level::ERROR)
.with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into())) .with_env_filter(EnvFilter::from_default_env())
.init(); .try_init()
.unwrap_or(());
} }
#[test] #[test]
fn test_prices() { fn test_loot_table() {
init();
info!("init");
let loot = expand_loot_table("common.loot_tables.creature.quad_medium.gentle");
let lootsum = loot.iter().fold(0.0, |s, i| s + i.0);
assert!((lootsum - 1.0).abs() < 1e-3);
// hierarchical
let loot2 = expand_loot_table("common.loot_tables.creature.quad_medium.catoblepas");
let lootsum2 = loot2.iter().fold(0.0, |s, i| s + i.0);
assert!((lootsum2 - 1.0).abs() < 1e-4);
// highly nested
let loot3 = expand_loot_table("common.loot_tables.creature.biped_large.wendigo");
let lootsum3 = loot3.iter().fold(0.0, |s, i| s + i.0);
assert!((lootsum3 - 1.0).abs() < 1e-5);
}
#[test]
fn test_prices1() {
init(); init();
info!("init"); info!("init");
TradePricing::instance().print_sorted(); TradePricing::instance().print_sorted();
}
#[test]
fn test_prices2() {
init();
info!("init");
for _ in 0..5 { for _ in 0..5 {
if let Some(item_id) = TradePricing::random_item(Good::Armor, 5.0, false) { if let Some(item_id) = TradePricing::random_item(Good::Armor, 5.0, false) {
info!("Armor 5 {}", item_id); info!("Armor 5 {}", item_id);
} }
} }
} }
fn normalized(probability: &ProbabilityFile) -> bool {
let sum = probability.content.iter().map(|(p, _, _)| p).sum::<f32>();
(dbg!(sum) - 1.0).abs() < 1e-3
}
#[test]
fn test_normalizing_table1() {
let item = |asset: &str| LootSpec::Item(asset.to_owned());
let loot_table = vec![(1.0, item("wow")), (1.0, item("nice"))];
let probability: ProbabilityFile = loot_table.into();
assert!(normalized(&probability));
}
#[test]
fn test_normalizing_table2() {
let table = |asset: &str| LootSpec::LootTable(asset.to_owned());
let loot_table = vec![(
1.0,
table("common.loot_tables.creature.quad_medium.catoblepas"),
)];
let probability: ProbabilityFile = loot_table.into();
assert!(normalized(&probability));
}
#[test]
fn test_normalizing_table3() {
let table = |asset: &str| LootSpec::LootTable(asset.to_owned());
let loot_table = vec![
(
1.0,
table("common.loot_tables.creature.quad_medium.catoblepas"),
),
(1.0, table("common.loot_tables.creature.quad_medium.gentle")),
];
let probability: ProbabilityFile = loot_table.into();
assert!(normalized(&probability));
}
#[test]
fn test_normalizing_table4() {
let quantity = |asset: &str, a, b| LootSpec::ItemQuantity(asset.to_owned(), a, b);
let loot_table = vec![(1.0, quantity("such", 3, 5)), (1.0, quantity("much", 5, 9))];
let probability: ProbabilityFile = loot_table.into();
assert!(normalized(&probability));
}
} }