Merge branch 'christof/modular-weapon-prices' into 'master'

Implement material (de-)composition for modular weapons, add tusk+crest+pincer to price list

See merge request veloren/veloren!3469
This commit is contained in:
Christof Petig 2022-07-14 18:24:21 +00:00
commit 2dce472d1f
10 changed files with 537 additions and 124 deletions

View File

@ -80,6 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Most sfx now correctly play when camera is underwater
- All sounds now stop upon quitting to main menu
- Combat music now loops and ends properly
- Modular weapons now have a selling price
## [0.12.0] - 2022-02-19

View File

@ -30,8 +30,11 @@
(4.0, Item("common.items.crafting_ing.hide.animal_hide")),
// Mob Drops
(0.10, Item("common.items.crafting_ing.animal_misc.long_tusk")),
(0.15, Item("common.items.crafting_ing.animal_misc.elegant_crest")),
(0.15, Item("common.items.crafting_ing.animal_misc.grim_eyeball")),
(0.15, Item("common.items.crafting_ing.animal_misc.icy_fang")),
(0.2, Item("common.items.crafting_ing.animal_misc.strong_pincer")),
(0.5, Item("common.items.crafting_ing.animal_misc.raptor_feather")),
(1.2, Item("common.items.crafting_ing.animal_misc.claw")),
(2.5, Item("common.items.crafting_ing.animal_misc.fur")),

View File

@ -742,6 +742,42 @@ impl Item {
item
}
pub fn new_from_item_definition_id(
item_definition_id: ItemDefinitionId<'_>,
ability_map: &AbilityMap,
msm: &MaterialStatManifest,
) -> Result<Self, Error> {
let (base, components) = match item_definition_id {
ItemDefinitionId::Simple(spec) => {
let base = ItemBase::Simple(Arc::<ItemDef>::load_cloned(spec)?);
(base, Vec::new())
},
ItemDefinitionId::Modular {
pseudo_base,
components,
} => {
let base = ItemBase::Modular(ModularBase::load_from_pseudo_id(pseudo_base));
let components = components
.into_iter()
.map(|id| Item::new_from_item_definition_id(id, ability_map, msm))
.collect::<Result<Vec<_>, _>>()?;
(base, components)
},
ItemDefinitionId::Compound {
simple_base,
components,
} => {
let base = ItemBase::Simple(Arc::<ItemDef>::load_cloned(simple_base)?);
let components = components
.into_iter()
.map(|id| Item::new_from_item_definition_id(id, ability_map, msm))
.collect::<Result<Vec<_>, _>>()?;
(base, components)
},
};
Ok(Item::new_from_item_base(base, components, ability_map, msm))
}
/// Creates a new instance of an `Item` from the provided asset identifier
/// Panics if the asset does not exist.
pub fn new_from_asset_expect(asset_specifier: &str) -> Self {

View File

@ -1,8 +1,15 @@
use crate::{
assets::{self, AssetExt},
comp::item::Item,
comp::{
inventory,
item::{
Item, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, MaterialStatManifest,
ModularBase,
},
tool::AbilityMap,
},
lottery::LootSpec,
recipe::{default_recipe_book, RecipeInput},
recipe::{default_component_recipe_book, default_recipe_book, RecipeInput},
trade::Good,
};
use assets::AssetGuard;
@ -12,6 +19,8 @@ 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)]
@ -61,6 +70,23 @@ impl std::ops::Add for MaterialUse {
}
}
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)];
@ -120,18 +146,17 @@ impl std::ops::AddAssign for MaterialFrequency {
fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
}
#[derive(Default, Debug)]
#[derive(Debug)]
struct PriceEntry {
// item asset specifier
name: String,
name: ItemDefinitionIdOwned,
price: MaterialUse,
// sellable by merchants
sell: bool,
stackable: bool,
}
#[derive(Default, Debug)]
#[derive(Debug)]
struct FreqEntry {
name: String,
name: ItemDefinitionIdOwned,
freq: MaterialFrequency,
sell: bool,
stackable: bool,
@ -161,7 +186,7 @@ impl FreqEntries {
fn add(
&mut self,
eqset: &EqualitySet,
item_name: &str,
item_name: &ItemDefinitionIdOwned,
good: Good,
probability: f32,
can_sell: bool,
@ -181,15 +206,19 @@ impl FreqEntries {
}) = old
{
if PRICING_DEBUG {
info!("Update {} {:?}+{:?}", asset, old_probability, probability);
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 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,
@ -225,7 +254,7 @@ lazy_static! {
/// hierarchically from `LootSpec`s
/// (probability, item id, average amount)
pub struct ProbabilityFile {
pub content: Vec<(f32, String, f32)>,
pub content: Vec<(f32, ItemDefinitionIdOwned, f32)>,
}
impl assets::Asset for ProbabilityFile {
@ -234,8 +263,99 @@ impl assets::Asset for ProbabilityFile {
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();
// Load recipe book (done to check that material is valid for a particular component)
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; // once there exists a hand restriction add the logic here - for a slight price correction
let entry = component_pool.entry((*toolkind, String::from(material))).or_insert(Vec::new());
entry.push((component, hand_restriction));
});
component_pool
};
static ref SECONDARY_COMPONENT_POOL: ComponentPool = {
let mut component_pool = HashMap::new();
// Load recipe book (done to check that material is valid for a particular component)
//use crate::recipe::ComponentKey;
let recipes = default_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 = component_pool.entry((toolkind, String::new())).or_insert(Vec::new());
entry.push((component, hand_restriction));
}});
component_pool
};
}
// expand this loot specification towards individual item descriptions
// partial duplicate of random_weapon_primary_component
// returning an Iterator is difficult due to the branch and it is always used as
// a vec afterwards
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 {
#[allow(clippy::cast_precision_loss)]
fn from(content: Vec<(f32, LootSpec<String>)>) -> Self {
let rescale = if content.is_empty() {
1.0
@ -246,10 +366,14 @@ impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
content: content
.into_iter()
.flat_map(|(p0, loot)| match loot {
LootSpec::Item(asset) => vec![(p0 * rescale, asset, 1.0)].into_iter(),
LootSpec::ItemQuantity(asset, a, b) => {
vec![(p0 * rescale, asset, (a + b) as f32 * 0.5)].into_iter()
LootSpec::Item(asset) => {
vec![(p0 * rescale, ItemDefinitionIdOwned::Simple(asset), 1.0)]
},
LootSpec::ItemQuantity(asset, a, b) => vec![(
p0 * rescale,
ItemDefinitionIdOwned::Simple(asset),
(a + b) as f32 * 0.5,
)],
LootSpec::LootTable(table_asset) => {
let unscaled = &Self::load_expect(&table_asset).read().content;
let scale = p0 * rescale;
@ -257,11 +381,54 @@ impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
.iter()
.map(|(p1, asset, amount)| (*p1 * scale, asset.clone(), *amount))
.collect::<Vec<_>>()
.into_iter()
},
LootSpec::Nothing
// TODO: Let someone else wrangle modular weapons into the economy
| LootSpec::ModularWeapon { .. } | LootSpec::ModularWeaponPrimaryComponent { .. } => Vec::new().into_iter(),
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(),
})
.collect(),
}
@ -284,15 +451,15 @@ impl assets::Asset for TradingPriceFile {
#[derive(Clone, Debug, Default)]
struct EqualitySet {
// which item should this item's occurrences be counted towards
equivalence_class: HashMap<String, String>,
equivalence_class: HashMap<ItemDefinitionIdOwned, ItemDefinitionIdOwned>,
}
impl EqualitySet {
fn canonical<'a>(&'a self, item_name: &'a str) -> &'a str {
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);
.map_or(item_name, |i| &*i);
canonical_itemname
}
@ -312,22 +479,22 @@ impl assets::Compound for EqualitySet {
let manifest = &cache.load::<assets::Ron<Vec<EqualitySpec>>>(id)?.read().0;
for set in manifest {
let items = match set {
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.clone(),
EqualitySpec::Set(xs) => xs
.iter()
.map(|s| ItemDefinitionIdOwned::Simple(s.clone()))
.collect(),
};
let mut iter = items.iter();
if let Some(first) = iter.next() {
let first = first.to_string();
eqset.equivalence_class.insert(first.clone(), first.clone());
for item in iter {
eqset
.equivalence_class
.insert(item.to_string(), first.clone());
eqset.equivalence_class.insert(item.clone(), first.clone());
}
}
}
@ -337,10 +504,10 @@ impl assets::Compound for EqualitySet {
#[derive(Debug)]
struct RememberedRecipe {
output: String,
output: ItemDefinitionIdOwned,
amount: u32,
material_cost: Option<f32>,
input: Vec<(String, u32)>,
input: Vec<(ItemDefinitionIdOwned, u32)>,
}
fn get_scaling(contents: &AssetGuard<TradingPriceFile>, good: Good) -> f32 {
@ -351,49 +518,136 @@ fn get_scaling(contents: &AssetGuard<TradingPriceFile>, good: Good) -> f32 {
.map_or(1.0, |(_, scaling)| *scaling)
}
#[cfg(test)]
impl std::cmp::PartialOrd for ItemDefinitionIdOwned {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
#[cfg(test)]
impl std::cmp::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;
// increase price a bit compared to sum of ingredients
const INVEST_FACTOR: f32 = 0.33;
fn good_from_item(name: &str) -> Good {
fn good_from_item(name: &ItemDefinitionIdOwned) -> Good {
match name {
_ if name.starts_with("common.items.armor.") => Good::Armor,
ItemDefinitionIdOwned::Simple(name) 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,
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
},
_ 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.log.") => Good::Ingredients,
_ if name.starts_with("common.items.flowers.") => Good::Ingredients,
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
},
_ if name.starts_with("common.items.consumable.") => Good::Potions,
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.consumable.") => {
Good::Potions
},
_ if name.starts_with("common.items.food.") => Good::Food,
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.food.") => {
Good::Food
},
Self::COIN_ITEM => Good::Coin,
ItemDefinitionIdOwned::Simple(name) if name.as_str() == 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(),
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.glider.") => {
Good::default()
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.utility.") => {
Good::default()
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.boss_drops.") => {
Good::default()
},
ItemDefinitionIdOwned::Simple(name)
if name.starts_with("common.items.crafting_tools.") =>
{
Good::default()
},
ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.lantern.") => {
Good::default()
},
ItemDefinitionIdOwned::Modular {
pseudo_base: _,
components: _,
} => Good::Tools,
ItemDefinitionIdOwned::Compound {
simple_base: _,
components: _,
} => Good::Ingredients,
_ => {
warn!("unknown loot item {}", name);
warn!("unknown loot item {:?}", name);
Good::default()
},
}
}
// look up price (inverse frequency) of an item
fn price_lookup(&self, requested_name: &str) -> Option<&MaterialUse> {
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)
.find(|e| &e.name == canonical_name)
.map(|e| &e.price)
}
@ -453,7 +707,6 @@ impl TradePricing {
.is_some()
}
// #[allow(clippy::cast_precision_loss)]
fn read() -> Self {
let mut result = Self::default();
let mut freq = FreqEntries::default();
@ -482,7 +735,7 @@ impl TradePricing {
}
freq.add(
&result.equality_set,
Self::COIN_ITEM,
&ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
Good::Coin,
get_scaling(&price_config, Good::Coin),
true,
@ -492,7 +745,7 @@ impl TradePricing {
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);
let can_freq = freq.0.iter().find(|i| &i.name == canonical_name);
can_freq
.map(|e| PriceEntry {
name: elem.name.clone(),
@ -522,12 +775,26 @@ impl TradePricing {
}
// Apply recipe book
let mut secondaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
let book = default_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(
crate::comp::inventory::item::modular::ModularComponent::ToolSecondaryComponent {
toolkind,
stats: _,
hand_restriction: _,
},
) = asset_path.kind
{
secondaries
.entry(toolkind)
.or_insert(Vec::new())
.push(ItemDefinitionIdOwned::Simple(asset_path.id().into()));
}
ordered_recipes.push(RememberedRecipe {
output: asset_path.id().into(),
output: ItemDefinitionIdOwned::Simple(asset_path.id().into()),
amount,
material_cost: None,
input: recipe
@ -539,7 +806,7 @@ impl TradePricing {
if count == 0 {
None
} else {
Some((it.id().into(), count))
Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
}
} else {
None
@ -549,8 +816,63 @@ impl TradePricing {
});
}
// modular weapon recipes
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_insert(Vec::new())
.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(),
});
}
// drain the larger map while iterating the shorter
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);
// re-evaluate prices based on crafting tables
// (start with cheap ones to avoid changing material prices after evaluation)
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 {
@ -567,8 +889,12 @@ impl TradePricing {
.find(|item| item.name == *input)
.map_or(false, |item| item.sell)
});
let item = Item::new_from_asset_expect(&recipe.output);
let stackable = item.is_stackable();
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)),
@ -601,7 +927,7 @@ impl TradePricing {
selling: bool,
always_coin: bool,
limit: u32,
) -> Vec<(String, u32)> {
) -> Vec<(ItemDefinitionIdOwned, u32)> {
let mut candidates: Vec<&PriceEntry> = self
.items
.0
@ -613,14 +939,18 @@ impl TradePricing {
.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)
&& (!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((Self::COIN_ITEM.into(), amount));
result.push((
ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
amount,
));
number -= 1;
}
}
@ -663,7 +993,9 @@ impl TradePricing {
result
}
fn get_materials_impl(&self, item: &str) -> Option<&MaterialUse> { self.price_lookup(item) }
fn get_materials_impl(&self, item: &ItemDefinitionId) -> Option<MaterialUse> {
self.price_lookup(&item.to_owned()).cloned()
}
#[must_use]
pub fn random_items(
@ -672,12 +1004,12 @@ impl TradePricing {
selling: bool,
always_coin: bool,
limit: u32,
) -> Vec<(String, u32)> {
) -> Vec<(ItemDefinitionIdOwned, u32)> {
TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit)
}
#[must_use]
pub fn get_materials(item: &str) -> Option<&MaterialUse> {
pub fn get_materials(item: &ItemDefinitionId) -> Option<MaterialUse> {
TRADE_PRICING.get_materials_impl(item)
}
@ -686,7 +1018,7 @@ impl TradePricing {
#[cfg(test)]
fn print_sorted(&self) {
use crate::comp::item::{armor, ItemKind, MaterialStatManifest};
use crate::comp::item::armor; //, ItemKind, MaterialStatManifest};
println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
@ -748,30 +1080,37 @@ impl TradePricing {
},
) 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,
);
Item::new_from_item_definition_id(
item_id.as_ref(),
&AbilityMap::load().read(),
&MaterialStatManifest::load().read(),
)
.ok()
.map(|it| {
//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,
);
});
}
}
}
/// hierarchically combine and scale this loot table
#[must_use]
pub fn expand_loot_table(loot_table: &str) -> Vec<(f32, String, f32)> {
pub fn expand_loot_table(loot_table: &str) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
ProbabilityFile::from(vec![(1.0, LootSpec::LootTable(loot_table.into()))]).content
}
@ -800,9 +1139,6 @@ mod tests {
init();
info!("init");
// Note: This test breaks when the loot table contains `Nothing` as a potential
// drop.
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);
@ -812,13 +1148,16 @@ mod tests {
assert!((lootsum2 - 1.0).abs() < 1e-4);
// highly nested
// TODO: Re-enable this. See note at top of test (though this specific
// table can also be fixed by properly integrating modular weapons into
// probability files)
// 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);
let loot3 = expand_loot_table("common.loot_tables.creature.biped_large.wendigo");
let lootsum3 = loot3.iter().fold(0.0, |s, i| s + i.0);
//tracing::trace!("{:?} {}", loot3, lootsum3);
assert!((lootsum3 - 1.0).abs() < 1e-5);
// includes tier-5 modular weapons
let loot4 = expand_loot_table("common.loot_tables.dungeon.tier-4.boss");
let lootsum4 = loot4.iter().fold(0.0, |s, i| s + i.0);
//tracing::trace!("{:?} {}", loot4, lootsum4);
assert!((lootsum4 - 1.0).abs() < 1e-5);
}
#[test]
@ -846,7 +1185,7 @@ mod tests {
let loadout = TradePricing::random_items(&mut stock, 20, false, false, 999);
for i in loadout.iter() {
info!("Random item {}*{}", i.0, i.1);
info!("Random item {:?}*{}", i.0, i.1);
}
}

View File

@ -5,7 +5,7 @@ use crate::{
item::{
modular,
tool::{AbilityMap, ToolKind},
ItemBase, ItemDef, ItemKind, ItemTag, MaterialStatManifest,
ItemBase, ItemDef, ItemDefinitionIdOwned, ItemKind, ItemTag, MaterialStatManifest,
},
Inventory, Item,
},
@ -472,6 +472,11 @@ pub struct ComponentRecipeBook {
recipes: HashMap<ComponentKey, ComponentRecipe>,
}
#[derive(Clone, Debug)]
pub struct ReverseComponentRecipeBook {
recipes: HashMap<ItemDefinitionIdOwned, ComponentRecipe>,
}
impl ComponentRecipeBook {
pub fn get(&self, key: &ComponentKey) -> Option<&ComponentRecipe> { self.recipes.get(key) }
@ -480,6 +485,12 @@ impl ComponentRecipeBook {
}
}
impl ReverseComponentRecipeBook {
pub fn get(&self, key: &ItemDefinitionIdOwned) -> Option<&ComponentRecipe> {
self.recipes.get(key)
}
}
#[derive(Clone, Deserialize)]
#[serde(transparent)]
struct RawComponentRecipeBook(Vec<RawComponentRecipe>);
@ -646,6 +657,24 @@ impl ComponentRecipe {
)
}
pub fn itemdef_output(&self) -> ItemDefinitionIdOwned {
match &self.output {
ComponentOutput::ItemComponents {
item: item_def,
components,
} => {
let components = components
.iter()
.map(|item_def| ItemDefinitionIdOwned::Simple(item_def.id().to_owned()))
.collect::<Vec<_>>();
ItemDefinitionIdOwned::Compound {
simple_base: item_def.id().to_owned(),
components,
}
},
}
}
pub fn item_output(&self, ability_map: &AbilityMap, msm: &MaterialStatManifest) -> Item {
match &self.output {
ComponentOutput::ItemComponents {
@ -818,3 +847,14 @@ pub fn default_recipe_book() -> AssetHandle<RecipeBook> {
pub fn default_component_recipe_book() -> AssetHandle<ComponentRecipeBook> {
ComponentRecipeBook::load_expect("common.component_recipe_book")
}
impl assets::Compound for ReverseComponentRecipeBook {
fn load(cache: assets::AnyCache, specifier: &str) -> Result<Self, assets::BoxedError> {
let forward = cache.load::<ComponentRecipeBook>(specifier)?.cloned();
let mut recipes = HashMap::new();
for (_, recipe) in forward.iter() {
recipes.insert(recipe.itemdef_output(), recipe.clone());
}
Ok(ReverseComponentRecipeBook { recipes })
}
}

View File

@ -389,13 +389,7 @@ impl SitePrices {
.as_ref()
.and_then(|ri| {
ri.inventory.get(slot).map(|item| {
if let Some(vec) = item
.name
.as_ref()
// TODO: This won't handle compound items with components well, or pure modular items at all
.itemdef_id()
.and_then(TradePricing::get_materials)
{
if let Some(vec) = TradePricing::get_materials(&item.name.as_ref()) {
vec.iter()
.map(|(amount2, material)| {
self.values.get(material).copied().unwrap_or_default()

View File

@ -3644,10 +3644,8 @@ impl Hud {
false,
);
if let Some(item) = inventory.get(slot) {
if let Some(materials) = item
.item_definition_id()
.itemdef_id()
.and_then(TradePricing::get_materials)
if let Some(materials) =
TradePricing::get_materials(&item.item_definition_id())
{
let unit_price: f32 = materials
.iter()

View File

@ -5,7 +5,7 @@ use common::{
item::{
armor::{Armor, ArmorKind, Protection},
tool::{Hands, Tool, ToolKind},
ItemDesc, ItemKind, MaterialKind, MaterialStatManifest,
ItemDefinitionId, ItemDesc, ItemKind, MaterialKind, MaterialStatManifest,
},
BuffKind,
},
@ -18,11 +18,11 @@ use std::{borrow::Cow, fmt::Write};
pub fn price_desc(
prices: &Option<SitePrices>,
item_definition_id: &str,
item_definition_id: ItemDefinitionId<'_>,
i18n: &Localization,
) -> Option<(String, String, f32)> {
if let Some(prices) = prices {
if let Some(materials) = TradePricing::get_materials(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: f32 = materials
.iter()

View File

@ -1357,10 +1357,8 @@ impl<'a> Widget for ItemTooltip<'a> {
}
// Price display
if let Some((buy, sell, factor)) = item
.item_definition_id()
.itemdef_id()
.and_then(|id| util::price_desc(self.prices, id, i18n))
if let Some((buy, sell, factor)) =
util::price_desc(self.prices, item.item_definition_id(), i18n)
{
widget::Text::new(&buy)
.x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
@ -1395,7 +1393,7 @@ impl<'a> Widget for ItemTooltip<'a> {
widget::Text::new(&format!(
"{}\n{}",
i18n.get("hud.trade.tooltip_hint_1"),
i18n.get("hud.trade.tooltip_hint_2")
i18n.get("hud.trade.tooltip_hint_2"),
))
.x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
.graphics_for(id)
@ -1457,11 +1455,11 @@ impl<'a> Widget for ItemTooltip<'a> {
};
// Price
let price_h: f64 = if let Some((buy, sell, _)) = item
.item_definition_id()
.itemdef_id()
.and_then(|id| util::price_desc(self.prices, id, self.localized_strings))
{
let price_h: f64 = if let Some((buy, sell, _)) = util::price_desc(
self.prices,
item.item_definition_id(),
self.localized_strings,
) {
//Get localized tooltip strings (gotten here because these should only show if
// in a trade- aka if buy/sell prices are present)
let tt_hint_1 = self.localized_strings.get("hud.trade.tooltip_hint_1");

View File

@ -1050,15 +1050,19 @@ pub fn merchant_loadout(
.filter(|(good, _amount)| **good != Good::Coin)
.for_each(|(_good, amount)| *amount *= 0.1);
// Fill bags with stuff according to unclaimed stock
let ability_map = &comp::tool::AbilityMap::load().read();
let msm = &comp::item::MaterialStatManifest::load().read();
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
.filter_map(|(n, a)| {
let i = Item::new_from_item_definition_id(n.as_ref(), ability_map, msm).ok();
i.map(|mut i| {
i.set_amount(*a)
.map_err(|_| tracing::error!("merchant loadout amount failure"))
.ok();
i
})
})
.collect();
sort_wares(&mut wares);