Skip to main content

veloren_common/comp/inventory/
trade_pricing.rs

1use crate::{
2    assets::{
3        Asset, AssetCache, AssetExt, AssetReadGuard, BoxedError, FileAsset, Ron, SharedString,
4        load_ron,
5    },
6    comp::{
7        inventory,
8        item::{
9            Item, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, MaterialStatManifest,
10            ModularBase,
11        },
12        tool::AbilityMap,
13    },
14    lottery::LootSpec,
15    recipe::{RecipeInput, complete_recipe_book, default_component_recipe_book},
16    trade::Good,
17};
18use hashbrown::HashMap;
19use lazy_static::lazy_static;
20use serde::Deserialize;
21use std::{borrow::Cow, cmp::Ordering};
22use tracing::{error, info, warn};
23
24use super::item::{Material, ToolKind};
25
26const PRICING_DEBUG: bool = false;
27
28#[derive(Default, Debug)]
29pub struct TradePricing {
30    items: PriceEntries,
31    equality_set: EqualitySet,
32}
33
34// combination logic:
35// price is the inverse of frequency
36// you can use either equivalent A or B => add frequency
37// you need both equivalent A and B => add price
38
39/// Material equivalent for an item (price)
40#[derive(Default, Debug, Clone, PartialEq, PartialOrd)]
41pub struct MaterialUse(Vec<(f32, Good)>);
42
43impl std::ops::Mul<f32> for MaterialUse {
44    type Output = Self;
45
46    fn mul(self, rhs: f32) -> Self::Output {
47        Self(self.0.iter().map(|v| (v.0 * rhs, v.1)).collect())
48    }
49}
50
51// used by the add variants
52fn vector_add_eq(result: &mut Vec<(f32, Good)>, rhs: &[(f32, Good)]) {
53    for (amount, good) in rhs {
54        if result
55            .iter_mut()
56            .find(|(_amount2, good2)| *good == *good2)
57            .map(|elem| elem.0 += *amount)
58            .is_none()
59        {
60            result.push((*amount, *good));
61        }
62    }
63}
64
65impl std::ops::Add for MaterialUse {
66    type Output = Self;
67
68    fn add(self, rhs: Self) -> Self::Output {
69        let mut result = self;
70        vector_add_eq(&mut result.0, &rhs.0);
71        result
72    }
73}
74
75impl std::ops::AddAssign for MaterialUse {
76    fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
77}
78
79impl std::iter::Sum<MaterialUse> for MaterialUse {
80    fn sum<I>(iter: I) -> Self
81    where
82        I: Iterator<Item = Self>,
83    {
84        let mut ret = Self::default();
85        for i in iter {
86            ret += i;
87        }
88        ret
89    }
90}
91
92impl std::ops::Deref for MaterialUse {
93    type Target = [(f32, Good)];
94
95    fn deref(&self) -> &Self::Target { self.0.deref() }
96}
97
98/// Frequency
99#[derive(Default, Debug, Clone)]
100pub struct MaterialFrequency(Vec<(f32, Good)>);
101
102// to compute price from frequency:
103// price[i] = 1/frequency[i] * 1/sum(frequency) * 1/sum(1/frequency)
104// scaling individual components so that ratio is inverted and the sum of all
105// inverted elements is equivalent to inverse of the original sum
106fn vector_invert(result: &mut [(f32, Good)]) {
107    let mut oldsum: f32 = 0.0;
108    let mut newsum: f32 = 0.0;
109    for (value, _good) in result.iter_mut() {
110        oldsum += *value;
111        *value = 1.0 / *value;
112        newsum += *value;
113    }
114    let scale = 1.0 / (oldsum * newsum);
115    for (value, _good) in result.iter_mut() {
116        *value *= scale;
117    }
118}
119
120impl From<MaterialUse> for MaterialFrequency {
121    fn from(u: MaterialUse) -> Self {
122        let mut result = Self(u.0);
123        vector_invert(&mut result.0);
124        result
125    }
126}
127
128// identical computation
129impl From<MaterialFrequency> for MaterialUse {
130    fn from(u: MaterialFrequency) -> Self {
131        let mut result = Self(u.0);
132        vector_invert(&mut result.0);
133        result
134    }
135}
136
137impl std::ops::Add for MaterialFrequency {
138    type Output = Self;
139
140    fn add(self, rhs: Self) -> Self::Output {
141        let mut result = self;
142        vector_add_eq(&mut result.0, &rhs.0);
143        result
144    }
145}
146
147impl std::ops::AddAssign for MaterialFrequency {
148    fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
149}
150
151#[derive(Debug)]
152struct PriceEntry {
153    name: ItemDefinitionIdOwned,
154    price: MaterialUse,
155    // sellable by merchants
156    sell: bool,
157    stackable: bool,
158}
159#[derive(Debug)]
160struct FreqEntry {
161    name: ItemDefinitionIdOwned,
162    freq: MaterialFrequency,
163    sell: bool,
164    stackable: bool,
165}
166
167#[derive(Default, Debug)]
168struct PriceEntries(Vec<PriceEntry>);
169#[derive(Default, Debug)]
170struct FreqEntries(Vec<FreqEntry>);
171
172impl PriceEntries {
173    fn add_alternative(&mut self, b: PriceEntry) {
174        // alternatives are added in frequency (gets more frequent)
175        let already = self.0.iter_mut().find(|i| i.name == b.name);
176        if let Some(entry) = already {
177            let entry_freq: MaterialFrequency = std::mem::take(&mut entry.price).into();
178            let b_freq: MaterialFrequency = b.price.into();
179            let result = entry_freq + b_freq;
180            entry.price = result.into();
181        } else {
182            self.0.push(b);
183        }
184    }
185}
186
187impl FreqEntries {
188    fn add(
189        &mut self,
190        eqset: &EqualitySet,
191        item_name: &ItemDefinitionIdOwned,
192        good: Good,
193        probability: f32,
194        can_sell: bool,
195    ) {
196        let canonical_itemname = eqset.canonical(item_name);
197        let old = self
198            .0
199            .iter_mut()
200            .find(|elem| elem.name == *canonical_itemname);
201        let new_freq = MaterialFrequency(vec![(probability, good)]);
202        // Increase probability if already in entries, or add new entry
203        if let Some(FreqEntry {
204            name: asset,
205            freq: old_probability,
206            sell: old_can_sell,
207            stackable: _,
208        }) = old
209        {
210            if PRICING_DEBUG {
211                info!("Update {:?} {:?}+{:?}", asset, old_probability, probability);
212            }
213            if !can_sell && *old_can_sell {
214                *old_can_sell = false;
215            }
216            *old_probability += new_freq;
217        } else {
218            let stackable = Item::new_from_item_definition_id(
219                canonical_itemname.as_ref(),
220                &AbilityMap::load().read(),
221                &MaterialStatManifest::load().read(),
222            )
223            .is_ok_and(|i| i.is_stackable());
224            let new_mat_prob: FreqEntry = FreqEntry {
225                name: canonical_itemname.to_owned(),
226                freq: new_freq,
227                sell: can_sell,
228                stackable,
229            };
230            if PRICING_DEBUG {
231                info!("New {:?}", new_mat_prob);
232            }
233            self.0.push(new_mat_prob);
234        }
235
236        // Add the non-canonical item so that it'll show up in merchant inventories
237        // It will have infinity as its price, but it's fine,
238        // because we determine all prices based on canonical value
239        if canonical_itemname != item_name && !self.0.iter().any(|elem| elem.name == *item_name) {
240            self.0.push(FreqEntry {
241                name: item_name.to_owned(),
242                freq: Default::default(),
243                sell: can_sell,
244                stackable: false,
245            });
246        }
247    }
248}
249
250lazy_static! {
251    static ref TRADE_PRICING: TradePricing = TradePricing::read();
252}
253
254#[derive(Clone)]
255/// A collection of items with probabilty, created
256/// hierarchically from `LootSpec`s
257/// (probability, item id, average amount)
258///
259/// This collection is NOT normalized (the sum of probabilities may not equal to
260/// one, as maltiple items can drop in one roll)
261pub struct ProbabilityFile {
262    pub content: Vec<(f32, ItemDefinitionIdOwned, f32)>,
263}
264
265impl FileAsset for ProbabilityFile {
266    const EXTENSION: &'static str = "ron";
267
268    fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> {
269        load_ron::<Vec<(f32, LootSpec<String>)>>(&bytes).map(Vec::into)
270    }
271}
272
273type ComponentPool =
274    HashMap<(ToolKind, String), Vec<(ItemDefinitionIdOwned, Option<inventory::item::Hands>)>>;
275
276lazy_static! {
277    static ref PRIMARY_COMPONENT_POOL: ComponentPool = {
278        let mut component_pool = HashMap::new();
279
280        // Load recipe book (done to check that material is valid for a particular component)
281        use crate::recipe::ComponentKey;
282        let recipes = default_component_recipe_book().read();
283
284        recipes
285            .iter()
286            .for_each(|(ComponentKey { toolkind, material, .. }, recipe)| {
287                let component = recipe.itemdef_output();
288                let hand_restriction = None; // once there exists a hand restriction add the logic here - for a slight price correction
289                let entry: &mut Vec<_> = component_pool.entry((*toolkind, String::from(material))).or_default();
290                entry.push((component, hand_restriction));
291            });
292
293        component_pool
294    };
295
296    static ref SECONDARY_COMPONENT_POOL: ComponentPool = {
297        let mut component_pool = HashMap::new();
298
299        // Load recipe book (done to check that material is valid for a particular component)
300        //use crate::recipe::ComponentKey;
301        let recipes = complete_recipe_book().read();
302
303        recipes
304            .iter()
305            .for_each(|(_, recipe)| {
306                let (ref asset_path, _) = recipe.output;
307                if let ItemKind::ModularComponent(
308                    crate::comp::inventory::item::modular::ModularComponent::ToolSecondaryComponent {
309                        toolkind,
310                        stats: _,
311                        hand_restriction,
312                    },
313                ) = asset_path.kind
314                {
315                    let component = ItemDefinitionIdOwned::Simple(asset_path.id().into());
316                    let entry: &mut Vec<_> = component_pool.entry((toolkind, String::new())).or_default();
317                    entry.push((component, hand_restriction));
318                }});
319
320        component_pool
321    };
322}
323
324// expand this loot specification towards individual item descriptions
325// partial duplicate of random_weapon_primary_component
326// returning an Iterator is difficult due to the branch and it is always used as
327// a vec afterwards
328pub fn expand_primary_component(
329    tool: ToolKind,
330    material: Material,
331    hand_restriction: Option<inventory::item::Hands>,
332) -> Vec<ItemDefinitionIdOwned> {
333    if let Some(material_id) = material.asset_identifier() {
334        PRIMARY_COMPONENT_POOL
335            .get(&(tool, material_id.to_owned()))
336            .into_iter()
337            .flatten()
338            .filter(move |(_comp, hand)| match (hand_restriction, *hand) {
339                (Some(restriction), Some(hand)) => restriction == hand,
340                (None, _) | (_, None) => true,
341            })
342            .map(|e| e.0.clone())
343            .collect()
344    } else {
345        Vec::new()
346    }
347}
348
349pub fn expand_secondary_component(
350    tool: ToolKind,
351    _material: Material,
352    hand_restriction: Option<inventory::item::Hands>,
353) -> impl Iterator<Item = ItemDefinitionIdOwned> {
354    SECONDARY_COMPONENT_POOL
355        .get(&(tool, String::new()))
356        .into_iter()
357        .flatten()
358        .filter(move |(_comp, hand)| match (hand_restriction, *hand) {
359            (Some(restriction), Some(hand)) => restriction == hand,
360            (None, _) | (_, None) => true,
361        })
362        .map(|e| e.0.clone())
363}
364
365impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
366    fn from(content: Vec<(f32, LootSpec<String>)>) -> Self {
367        let rescale = if content.is_empty() {
368            1.0
369        } else {
370            1.0 / content.iter().map(|e| e.0).sum::<f32>()
371        };
372        fn get_content(
373            rescale: f32,
374            p0: f32,
375            loot: LootSpec<String>,
376        ) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
377            match loot {
378                LootSpec::Item(asset) => {
379                    vec![(p0 * rescale, ItemDefinitionIdOwned::Simple(asset), 1.0)]
380                },
381                LootSpec::LootTable(table_asset) => {
382                    let unscaled = &ProbabilityFile::load_expect(&table_asset).read().content;
383                    let scale = p0 * rescale;
384                    unscaled
385                        .iter()
386                        .map(|(p1, asset, amount)| (*p1 * scale, asset.clone(), *amount))
387                        .collect::<Vec<_>>()
388                },
389                LootSpec::Lottery(table) => {
390                    let unscaled = ProbabilityFile::from(table);
391                    let scale = p0 * rescale;
392                    unscaled
393                        .content
394                        .into_iter()
395                        .map(|(p1, asset, amount)| (p1 * scale, asset, amount))
396                        .collect::<Vec<_>>()
397                },
398                LootSpec::ModularWeapon {
399                    tool,
400                    material,
401                    hands,
402                } => {
403                    let mut primary = expand_primary_component(tool, material, hands);
404                    let secondary: Vec<ItemDefinitionIdOwned> =
405                        expand_secondary_component(tool, material, hands).collect();
406                    let freq = if primary.is_empty() || secondary.is_empty() {
407                        0.0
408                    } else {
409                        p0 * rescale / ((primary.len() * secondary.len()) as f32)
410                    };
411                    let res: Vec<(f32, ItemDefinitionIdOwned, f32)> = primary
412                        .drain(0..)
413                        .flat_map(|p| {
414                            secondary.iter().map(move |s| {
415                                let components = vec![p.clone(), s.clone()];
416                                (
417                                    freq,
418                                    ItemDefinitionIdOwned::Modular {
419                                        pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
420                                        components,
421                                    },
422                                    1.0f32,
423                                )
424                            })
425                        })
426                        .collect();
427                    res
428                },
429                LootSpec::ModularWeaponPrimaryComponent {
430                    tool,
431                    material,
432                    hands,
433                } => {
434                    let mut res = expand_primary_component(tool, material, hands);
435                    let freq = if res.is_empty() {
436                        0.0
437                    } else {
438                        p0 * rescale / (res.len() as f32)
439                    };
440                    let res: Vec<(f32, ItemDefinitionIdOwned, f32)> =
441                        res.drain(0..).map(|e| (freq, e, 1.0f32)).collect();
442                    res
443                },
444                LootSpec::Nothing => Vec::new(),
445                LootSpec::MultiDrop(loot, a, b) => {
446                    let average_count = (a + b) as f32 * 0.5;
447                    let mut content = get_content(rescale, p0, *loot);
448                    for (_, _, count) in content.iter_mut() {
449                        *count *= average_count;
450                    }
451                    content
452                },
453                LootSpec::All(loot_specs) => loot_specs
454                    .into_iter()
455                    .flat_map(|loot| get_content(rescale, p0, loot))
456                    .collect(),
457            }
458        }
459        Self {
460            content: content
461                .into_iter()
462                .flat_map(|(p0, loot)| get_content(rescale, p0, loot))
463                .collect(),
464        }
465    }
466}
467
468#[derive(Debug, Deserialize)]
469struct TradingPriceFile {
470    /// Tuple format: (frequency, can_sell, asset_path)
471    pub loot_tables: Vec<(f32, bool, String)>,
472    /// the amount of Good equivalent to the most common item
473    pub good_scaling: Vec<(Good, f32)>,
474}
475
476#[derive(Clone, Debug, Default)]
477struct EqualitySet {
478    // which item should this item's occurrences be counted towards
479    equivalence_class: HashMap<ItemDefinitionIdOwned, ItemDefinitionIdOwned>,
480}
481
482impl EqualitySet {
483    fn canonical<'a>(&'a self, item_name: &'a ItemDefinitionIdOwned) -> &'a ItemDefinitionIdOwned {
484        // TODO: use hashbrown Equivalent trait to avoid needing owned item def here
485
486        (self
487            .equivalence_class
488            .get(item_name)
489            .map_or(item_name, |i| i)) as _
490    }
491}
492
493impl Asset for EqualitySet {
494    fn load(cache: &AssetCache, id: &SharedString) -> Result<Self, BoxedError> {
495        #[derive(Debug, Deserialize)]
496        enum EqualitySpec {
497            LootTable(String),
498            Set(Vec<String>),
499        }
500
501        let mut eqset = Self {
502            equivalence_class: HashMap::new(),
503        };
504
505        let manifest = &cache.load::<Ron<Vec<EqualitySpec>>>(id)?.read().0;
506        for set in manifest {
507            let items: Vec<ItemDefinitionIdOwned> = match set {
508                EqualitySpec::LootTable(table) => {
509                    let acc = &ProbabilityFile::load_expect(table).read().content;
510
511                    acc.iter().map(|(_p, item, _)| item).cloned().collect()
512                },
513                EqualitySpec::Set(xs) => xs
514                    .iter()
515                    .map(|s| ItemDefinitionIdOwned::Simple(s.clone()))
516                    .collect(),
517            };
518            let mut iter = items.iter();
519            if let Some(first) = iter.next() {
520                eqset.equivalence_class.insert(first.clone(), first.clone());
521                for item in iter {
522                    eqset.equivalence_class.insert(item.clone(), first.clone());
523                }
524            }
525        }
526        Ok(eqset)
527    }
528}
529
530#[derive(Debug)]
531struct RememberedRecipe {
532    output: ItemDefinitionIdOwned,
533    amount: u32,
534    material_cost: Option<f32>,
535    input: Vec<(ItemDefinitionIdOwned, u32)>,
536}
537
538fn get_scaling(contents: &AssetReadGuard<Ron<TradingPriceFile>>, good: Good) -> f32 {
539    contents
540        .0
541        .good_scaling
542        .iter()
543        .find(|(good_kind, _)| *good_kind == good)
544        .map_or(1.0, |(_, scaling)| *scaling)
545}
546
547#[cfg(test)]
548impl PartialOrd for ItemDefinitionIdOwned {
549    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
550}
551
552#[cfg(test)]
553impl Ord for ItemDefinitionIdOwned {
554    fn cmp(&self, other: &Self) -> Ordering {
555        match self {
556            ItemDefinitionIdOwned::Simple(na) => match other {
557                ItemDefinitionIdOwned::Simple(nb) => na.cmp(nb),
558                _ => Ordering::Less,
559            },
560            ItemDefinitionIdOwned::Modular {
561                pseudo_base,
562                components,
563            } => match other {
564                ItemDefinitionIdOwned::Simple(_) => Ordering::Greater,
565                ItemDefinitionIdOwned::Modular {
566                    pseudo_base: pseudo_base2,
567                    components: components2,
568                } => pseudo_base
569                    .cmp(pseudo_base2)
570                    .then_with(|| components.cmp(components2)),
571                _ => Ordering::Less,
572            },
573            ItemDefinitionIdOwned::Compound {
574                simple_base,
575                components,
576            } => match other {
577                ItemDefinitionIdOwned::Compound {
578                    simple_base: simple_base2,
579                    components: components2,
580                } => simple_base
581                    .cmp(simple_base2)
582                    .then_with(|| components.cmp(components2)),
583                _ => Ordering::Greater,
584            },
585        }
586    }
587}
588
589impl TradePricing {
590    const COIN_ITEM: &'static str = "common.items.utility.coins";
591    const CRAFTING_FACTOR: f32 = 0.95;
592    // increase price a bit compared to sum of ingredients
593    const INVEST_FACTOR: f32 = 0.33;
594
595    pub fn good_from_item(name: &ItemDefinitionIdOwned) -> Good {
596        Self::good_from_itemdef_id(name.as_ref())
597    }
598
599    pub fn good_from_itemdef_id(name: ItemDefinitionId) -> Good {
600        match name {
601            ItemDefinitionId::Simple(name) if name.starts_with("common.items.armor.") => {
602                Good::Armor
603            },
604
605            ItemDefinitionId::Simple(name) if name.starts_with("common.items.weapons.") => {
606                Good::Tools
607            },
608            ItemDefinitionId::Simple(name) if name.starts_with("common.items.modular.weapon.") => {
609                Good::Tools
610            },
611            ItemDefinitionId::Simple(name) if name.starts_with("common.items.tool.") => Good::Tools,
612            ItemDefinitionId::Simple(name) if name.starts_with("common.items.crafting_ing.") => {
613                Good::Ingredients
614            },
615            ItemDefinitionId::Simple(name) if name.starts_with("common.items.mineral.") => {
616                Good::Ingredients
617            },
618            ItemDefinitionId::Simple(name) if name.starts_with("common.items.log.") => Good::Wood,
619            ItemDefinitionId::Simple(name) if name.starts_with("common.items.flowers.") => {
620                Good::Ingredients
621            },
622            ItemDefinitionId::Simple(name) if name.starts_with("common.items.consumable.") => {
623                Good::Potions
624            },
625            ItemDefinitionId::Simple(name) if name.starts_with("common.items.charms.") => {
626                Good::Potions
627            },
628            ItemDefinitionId::Simple(name) if name.starts_with("common.items.food.") => Good::Food,
629            ItemDefinitionId::Simple(name) if name == Self::COIN_ITEM => Good::Coin,
630            ItemDefinitionId::Simple(name) if name.starts_with("common.items.recipes.") => {
631                Good::Recipe
632            },
633            ItemDefinitionId::Simple(name) if name.starts_with("common.items.glider.") => {
634                Good::Tools
635            },
636            ItemDefinitionId::Simple(name) if name.starts_with("common.items.utility.") => {
637                Good::default()
638            },
639            ItemDefinitionId::Simple(name) if name.starts_with("common.items.boss_drops.") => {
640                Good::Tools
641            },
642            ItemDefinitionId::Simple(name) if name.starts_with("common.items.crafting_tools.") => {
643                Good::default()
644            },
645            ItemDefinitionId::Simple(name) if name.starts_with("common.items.lantern.") => {
646                Good::Tools
647            },
648            ItemDefinitionId::Simple(name) if name.starts_with("common.items.keys.") => Good::Tools,
649            ItemDefinitionId::Modular {
650                pseudo_base: _,
651                components: _,
652            } => Good::Tools,
653            ItemDefinitionId::Compound {
654                simple_base: _,
655                components: _,
656            } => Good::Tools,
657            _ => {
658                warn!("unknown loot item {:?}", name);
659                Good::default()
660            },
661        }
662    }
663
664    // look up price (inverse frequency) of an item
665    fn price_lookup(&self, requested_name: &ItemDefinitionIdOwned) -> Option<&MaterialUse> {
666        let canonical_name = self.equality_set.canonical(requested_name);
667        self.items
668            .0
669            .iter()
670            .find(|e| &e.name == canonical_name)
671            .map(|e| &e.price)
672    }
673
674    fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
675        r.input
676            .iter()
677            .map(|(name, amount)| {
678                self.price_lookup(name).map(|x| {
679                    x.clone()
680                        * (if *amount > 0 {
681                            *amount as f32
682                        } else {
683                            Self::INVEST_FACTOR
684                        })
685                })
686            })
687            .try_fold(MaterialUse::default(), |acc, elem| Some(acc + elem?))
688    }
689
690    fn calculate_material_cost_sum(&self, r: &RememberedRecipe) -> Option<f32> {
691        self.calculate_material_cost(r)?
692            .iter()
693            .fold(None, |acc, elem| Some(acc.unwrap_or_default() + elem.0))
694    }
695
696    // re-look up prices and sort the vector by ascending material cost, return
697    // whether first cost is finite
698    fn sort_by_price(&self, recipes: &mut [RememberedRecipe]) -> bool {
699        for recipe in recipes.iter_mut() {
700            recipe.material_cost = self.calculate_material_cost_sum(recipe);
701        }
702        // put None to the end
703        recipes.sort_by(|a, b| {
704            if a.material_cost.is_some() {
705                if b.material_cost.is_some() {
706                    a.material_cost
707                        .partial_cmp(&b.material_cost)
708                        .unwrap_or(Ordering::Equal)
709                } else {
710                    Ordering::Less
711                }
712            } else if b.material_cost.is_some() {
713                Ordering::Greater
714            } else {
715                Ordering::Equal
716            }
717        });
718        if PRICING_DEBUG {
719            for i in recipes.iter() {
720                tracing::debug!("{:?}", *i);
721            }
722        }
723        //info!(? recipes);
724        recipes
725            .first()
726            .filter(|recipe| recipe.material_cost.is_some())
727            .is_some()
728    }
729
730    fn read() -> Self {
731        let mut result = Self::default();
732        let mut freq = FreqEntries::default();
733        let price_config =
734            Ron::<TradingPriceFile>::load_expect("common.trading.item_price_calculation").read();
735        result.equality_set = EqualitySet::load_expect("common.trading.item_price_equality")
736            .read()
737            .clone();
738        for table in &price_config.0.loot_tables {
739            if PRICING_DEBUG {
740                info!(?table);
741            }
742            let (frequency, can_sell, asset_path) = table;
743            let loot = ProbabilityFile::load_expect(asset_path);
744            for (p, item_asset, amount) in &loot.read().content {
745                let good = Self::good_from_item(item_asset);
746                let scaling = get_scaling(&price_config, good);
747                freq.add(
748                    &result.equality_set,
749                    item_asset,
750                    good,
751                    frequency * p * *amount * scaling,
752                    *can_sell,
753                );
754            }
755        }
756        freq.add(
757            &result.equality_set,
758            &ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
759            Good::Coin,
760            get_scaling(&price_config, Good::Coin),
761            true,
762        );
763        // convert frequency to price
764        result.items.0.extend(freq.0.iter().map(|elem| {
765            if elem.freq.0.is_empty() {
766                // likely equality
767                let canonical_name = result.equality_set.canonical(&elem.name);
768                let can_freq = freq.0.iter().find(|i| &i.name == canonical_name);
769                can_freq
770                    .map(|e| PriceEntry {
771                        name: elem.name.clone(),
772                        price: MaterialUse::from(e.freq.clone()),
773                        sell: elem.sell && e.sell,
774                        stackable: elem.stackable,
775                    })
776                    .unwrap_or(PriceEntry {
777                        name: elem.name.clone(),
778                        price: MaterialUse::from(elem.freq.clone()),
779                        sell: elem.sell,
780                        stackable: elem.stackable,
781                    })
782            } else {
783                PriceEntry {
784                    name: elem.name.clone(),
785                    price: MaterialUse::from(elem.freq.clone()),
786                    sell: elem.sell,
787                    stackable: elem.stackable,
788                }
789            }
790        }));
791        if PRICING_DEBUG {
792            for i in result.items.0.iter() {
793                tracing::debug!("before recipes {:?}", *i);
794            }
795        }
796
797        // Apply recipe book
798        let mut secondaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
799        let book = complete_recipe_book().read();
800        let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
801        for (_, recipe) in book.iter() {
802            let (ref asset_path, amount) = recipe.output;
803            if let ItemKind::ModularComponent(
804                inventory::item::modular::ModularComponent::ToolSecondaryComponent {
805                    toolkind,
806                    stats: _,
807                    hand_restriction: _,
808                },
809            ) = asset_path.kind
810            {
811                secondaries
812                    .entry(toolkind)
813                    .or_default()
814                    .push(ItemDefinitionIdOwned::Simple(asset_path.id().into()));
815            }
816            ordered_recipes.push(RememberedRecipe {
817                output: ItemDefinitionIdOwned::Simple(asset_path.id().into()),
818                amount,
819                material_cost: None,
820                input: recipe
821                    .inputs
822                    .iter()
823                    .filter_map(|&(ref recipe_input, count, _)| {
824                        if let RecipeInput::Item(it) = recipe_input {
825                            // If item is not consumed in craft, ignore it
826                            if count == 0 {
827                                None
828                            } else {
829                                Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
830                            }
831                        } else {
832                            None
833                        }
834                    })
835                    .collect(),
836            });
837        }
838
839        // modular weapon recipes
840        let mut primaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
841        let comp_book = default_component_recipe_book().read();
842        for (key, recipe) in comp_book.iter() {
843            primaries
844                .entry(key.toolkind)
845                .or_default()
846                .push(recipe.itemdef_output());
847            ordered_recipes.push(RememberedRecipe {
848                output: recipe.itemdef_output(),
849                amount: 1,
850                material_cost: None,
851                input: recipe
852                    .inputs()
853                    .filter_map(|(ref recipe_input, count)| {
854                        if count == 0 {
855                            None
856                        } else {
857                            match recipe_input {
858                                RecipeInput::Item(it) => {
859                                    Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
860                                },
861                                RecipeInput::Tag(_) => todo!(),
862                                RecipeInput::TagSameItem(_) => todo!(),
863                                RecipeInput::ListSameItem(_) => todo!(),
864                            }
865                        }
866                    })
867                    .collect(),
868            });
869        }
870
871        // drain the larger map while iterating the shorter
872        for (kind, mut primary_vec) in primaries.drain() {
873            for primary in primary_vec.drain(0..) {
874                for secondary in secondaries[&kind].iter() {
875                    let input = vec![(primary.clone(), 1), (secondary.clone(), 1)];
876                    let components = vec![primary.clone(), secondary.clone()];
877                    let output = ItemDefinitionIdOwned::Modular {
878                        pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
879                        components,
880                    };
881                    ordered_recipes.push(RememberedRecipe {
882                        output,
883                        amount: 1,
884                        material_cost: None,
885                        input,
886                    });
887                }
888            }
889        }
890        drop(secondaries);
891
892        // re-evaluate prices based on crafting tables
893        // (start with cheap ones to avoid changing material prices after evaluation)
894        let ability_map = &AbilityMap::load().read();
895        let msm = &MaterialStatManifest::load().read();
896        while result.sort_by_price(&mut ordered_recipes) {
897            ordered_recipes.retain(|recipe| {
898                if recipe.material_cost.is_some_and(|p| p < 1e-5) || recipe.amount == 0 {
899                    // don't handle recipes which have no raw materials
900                    false
901                } else if recipe.material_cost.is_some() {
902                    let actual_cost = result.calculate_material_cost(recipe);
903                    if let Some(usage) = actual_cost {
904                        let output_tradeable = recipe.input.iter().all(|(input, _)| {
905                            result
906                                .items
907                                .0
908                                .iter()
909                                .find(|item| item.name == *input)
910                                .is_some_and(|item| item.sell)
911                        });
912                        let stackable = Item::new_from_item_definition_id(
913                            recipe.output.as_ref(),
914                            ability_map,
915                            msm,
916                        )
917                        .is_ok_and(|i| i.is_stackable());
918                        let new_entry = PriceEntry {
919                            name: recipe.output.clone(),
920                            price: usage * (1.0 / (recipe.amount as f32 * Self::CRAFTING_FACTOR)),
921                            sell: output_tradeable,
922                            stackable,
923                        };
924                        if PRICING_DEBUG {
925                            tracing::trace!("Recipe {:?}", new_entry);
926                        }
927                        result.items.add_alternative(new_entry);
928                    } else {
929                        error!("Recipe {:?} incomplete confusion", recipe);
930                    }
931                    false
932                } else {
933                    // handle incomplete recipes later
934                    true
935                }
936            });
937            //info!(?ordered_recipes);
938        }
939        result
940    }
941
942    // TODO: optimize repeated use
943    fn random_items_impl(
944        &self,
945        stockmap: &mut HashMap<Good, f32>,
946        mut number: u32,
947        selling: bool,
948        always_coin: bool,
949        limit: u32,
950        mut permitted: impl FnMut(Good) -> bool,
951    ) -> Vec<(ItemDefinitionIdOwned, u32)> {
952        // 1. Pre-filter from all possible items to keep ones we want.
953        // * Sellable
954        // * Fit under good limit (would probably always be true here)
955        // * Permitted by good disriminant
956        let mut candidates: Vec<&PriceEntry> = self
957            .items
958            .0
959            .iter()
960            .filter(|i| {
961                let excess = i
962                    .price
963                    .iter()
964                    .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
965                permitted(TradePricing::good_from_item(&i.name))
966                    && excess.is_none()
967                    && (!selling || i.sell)
968                    && (!always_coin
969                        || i.name != ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()))
970            })
971            .collect();
972        // 2. Start putting items
973        let mut result = Vec::new();
974        // 3. Put the coin stack
975        if always_coin && number > 0 {
976            let amount = stockmap.get(&Good::Coin).copied().unwrap_or_default() as u32;
977            if amount > 0 {
978                result.push((
979                    ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
980                    amount,
981                ));
982                number -= 1;
983            }
984        }
985        // 4. At each step, roll an item (and its amount), then discard
986        // corresponding ingredients that the item is made of
987        //
988        // If all we have no more resources (in at least one category),
989        // break out of the loop and return.
990        for _ in 0..number {
991            candidates.retain(|i| {
992                let excess = i
993                    .price
994                    .iter()
995                    .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
996                excess.is_none()
997            });
998            if candidates.is_empty() {
999                break;
1000            }
1001            let index = (rand::random::<f32>() * candidates.len() as f32).floor() as usize;
1002            let result2 = candidates[index];
1003            let amount: u32 = if result2.stackable {
1004                let max_amount = result2
1005                    .price
1006                    .iter()
1007                    .map(|e| {
1008                        stockmap
1009                            .get_mut(&e.1)
1010                            .map(|stock| *stock / e.0.max(0.001))
1011                            .unwrap_or_default()
1012                    })
1013                    .fold(f32::INFINITY, f32::min)
1014                    .min(limit as f32);
1015                (rand::random::<f32>() * (max_amount - 1.0)).floor() as u32 + 1
1016            } else {
1017                1
1018            };
1019            for i in result2.price.iter() {
1020                stockmap.get_mut(&i.1).map(|v| *v -= i.0 * (amount as f32));
1021            }
1022            result.push((result2.name.clone(), amount));
1023            // avoid duplicates
1024            candidates.remove(index);
1025        }
1026        result
1027    }
1028
1029    fn get_materials_impl(&self, item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1030        self.price_lookup(&item.to_owned()).cloned()
1031    }
1032
1033    #[must_use]
1034    pub fn random_items(
1035        stock: &mut HashMap<Good, f32>,
1036        number: u32,
1037        selling: bool,
1038        always_coin: bool,
1039        limit: u32,
1040        permitted: impl FnMut(Good) -> bool,
1041    ) -> Vec<(ItemDefinitionIdOwned, u32)> {
1042        TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit, permitted)
1043    }
1044
1045    #[must_use]
1046    pub fn get_materials(item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1047        TRADE_PRICING.get_materials_impl(item)
1048    }
1049
1050    #[cfg(test)]
1051    fn instance() -> &'static Self { &TRADE_PRICING }
1052
1053    #[cfg(test)]
1054    fn print_sorted(&self) {
1055        use crate::comp::item::{DurabilityMultiplier, armor}; //, ItemKind, MaterialStatManifest};
1056
1057        println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
1058
1059        fn more_information(i: &Item, p: f32) -> (String, &'static str) {
1060            let msm = &MaterialStatManifest::load().read();
1061            let durability_multiplier = DurabilityMultiplier(1.0);
1062
1063            if let ItemKind::Armor(a) = &*i.kind() {
1064                (
1065                    match a.stats(msm, durability_multiplier).protection {
1066                        Some(armor::Protection::Invincible) => "Invincible".into(),
1067                        Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
1068                        None => "0.0".into(),
1069                    },
1070                    "prot/val",
1071                )
1072            } else if let ItemKind::Tool(t) = &*i.kind() {
1073                let stats = t.stats(durability_multiplier);
1074                (format!("{:.4}", stats.power * stats.speed * p), "dps/val")
1075            } else if let ItemKind::Consumable {
1076                kind: _,
1077                effects,
1078                container: _,
1079            } = &*i.kind()
1080            {
1081                (
1082                    effects
1083                        .effects()
1084                        .iter()
1085                        .map(|e| {
1086                            if let crate::effect::Effect::Buff(b) = e {
1087                                format!("{:.2}", b.data.strength * p)
1088                            } else {
1089                                format!("{:?}", e)
1090                            }
1091                        })
1092                        .collect::<Vec<String>>()
1093                        .join(" "),
1094                    "str/val",
1095                )
1096            } else {
1097                (Default::default(), "")
1098            }
1099        }
1100        let mut sorted: Vec<(f32, &PriceEntry)> = self
1101            .items
1102            .0
1103            .iter()
1104            .map(|e| (e.price.iter().map(|i| i.0.to_owned()).sum(), e))
1105            .collect();
1106        sorted.sort_by(|(p, e), (p2, e2)| {
1107            p2.partial_cmp(p)
1108                .unwrap_or(Ordering::Equal)
1109                .then(e.name.cmp(&e2.name))
1110        });
1111
1112        for (
1113            pricesum,
1114            PriceEntry {
1115                name: item_id,
1116                price: mat_use,
1117                sell: can_sell,
1118                stackable: _,
1119            },
1120        ) in sorted.iter()
1121        {
1122            Item::new_from_item_definition_id(
1123                item_id.as_ref(),
1124                &AbilityMap::load().read(),
1125                &MaterialStatManifest::load().read(),
1126            )
1127            .ok()
1128            .map(|it| {
1129                //let price = mat_use.iter().map(|(amount, _good)| *amount).sum::<f32>();
1130                let prob = 1.0 / pricesum;
1131                let (info, unit) = more_information(&it, prob);
1132                let materials = mat_use
1133                    .iter()
1134                    .fold(String::new(), |agg, i| agg + &format!("{:?}.", i.1));
1135                println!(
1136                    "{:?}, {}, {:>4.2}, {}, {:?}, {}, {},",
1137                    item_id,
1138                    if *can_sell { "yes" } else { "no" },
1139                    pricesum,
1140                    materials,
1141                    it.quality(),
1142                    info,
1143                    unit,
1144                );
1145            });
1146        }
1147    }
1148}
1149
1150/// hierarchically combine and scale this loot table
1151#[must_use]
1152pub fn expand_loot_table(loot_table: &str) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
1153    ProbabilityFile::from(vec![(1.0, LootSpec::LootTable(loot_table.into()))]).content
1154}
1155
1156// if you want to take a look at the calculated values run:
1157// cd common && cargo test trade_pricing -- --nocapture
1158#[cfg(test)]
1159mod tests {
1160    use crate::{
1161        assets,
1162        assets::AssetExt,
1163        comp::{
1164            Item,
1165            inventory::{
1166                ItemDefinitionIdOwned,
1167                trade_pricing::{MaterialUse, TradePricing},
1168            },
1169        },
1170        generation::{EntityConfig, try_all_entity_configs},
1171        lottery::{LootSpec, Lottery},
1172        terrain::SpriteKind,
1173        trade::Good,
1174    };
1175    use tracing::{Level, info};
1176    use tracing_subscriber::{FmtSubscriber, filter::EnvFilter};
1177
1178    fn init() {
1179        FmtSubscriber::builder()
1180            .with_max_level(Level::ERROR)
1181            .with_env_filter(EnvFilter::from_default_env())
1182            .try_init()
1183            .unwrap_or(());
1184    }
1185
1186    #[cfg(test)]
1187    /// Gives you all the items we care about
1188    pub fn get_items_from_loot_spec<T: AsRef<str>>(item: &LootSpec<T>) -> Vec<Item> {
1189        match item {
1190            LootSpec::Item(item) => vec![Item::new_from_asset_expect(item.as_ref())],
1191            LootSpec::LootTable(loot_table) => {
1192                Lottery::<LootSpec<String>>::load_expect(loot_table.as_ref())
1193                    .read()
1194                    .iter()
1195                    .flat_map(|(_weight, s)| get_items_from_loot_spec(s))
1196                    .collect()
1197            },
1198            LootSpec::Nothing => vec![],
1199            LootSpec::ModularWeapon { .. } => {
1200                // TODO: populate?
1201                // We don't care about modular weapons in this case, as
1202                // they are properly priced
1203                //
1204                // If you wanted, you could reimplemented it with specific
1205                // functions, such as ones used for `all_items_expect`
1206                vec![]
1207            },
1208            LootSpec::ModularWeaponPrimaryComponent { .. } => {
1209                // Same as above
1210                vec![]
1211            },
1212            LootSpec::MultiDrop(loot_spec, _lower, _upper) => get_items_from_loot_spec(loot_spec),
1213            LootSpec::All(loot_specs) => loot_specs
1214                .iter()
1215                .flat_map(|s| get_items_from_loot_spec(s))
1216                .collect(),
1217            LootSpec::Lottery(table) => table
1218                .iter()
1219                .flat_map(|(_weight, s)| get_items_from_loot_spec(s))
1220                .collect(),
1221        }
1222    }
1223
1224    #[cfg(test)]
1225    /// Well, almost all loot items, modulars are currently discarded
1226    fn all_loot_items() -> Vec<Item> {
1227        let loot_tables = assets::load_rec_dir::<Lottery<LootSpec<String>>>("common.loot_tables")
1228            .expect("load loot_tables");
1229        let mut buf = vec![];
1230        for loot_table in loot_tables.read().ids() {
1231            for (_weight, loot_spec) in Lottery::<LootSpec<String>>::load_expect(loot_table)
1232                .read()
1233                .iter()
1234            {
1235                buf.extend(get_items_from_loot_spec(loot_spec));
1236            }
1237        }
1238
1239        for entity_config in try_all_entity_configs().unwrap() {
1240            let config = EntityConfig::from_asset_expect_owned(&entity_config);
1241            buf.extend(get_items_from_loot_spec(&config.loot));
1242        }
1243
1244        for sprite in SpriteKind::all() {
1245            let Some(Some(spec)) = sprite.default_loot_spec() else {
1246                continue;
1247            };
1248            buf.extend(get_items_from_loot_spec(&spec));
1249        }
1250
1251        buf
1252    }
1253
1254    #[test]
1255    fn test_all_included() {
1256        let todos = [
1257            // Calendar
1258            ItemDefinitionIdOwned::Simple("common.items.food.honeycorn".to_owned()),
1259            ItemDefinitionIdOwned::Simple("common.items.food.pumpkin_spice_brew".to_owned()),
1260            ItemDefinitionIdOwned::Simple("common.items.food.blue_cheese".to_owned()),
1261            ItemDefinitionIdOwned::Simple("common.items.consumable.potion_curious".to_owned()),
1262            ItemDefinitionIdOwned::Simple("common.items.armor.misc.head.hare_hat".to_owned()),
1263            ItemDefinitionIdOwned::Simple(
1264                "common.items.armor.misc.head.scarlet_spectacles".to_owned(),
1265            ),
1266            ItemDefinitionIdOwned::Simple("common.items.armor.misc.head.facegourd".to_owned()),
1267            ItemDefinitionIdOwned::Simple("common.items.armor.misc.back.rat_tail".to_owned()),
1268            ItemDefinitionIdOwned::Simple("common.items.armor.misc.head.cat_capuche".to_owned()),
1269            ItemDefinitionIdOwned::Simple(
1270                "common.items.calendar.christmas.armor.misc.head.woolly_wintercap".to_owned(),
1271            ),
1272            ItemDefinitionIdOwned::Simple("common.items.utility.surprise_egg".to_owned()),
1273            // Lanterns
1274            ItemDefinitionIdOwned::Simple("common.items.lantern.divers_light".to_owned()),
1275            ItemDefinitionIdOwned::Simple("common.items.lantern.luminous_bloom".to_owned()),
1276            ItemDefinitionIdOwned::Simple("common.items.lantern.frozen_heart".to_owned()),
1277            // Quests, figure out what to do with them
1278            ItemDefinitionIdOwned::Simple("common.items.quest.gnarling_carving".to_owned()),
1279            ItemDefinitionIdOwned::Simple("common.items.quest.legoom_leaf".to_owned()),
1280        ];
1281
1282        let mut items: Vec<_> = all_loot_items()
1283            .into_iter()
1284            .map(|i| i.item_definition_id().to_owned())
1285            .map(|i| (i.clone(), TradePricing::get_materials(&i.as_ref())))
1286            .collect();
1287
1288        items.retain(|(i, mat)| mat.is_none() && !todos.contains(i));
1289
1290        // Remove duplicates, dedup requires sorting first, as it only removes
1291        // adjacent dedups.
1292        //
1293        // Weird sort_by because floats are evil. That's also why we can't just
1294        // use HashSet here.
1295        items.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Less));
1296        items.dedup();
1297
1298        assert_eq!(
1299            Vec::<(ItemDefinitionIdOwned, Option<MaterialUse>)>::new(),
1300            items,
1301            "please add these items to assets/common/trading/"
1302        );
1303    }
1304
1305    #[test]
1306    fn test_prices1() {
1307        init();
1308        info!("init");
1309
1310        TradePricing::instance().print_sorted();
1311    }
1312
1313    #[test]
1314    fn test_prices2() {
1315        init();
1316        info!("init");
1317
1318        let mut stock: hashbrown::HashMap<Good, f32> = [
1319            (Good::Ingredients, 50.0),
1320            (Good::Tools, 10.0),
1321            (Good::Armor, 10.0),
1322            //(Good::Ores, 20.0),
1323        ]
1324        .iter()
1325        .copied()
1326        .collect();
1327
1328        let loadout = TradePricing::random_items(&mut stock, 20, false, false, 999, |_| true);
1329        for i in loadout.iter() {
1330            info!("Random item {:?}*{}", i.0, i.1);
1331        }
1332    }
1333}