use crate::{
assets::{self, AssetExt},
comp::{
inventory,
item::{
Item, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, MaterialStatManifest,
ModularBase,
},
tool::AbilityMap,
},
lottery::LootSpec,
recipe::{complete_recipe_book, default_component_recipe_book, RecipeInput},
trade::Good,
};
use assets::AssetReadGuard;
use hashbrown::HashMap;
use lazy_static::lazy_static;
use serde::Deserialize;
use std::cmp::Ordering;
use tracing::{error, info, warn};
use super::item::{Material, ToolKind};
const PRICING_DEBUG: bool = false;
#[derive(Default, Debug)]
pub struct TradePricing {
items: PriceEntries,
equality_set: EqualitySet,
}
#[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())
}
}
fn vector_add_eq(result: &mut Vec<(f32, Good)>, rhs: &[(f32, Good)]) {
for (amount, good) in rhs {
if result
.iter_mut()
.find(|(_amount2, good2)| *good == *good2)
.map(|elem| elem.0 += *amount)
.is_none()
{
result.push((*amount, *good));
}
}
}
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
}
}
impl std::ops::AddAssign for MaterialUse {
fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
}
impl std::iter::Sum<MaterialUse> for MaterialUse {
fn sum<I>(iter: I) -> Self
where
I: Iterator<Item = Self>,
{
let mut ret = Self::default();
for i in iter {
ret += i;
}
ret
}
}
impl std::ops::Deref for MaterialUse {
type Target = [(f32, Good)];
fn deref(&self) -> &Self::Target { self.0.deref() }
}
#[derive(Default, Debug, Clone)]
pub struct MaterialFrequency(Vec<(f32, Good)>);
fn vector_invert(result: &mut [(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
}
}
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(Debug)]
struct PriceEntry {
name: ItemDefinitionIdOwned,
price: MaterialUse,
sell: bool,
stackable: bool,
}
#[derive(Debug)]
struct FreqEntry {
name: ItemDefinitionIdOwned,
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) {
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 {
self.0.push(b);
}
}
}
impl FreqEntries {
fn add(
&mut self,
eqset: &EqualitySet,
item_name: &ItemDefinitionIdOwned,
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)]);
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 stackable = Item::new_from_item_definition_id(
canonical_itemname.as_ref(),
&AbilityMap::load().read(),
&MaterialStatManifest::load().read(),
)
.map_or(false, |i| i.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);
}
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,
});
}
}
}
lazy_static! {
static ref TRADE_PRICING: TradePricing = TradePricing::read();
}
#[derive(Clone)]
pub struct ProbabilityFile {
pub content: Vec<(f32, ItemDefinitionIdOwned, f32)>,
}
impl assets::Asset for ProbabilityFile {
type Loader = assets::LoadFrom<Vec<(f32, LootSpec<String>)>, assets::RonLoader>;
const EXTENSION: &'static str = "ron";
}
type ComponentPool =
HashMap<(ToolKind, String), Vec<(ItemDefinitionIdOwned, Option<inventory::item::Hands>)>>;
lazy_static! {
static ref PRIMARY_COMPONENT_POOL: ComponentPool = {
let mut component_pool = HashMap::new();
use crate::recipe::ComponentKey;
let recipes = default_component_recipe_book().read();
recipes
.iter()
.for_each(|(ComponentKey { toolkind, material, .. }, recipe)| {
let component = recipe.itemdef_output();
let hand_restriction = None; let entry: &mut Vec<_> = component_pool.entry((*toolkind, String::from(material))).or_default();
entry.push((component, hand_restriction));
});
component_pool
};
static ref SECONDARY_COMPONENT_POOL: ComponentPool = {
let mut component_pool = HashMap::new();
let recipes = complete_recipe_book().read();
recipes
.iter()
.for_each(|(_, recipe)| {
let (ref asset_path, _) = recipe.output;
if let ItemKind::ModularComponent(
crate::comp::inventory::item::modular::ModularComponent::ToolSecondaryComponent {
toolkind,
stats: _,
hand_restriction,
},
) = asset_path.kind
{
let component = ItemDefinitionIdOwned::Simple(asset_path.id().into());
let entry: &mut Vec<_> = component_pool.entry((toolkind, String::new())).or_default();
entry.push((component, hand_restriction));
}});
component_pool
};
}
pub fn expand_primary_component(
tool: ToolKind,
material: Material,
hand_restriction: Option<inventory::item::Hands>,
) -> Vec<ItemDefinitionIdOwned> {
if let Some(material_id) = material.asset_identifier() {
PRIMARY_COMPONENT_POOL
.get(&(tool, material_id.to_owned()))
.into_iter()
.flatten()
.filter(move |(_comp, hand)| match (hand_restriction, *hand) {
(Some(restriction), Some(hand)) => restriction == hand,
(None, _) | (_, None) => true,
})
.map(|e| e.0.clone())
.collect()
} else {
Vec::new()
}
}
pub fn expand_secondary_component(
tool: ToolKind,
_material: Material,
hand_restriction: Option<inventory::item::Hands>,
) -> impl Iterator<Item = ItemDefinitionIdOwned> {
SECONDARY_COMPONENT_POOL
.get(&(tool, String::new()))
.into_iter()
.flatten()
.filter(move |(_comp, hand)| match (hand_restriction, *hand) {
(Some(restriction), Some(hand)) => restriction == hand,
(None, _) | (_, None) => true,
})
.map(|e| e.0.clone())
}
impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
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>()
};
fn get_content(
rescale: f32,
p0: f32,
loot: LootSpec<String>,
) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
match loot {
LootSpec::Item(asset) => {
vec![(p0 * rescale, ItemDefinitionIdOwned::Simple(asset), 1.0)]
},
LootSpec::LootTable(table_asset) => {
let unscaled = &ProbabilityFile::load_expect(&table_asset).read().content;
let scale = p0 * rescale;
unscaled
.iter()
.map(|(p1, asset, amount)| (*p1 * scale, asset.clone(), *amount))
.collect::<Vec<_>>()
},
LootSpec::Lottery(table) => {
let unscaled = ProbabilityFile::from(table);
let scale = p0 * rescale;
unscaled
.content
.into_iter()
.map(|(p1, asset, amount)| (p1 * scale, asset, amount))
.collect::<Vec<_>>()
},
LootSpec::ModularWeapon {
tool,
material,
hands,
} => {
let mut primary = expand_primary_component(tool, material, hands);
let secondary: Vec<ItemDefinitionIdOwned> =
expand_secondary_component(tool, material, hands).collect();
let freq = if primary.is_empty() || secondary.is_empty() {
0.0
} else {
p0 * rescale / ((primary.len() * secondary.len()) as f32)
};
let res: Vec<(f32, ItemDefinitionIdOwned, f32)> = primary
.drain(0..)
.flat_map(|p| {
secondary.iter().map(move |s| {
let components = vec![p.clone(), s.clone()];
(
freq,
ItemDefinitionIdOwned::Modular {
pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
components,
},
1.0f32,
)
})
})
.collect();
res
},
LootSpec::ModularWeaponPrimaryComponent {
tool,
material,
hands,
} => {
let mut res = expand_primary_component(tool, material, hands);
let freq = if res.is_empty() {
0.0
} else {
p0 * rescale / (res.len() as f32)
};
let res: Vec<(f32, ItemDefinitionIdOwned, f32)> =
res.drain(0..).map(|e| (freq, e, 1.0f32)).collect();
res
},
LootSpec::Nothing => Vec::new(),
LootSpec::MultiDrop(loot, a, b) => {
let average_count = (a + b) as f32 * 0.5;
let mut content = get_content(rescale, p0, *loot);
for (_, _, count) in content.iter_mut() {
*count *= average_count;
}
content
},
LootSpec::All(loot_specs) => loot_specs
.into_iter()
.flat_map(|loot| get_content(rescale, p0, loot))
.collect(),
}
}
Self {
content: content
.into_iter()
.flat_map(|(p0, loot)| get_content(rescale, p0, loot))
.collect(),
}
}
}
#[derive(Debug, Deserialize)]
struct TradingPriceFile {
pub loot_tables: Vec<(f32, bool, String)>,
pub good_scaling: Vec<(Good, f32)>,
}
impl assets::Asset for TradingPriceFile {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
#[derive(Clone, Debug, Default)]
struct EqualitySet {
equivalence_class: HashMap<ItemDefinitionIdOwned, ItemDefinitionIdOwned>,
}
impl EqualitySet {
fn canonical<'a>(&'a self, item_name: &'a ItemDefinitionIdOwned) -> &'a ItemDefinitionIdOwned {
let canonical_itemname = self
.equivalence_class
.get(item_name)
.map_or(item_name, |i| i);
canonical_itemname
}
}
impl assets::Compound for EqualitySet {
fn load(
cache: assets::AnyCache,
id: &assets::SharedString,
) -> Result<Self, assets::BoxedError> {
#[derive(Debug, Deserialize)]
enum EqualitySpec {
LootTable(String),
Set(Vec<String>),
}
let mut eqset = Self {
equivalence_class: HashMap::new(),
};
let manifest = &cache.load::<assets::Ron<Vec<EqualitySpec>>>(id)?.read().0;
for set in manifest {
let items: Vec<ItemDefinitionIdOwned> = match set {
EqualitySpec::LootTable(table) => {
let acc = &ProbabilityFile::load_expect(table).read().content;
acc.iter().map(|(_p, item, _)| item).cloned().collect()
},
EqualitySpec::Set(xs) => xs
.iter()
.map(|s| ItemDefinitionIdOwned::Simple(s.clone()))
.collect(),
};
let mut iter = items.iter();
if let Some(first) = iter.next() {
eqset.equivalence_class.insert(first.clone(), first.clone());
for item in iter {
eqset.equivalence_class.insert(item.clone(), first.clone());
}
}
}
Ok(eqset)
}
}
#[derive(Debug)]
struct RememberedRecipe {
output: ItemDefinitionIdOwned,
amount: u32,
material_cost: Option<f32>,
input: Vec<(ItemDefinitionIdOwned, u32)>,
}
fn get_scaling(contents: &AssetReadGuard<TradingPriceFile>, good: Good) -> f32 {
contents
.good_scaling
.iter()
.find(|(good_kind, _)| *good_kind == good)
.map_or(1.0, |(_, scaling)| *scaling)
}
#[cfg(test)]
impl PartialOrd for ItemDefinitionIdOwned {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
#[cfg(test)]
impl Ord for ItemDefinitionIdOwned {
fn cmp(&self, other: &Self) -> Ordering {
match self {
ItemDefinitionIdOwned::Simple(na) => match other {
ItemDefinitionIdOwned::Simple(nb) => na.cmp(nb),
_ => Ordering::Less,
},
ItemDefinitionIdOwned::Modular {
pseudo_base,
components,
} => match other {
ItemDefinitionIdOwned::Simple(_) => Ordering::Greater,
ItemDefinitionIdOwned::Modular {
pseudo_base: pseudo_base2,
components: components2,
} => pseudo_base
.cmp(pseudo_base2)
.then_with(|| components.cmp(components2)),
_ => Ordering::Less,
},
ItemDefinitionIdOwned::Compound {
simple_base,
components,
} => match other {
ItemDefinitionIdOwned::Compound {
simple_base: simple_base2,
components: components2,
} => simple_base
.cmp(simple_base2)
.then_with(|| components.cmp(components2)),
_ => Ordering::Greater,
},
}
}
}
impl TradePricing {
const COIN_ITEM: &'static str = "common.items.utility.coins";
const CRAFTING_FACTOR: f32 = 0.95;
const INVEST_FACTOR: f32 = 0.33;
fn good_from_item(name: &ItemDefinitionIdOwned) -> Good {
match name {
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.armor.") => {
Good::Armor
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.weapons.") => {
Good::Tools
},
ItemDefinitionIdOwned::Simple(name)
if name.starts_with("common.items.modular.weapon.") =>
{
Good::Tools
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.tool.") => {
Good::Tools
},
ItemDefinitionIdOwned::Simple(name)
if name.starts_with("common.items.crafting_ing.") =>
{
Good::Ingredients
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.mineral.") => {
Good::Ingredients
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.log.") => {
Good::Wood
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.flowers.") => {
Good::Ingredients
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.consumable.") => {
Good::Potions
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.food.") => {
Good::Food
},
ItemDefinitionIdOwned::Simple(name) if name.as_str() == Self::COIN_ITEM => Good::Coin,
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.recipes.") => {
Good::Recipe
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.glider.") => {
Good::Tools
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.utility.") => {
Good::default()
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.boss_drops.") => {
Good::Tools
},
ItemDefinitionIdOwned::Simple(name)
if name.starts_with("common.items.crafting_tools.") =>
{
Good::default()
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.lantern.") => {
Good::Tools
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.keys.") => {
Good::Tools
},
ItemDefinitionIdOwned::Modular {
pseudo_base: _,
components: _,
} => Good::Tools,
ItemDefinitionIdOwned::Compound {
simple_base: _,
components: _,
} => Good::Ingredients,
_ => {
warn!("unknown loot item {:?}", name);
Good::default()
},
}
}
fn price_lookup(&self, requested_name: &ItemDefinitionIdOwned) -> Option<&MaterialUse> {
let canonical_name = self.equality_set.canonical(requested_name);
self.items
.0
.iter()
.find(|e| &e.name == canonical_name)
.map(|e| &e.price)
}
fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
r.input
.iter()
.map(|(name, amount)| {
self.price_lookup(name).map(|x| {
x.clone()
* (if *amount > 0 {
*amount as f32
} else {
Self::INVEST_FACTOR
})
})
})
.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))
}
fn sort_by_price(&self, recipes: &mut [RememberedRecipe]) -> bool {
for recipe in recipes.iter_mut() {
recipe.material_cost = self.calculate_material_cost_sum(recipe);
}
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 {
for i in recipes.iter() {
tracing::debug!("{:?}", *i);
}
}
recipes
.first()
.filter(|recipe| recipe.material_cost.is_some())
.is_some()
}
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();
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);
}
let (frequency, can_sell, asset_path) = table;
let loot = ProbabilityFile::load_expect(asset_path);
for (p, item_asset, amount) in &loot.read().content {
let good = Self::good_from_item(item_asset);
let scaling = get_scaling(&price_config, good);
freq.add(
&result.equality_set,
item_asset,
good,
frequency * p * *amount * scaling,
*can_sell,
);
}
}
freq.add(
&result.equality_set,
&ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
Good::Coin,
get_scaling(&price_config, Good::Coin),
true,
);
result.items.0.extend(freq.0.iter().map(|elem| {
if elem.freq.0.is_empty() {
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 {
for i in result.items.0.iter() {
tracing::debug!("before recipes {:?}", *i);
}
}
let mut secondaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
let book = complete_recipe_book().read();
let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
for (_, recipe) in book.iter() {
let (ref asset_path, amount) = recipe.output;
if let ItemKind::ModularComponent(
inventory::item::modular::ModularComponent::ToolSecondaryComponent {
toolkind,
stats: _,
hand_restriction: _,
},
) = asset_path.kind
{
secondaries
.entry(toolkind)
.or_default()
.push(ItemDefinitionIdOwned::Simple(asset_path.id().into()));
}
ordered_recipes.push(RememberedRecipe {
output: ItemDefinitionIdOwned::Simple(asset_path.id().into()),
amount,
material_cost: None,
input: recipe
.inputs
.iter()
.filter_map(|&(ref recipe_input, count, _)| {
if let RecipeInput::Item(it) = recipe_input {
if count == 0 {
None
} else {
Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
}
} else {
None
}
})
.collect(),
});
}
let mut primaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
let comp_book = default_component_recipe_book().read();
for (key, recipe) in comp_book.iter() {
primaries
.entry(key.toolkind)
.or_default()
.push(recipe.itemdef_output());
ordered_recipes.push(RememberedRecipe {
output: recipe.itemdef_output(),
amount: 1,
material_cost: None,
input: recipe
.inputs()
.filter_map(|(ref recipe_input, count)| {
if count == 0 {
None
} else {
match recipe_input {
RecipeInput::Item(it) => {
Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
},
RecipeInput::Tag(_) => todo!(),
RecipeInput::TagSameItem(_) => todo!(),
RecipeInput::ListSameItem(_) => todo!(),
}
}
})
.collect(),
});
}
for (kind, mut primary_vec) in primaries.drain() {
for primary in primary_vec.drain(0..) {
for secondary in secondaries[&kind].iter() {
let input = vec![(primary.clone(), 1), (secondary.clone(), 1)];
let components = vec![primary.clone(), secondary.clone()];
let output = ItemDefinitionIdOwned::Modular {
pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
components,
};
ordered_recipes.push(RememberedRecipe {
output,
amount: 1,
material_cost: None,
input,
});
}
}
}
drop(secondaries);
let ability_map = &AbilityMap::load().read();
let msm = &MaterialStatManifest::load().read();
while result.sort_by_price(&mut ordered_recipes) {
ordered_recipes.retain(|recipe| {
if recipe.material_cost.map_or(false, |p| p < 1e-5) || recipe.amount == 0 {
false
} 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
.items
.0
.iter()
.find(|item| item.name == *input)
.map_or(false, |item| item.sell)
});
let stackable = Item::new_from_item_definition_id(
recipe.output.as_ref(),
ability_map,
msm,
)
.map_or(false, |i| i.is_stackable());
let new_entry = PriceEntry {
name: recipe.output.clone(),
price: usage * (1.0 / (recipe.amount as f32 * Self::CRAFTING_FACTOR)),
sell: output_tradeable,
stackable,
};
if PRICING_DEBUG {
tracing::trace!("Recipe {:?}", new_entry);
}
result.items.add_alternative(new_entry);
} else {
error!("Recipe {:?} incomplete confusion", recipe);
}
false
} else {
true
}
});
}
result
}
fn random_items_impl(
&self,
stockmap: &mut HashMap<Good, f32>,
mut number: u32,
selling: bool,
always_coin: bool,
limit: u32,
) -> Vec<(ItemDefinitionIdOwned, u32)> {
let mut candidates: Vec<&PriceEntry> = self
.items
.0
.iter()
.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 != ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()))
})
.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((
ItemDefinitionIdOwned::Simple(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];
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 {
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));
candidates.remove(index);
}
result
}
fn get_materials_impl(&self, item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
self.price_lookup(&item.to_owned()).cloned()
}
#[must_use]
pub fn random_items(
stock: &mut HashMap<Good, f32>,
number: u32,
selling: bool,
always_coin: bool,
limit: u32,
) -> Vec<(ItemDefinitionIdOwned, u32)> {
TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit)
}
#[must_use]
pub fn get_materials(item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
TRADE_PRICING.get_materials_impl(item)
}
#[cfg(test)]
fn instance() -> &'static Self { &TRADE_PRICING }
#[cfg(test)]
fn print_sorted(&self) {
use crate::comp::item::{armor, DurabilityMultiplier}; println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
fn more_information(i: &Item, p: f32) -> (String, &'static str) {
let msm = &MaterialStatManifest::load().read();
let durability_multiplier = DurabilityMultiplier(1.0);
if let ItemKind::Armor(a) = &*i.kind() {
(
match a.stats(msm, durability_multiplier).protection {
Some(armor::Protection::Invincible) => "Invincible".into(),
Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
None => "0.0".into(),
},
"prot/val",
)
} else if let ItemKind::Tool(t) = &*i.kind() {
let stats = t.stats(durability_multiplier);
(format!("{:.4}", stats.power * stats.speed * p), "dps/val")
} else if let ItemKind::Consumable { kind: _, effects } = &*i.kind() {
(
effects
.effects()
.iter()
.map(|e| {
if let crate::effect::Effect::Buff(b) = e {
format!("{:.2}", b.data.strength * p)
} else {
format!("{:?}", e)
}
})
.collect::<Vec<String>>()
.join(" "),
"str/val",
)
} else {
(Default::default(), "")
}
}
let mut sorted: Vec<(f32, &PriceEntry)> = self
.items
.0
.iter()
.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: _,
},
) in sorted.iter()
{
Item::new_from_item_definition_id(
item_id.as_ref(),
&AbilityMap::load().read(),
&MaterialStatManifest::load().read(),
)
.ok()
.map(|it| {
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,
);
});
}
}
}
#[must_use]
pub fn expand_loot_table(loot_table: &str) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
ProbabilityFile::from(vec![(1.0, LootSpec::LootTable(loot_table.into()))]).content
}
#[cfg(test)]
mod tests {
use crate::{comp::inventory::trade_pricing::TradePricing, trade::Good};
use tracing::{info, Level};
use tracing_subscriber::{filter::EnvFilter, FmtSubscriber};
fn init() {
FmtSubscriber::builder()
.with_max_level(Level::ERROR)
.with_env_filter(EnvFilter::from_default_env())
.try_init()
.unwrap_or(());
}
#[test]
fn test_prices1() {
init();
info!("init");
TradePricing::instance().print_sorted();
}
#[test]
fn test_prices2() {
init();
info!("init");
let mut stock: hashbrown::HashMap<Good, f32> = [
(Good::Ingredients, 50.0),
(Good::Tools, 10.0),
(Good::Armor, 10.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);
}
}
}