1use crate::{
2 assets::{self, AssetExt},
3 comp::{
4 inventory,
5 item::{
6 Item, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, MaterialStatManifest,
7 ModularBase,
8 },
9 tool::AbilityMap,
10 },
11 lottery::LootSpec,
12 recipe::{RecipeInput, complete_recipe_book, default_component_recipe_book},
13 trade::Good,
14};
15use assets::AssetReadGuard;
16use hashbrown::HashMap;
17use lazy_static::lazy_static;
18use serde::Deserialize;
19use std::cmp::Ordering;
20use tracing::{error, info, warn};
21
22use super::item::{Material, ToolKind};
23
24const PRICING_DEBUG: bool = false;
25
26#[derive(Default, Debug)]
27pub struct TradePricing {
28 items: PriceEntries,
29 equality_set: EqualitySet,
30}
31
32#[derive(Default, Debug, Clone)]
39pub struct MaterialUse(Vec<(f32, Good)>);
40
41impl std::ops::Mul<f32> for MaterialUse {
42 type Output = Self;
43
44 fn mul(self, rhs: f32) -> Self::Output {
45 Self(self.0.iter().map(|v| (v.0 * rhs, v.1)).collect())
46 }
47}
48
49fn vector_add_eq(result: &mut Vec<(f32, Good)>, rhs: &[(f32, Good)]) {
51 for (amount, good) in rhs {
52 if result
53 .iter_mut()
54 .find(|(_amount2, good2)| *good == *good2)
55 .map(|elem| elem.0 += *amount)
56 .is_none()
57 {
58 result.push((*amount, *good));
59 }
60 }
61}
62
63impl std::ops::Add for MaterialUse {
64 type Output = Self;
65
66 fn add(self, rhs: Self) -> Self::Output {
67 let mut result = self;
68 vector_add_eq(&mut result.0, &rhs.0);
69 result
70 }
71}
72
73impl std::ops::AddAssign for MaterialUse {
74 fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
75}
76
77impl std::iter::Sum<MaterialUse> for MaterialUse {
78 fn sum<I>(iter: I) -> Self
79 where
80 I: Iterator<Item = Self>,
81 {
82 let mut ret = Self::default();
83 for i in iter {
84 ret += i;
85 }
86 ret
87 }
88}
89
90impl std::ops::Deref for MaterialUse {
91 type Target = [(f32, Good)];
92
93 fn deref(&self) -> &Self::Target { self.0.deref() }
94}
95
96#[derive(Default, Debug, Clone)]
98pub struct MaterialFrequency(Vec<(f32, Good)>);
99
100fn vector_invert(result: &mut [(f32, Good)]) {
105 let mut oldsum: f32 = 0.0;
106 let mut newsum: f32 = 0.0;
107 for (value, _good) in result.iter_mut() {
108 oldsum += *value;
109 *value = 1.0 / *value;
110 newsum += *value;
111 }
112 let scale = 1.0 / (oldsum * newsum);
113 for (value, _good) in result.iter_mut() {
114 *value *= scale;
115 }
116}
117
118impl From<MaterialUse> for MaterialFrequency {
119 fn from(u: MaterialUse) -> Self {
120 let mut result = Self(u.0);
121 vector_invert(&mut result.0);
122 result
123 }
124}
125
126impl From<MaterialFrequency> for MaterialUse {
128 fn from(u: MaterialFrequency) -> Self {
129 let mut result = Self(u.0);
130 vector_invert(&mut result.0);
131 result
132 }
133}
134
135impl std::ops::Add for MaterialFrequency {
136 type Output = Self;
137
138 fn add(self, rhs: Self) -> Self::Output {
139 let mut result = self;
140 vector_add_eq(&mut result.0, &rhs.0);
141 result
142 }
143}
144
145impl std::ops::AddAssign for MaterialFrequency {
146 fn add_assign(&mut self, rhs: Self) { vector_add_eq(&mut self.0, &rhs.0); }
147}
148
149#[derive(Debug)]
150struct PriceEntry {
151 name: ItemDefinitionIdOwned,
152 price: MaterialUse,
153 sell: bool,
155 stackable: bool,
156}
157#[derive(Debug)]
158struct FreqEntry {
159 name: ItemDefinitionIdOwned,
160 freq: MaterialFrequency,
161 sell: bool,
162 stackable: bool,
163}
164
165#[derive(Default, Debug)]
166struct PriceEntries(Vec<PriceEntry>);
167#[derive(Default, Debug)]
168struct FreqEntries(Vec<FreqEntry>);
169
170impl PriceEntries {
171 fn add_alternative(&mut self, b: PriceEntry) {
172 let already = self.0.iter_mut().find(|i| i.name == b.name);
174 if let Some(entry) = already {
175 let entry_freq: MaterialFrequency = std::mem::take(&mut entry.price).into();
176 let b_freq: MaterialFrequency = b.price.into();
177 let result = entry_freq + b_freq;
178 entry.price = result.into();
179 } else {
180 self.0.push(b);
181 }
182 }
183}
184
185impl FreqEntries {
186 fn add(
187 &mut self,
188 eqset: &EqualitySet,
189 item_name: &ItemDefinitionIdOwned,
190 good: Good,
191 probability: f32,
192 can_sell: bool,
193 ) {
194 let canonical_itemname = eqset.canonical(item_name);
195 let old = self
196 .0
197 .iter_mut()
198 .find(|elem| elem.name == *canonical_itemname);
199 let new_freq = MaterialFrequency(vec![(probability, good)]);
200 if let Some(FreqEntry {
202 name: asset,
203 freq: ref mut old_probability,
204 sell: old_can_sell,
205 stackable: _,
206 }) = old
207 {
208 if PRICING_DEBUG {
209 info!("Update {:?} {:?}+{:?}", asset, old_probability, probability);
210 }
211 if !can_sell && *old_can_sell {
212 *old_can_sell = false;
213 }
214 *old_probability += new_freq;
215 } else {
216 let stackable = Item::new_from_item_definition_id(
217 canonical_itemname.as_ref(),
218 &AbilityMap::load().read(),
219 &MaterialStatManifest::load().read(),
220 )
221 .is_ok_and(|i| i.is_stackable());
222 let new_mat_prob: FreqEntry = FreqEntry {
223 name: canonical_itemname.to_owned(),
224 freq: new_freq,
225 sell: can_sell,
226 stackable,
227 };
228 if PRICING_DEBUG {
229 info!("New {:?}", new_mat_prob);
230 }
231 self.0.push(new_mat_prob);
232 }
233
234 if canonical_itemname != item_name && !self.0.iter().any(|elem| elem.name == *item_name) {
238 self.0.push(FreqEntry {
239 name: item_name.to_owned(),
240 freq: Default::default(),
241 sell: can_sell,
242 stackable: false,
243 });
244 }
245 }
246}
247
248lazy_static! {
249 static ref TRADE_PRICING: TradePricing = TradePricing::read();
250}
251
252#[derive(Clone)]
253pub struct ProbabilityFile {
260 pub content: Vec<(f32, ItemDefinitionIdOwned, f32)>,
261}
262
263impl assets::Asset for ProbabilityFile {
264 type Loader = assets::LoadFrom<Vec<(f32, LootSpec<String>)>, assets::RonLoader>;
265
266 const EXTENSION: &'static str = "ron";
267}
268
269type ComponentPool =
270 HashMap<(ToolKind, String), Vec<(ItemDefinitionIdOwned, Option<inventory::item::Hands>)>>;
271
272lazy_static! {
273 static ref PRIMARY_COMPONENT_POOL: ComponentPool = {
274 let mut component_pool = HashMap::new();
275
276 use crate::recipe::ComponentKey;
278 let recipes = default_component_recipe_book().read();
279
280 recipes
281 .iter()
282 .for_each(|(ComponentKey { toolkind, material, .. }, recipe)| {
283 let component = recipe.itemdef_output();
284 let hand_restriction = None; let entry: &mut Vec<_> = component_pool.entry((*toolkind, String::from(material))).or_default();
286 entry.push((component, hand_restriction));
287 });
288
289 component_pool
290 };
291
292 static ref SECONDARY_COMPONENT_POOL: ComponentPool = {
293 let mut component_pool = HashMap::new();
294
295 let recipes = complete_recipe_book().read();
298
299 recipes
300 .iter()
301 .for_each(|(_, recipe)| {
302 let (ref asset_path, _) = recipe.output;
303 if let ItemKind::ModularComponent(
304 crate::comp::inventory::item::modular::ModularComponent::ToolSecondaryComponent {
305 toolkind,
306 stats: _,
307 hand_restriction,
308 },
309 ) = asset_path.kind
310 {
311 let component = ItemDefinitionIdOwned::Simple(asset_path.id().into());
312 let entry: &mut Vec<_> = component_pool.entry((toolkind, String::new())).or_default();
313 entry.push((component, hand_restriction));
314 }});
315
316 component_pool
317 };
318}
319
320pub fn expand_primary_component(
325 tool: ToolKind,
326 material: Material,
327 hand_restriction: Option<inventory::item::Hands>,
328) -> Vec<ItemDefinitionIdOwned> {
329 if let Some(material_id) = material.asset_identifier() {
330 PRIMARY_COMPONENT_POOL
331 .get(&(tool, material_id.to_owned()))
332 .into_iter()
333 .flatten()
334 .filter(move |(_comp, hand)| match (hand_restriction, *hand) {
335 (Some(restriction), Some(hand)) => restriction == hand,
336 (None, _) | (_, None) => true,
337 })
338 .map(|e| e.0.clone())
339 .collect()
340 } else {
341 Vec::new()
342 }
343}
344
345pub fn expand_secondary_component(
346 tool: ToolKind,
347 _material: Material,
348 hand_restriction: Option<inventory::item::Hands>,
349) -> impl Iterator<Item = ItemDefinitionIdOwned> {
350 SECONDARY_COMPONENT_POOL
351 .get(&(tool, String::new()))
352 .into_iter()
353 .flatten()
354 .filter(move |(_comp, hand)| match (hand_restriction, *hand) {
355 (Some(restriction), Some(hand)) => restriction == hand,
356 (None, _) | (_, None) => true,
357 })
358 .map(|e| e.0.clone())
359}
360
361impl From<Vec<(f32, LootSpec<String>)>> for ProbabilityFile {
362 fn from(content: Vec<(f32, LootSpec<String>)>) -> Self {
363 let rescale = if content.is_empty() {
364 1.0
365 } else {
366 1.0 / content.iter().map(|e| e.0).sum::<f32>()
367 };
368 fn get_content(
369 rescale: f32,
370 p0: f32,
371 loot: LootSpec<String>,
372 ) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
373 match loot {
374 LootSpec::Item(asset) => {
375 vec![(p0 * rescale, ItemDefinitionIdOwned::Simple(asset), 1.0)]
376 },
377 LootSpec::LootTable(table_asset) => {
378 let unscaled = &ProbabilityFile::load_expect(&table_asset).read().content;
379 let scale = p0 * rescale;
380 unscaled
381 .iter()
382 .map(|(p1, asset, amount)| (*p1 * scale, asset.clone(), *amount))
383 .collect::<Vec<_>>()
384 },
385 LootSpec::Lottery(table) => {
386 let unscaled = ProbabilityFile::from(table);
387 let scale = p0 * rescale;
388 unscaled
389 .content
390 .into_iter()
391 .map(|(p1, asset, amount)| (p1 * scale, asset, amount))
392 .collect::<Vec<_>>()
393 },
394 LootSpec::ModularWeapon {
395 tool,
396 material,
397 hands,
398 } => {
399 let mut primary = expand_primary_component(tool, material, hands);
400 let secondary: Vec<ItemDefinitionIdOwned> =
401 expand_secondary_component(tool, material, hands).collect();
402 let freq = if primary.is_empty() || secondary.is_empty() {
403 0.0
404 } else {
405 p0 * rescale / ((primary.len() * secondary.len()) as f32)
406 };
407 let res: Vec<(f32, ItemDefinitionIdOwned, f32)> = primary
408 .drain(0..)
409 .flat_map(|p| {
410 secondary.iter().map(move |s| {
411 let components = vec![p.clone(), s.clone()];
412 (
413 freq,
414 ItemDefinitionIdOwned::Modular {
415 pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
416 components,
417 },
418 1.0f32,
419 )
420 })
421 })
422 .collect();
423 res
424 },
425 LootSpec::ModularWeaponPrimaryComponent {
426 tool,
427 material,
428 hands,
429 } => {
430 let mut res = expand_primary_component(tool, material, hands);
431 let freq = if res.is_empty() {
432 0.0
433 } else {
434 p0 * rescale / (res.len() as f32)
435 };
436 let res: Vec<(f32, ItemDefinitionIdOwned, f32)> =
437 res.drain(0..).map(|e| (freq, e, 1.0f32)).collect();
438 res
439 },
440 LootSpec::Nothing => Vec::new(),
441 LootSpec::MultiDrop(loot, a, b) => {
442 let average_count = (a + b) as f32 * 0.5;
443 let mut content = get_content(rescale, p0, *loot);
444 for (_, _, count) in content.iter_mut() {
445 *count *= average_count;
446 }
447 content
448 },
449 LootSpec::All(loot_specs) => loot_specs
450 .into_iter()
451 .flat_map(|loot| get_content(rescale, p0, loot))
452 .collect(),
453 }
454 }
455 Self {
456 content: content
457 .into_iter()
458 .flat_map(|(p0, loot)| get_content(rescale, p0, loot))
459 .collect(),
460 }
461 }
462}
463
464#[derive(Debug, Deserialize)]
465struct TradingPriceFile {
466 pub loot_tables: Vec<(f32, bool, String)>,
468 pub good_scaling: Vec<(Good, f32)>,
470}
471
472impl assets::Asset for TradingPriceFile {
473 type Loader = assets::RonLoader;
474
475 const EXTENSION: &'static str = "ron";
476}
477
478#[derive(Clone, Debug, Default)]
479struct EqualitySet {
480 equivalence_class: HashMap<ItemDefinitionIdOwned, ItemDefinitionIdOwned>,
482}
483
484impl EqualitySet {
485 fn canonical<'a>(&'a self, item_name: &'a ItemDefinitionIdOwned) -> &'a ItemDefinitionIdOwned {
486 let canonical_itemname = self
488 .equivalence_class
489 .get(item_name)
490 .map_or(item_name, |i| i);
491
492 canonical_itemname
493 }
494}
495
496impl assets::Compound for EqualitySet {
497 fn load(
498 cache: assets::AnyCache,
499 id: &assets::SharedString,
500 ) -> Result<Self, assets::BoxedError> {
501 #[derive(Debug, Deserialize)]
502 enum EqualitySpec {
503 LootTable(String),
504 Set(Vec<String>),
505 }
506
507 let mut eqset = Self {
508 equivalence_class: HashMap::new(),
509 };
510
511 let manifest = &cache.load::<assets::Ron<Vec<EqualitySpec>>>(id)?.read().0;
512 for set in manifest {
513 let items: Vec<ItemDefinitionIdOwned> = match set {
514 EqualitySpec::LootTable(table) => {
515 let acc = &ProbabilityFile::load_expect(table).read().content;
516
517 acc.iter().map(|(_p, item, _)| item).cloned().collect()
518 },
519 EqualitySpec::Set(xs) => xs
520 .iter()
521 .map(|s| ItemDefinitionIdOwned::Simple(s.clone()))
522 .collect(),
523 };
524 let mut iter = items.iter();
525 if let Some(first) = iter.next() {
526 eqset.equivalence_class.insert(first.clone(), first.clone());
527 for item in iter {
528 eqset.equivalence_class.insert(item.clone(), first.clone());
529 }
530 }
531 }
532 Ok(eqset)
533 }
534}
535
536#[derive(Debug)]
537struct RememberedRecipe {
538 output: ItemDefinitionIdOwned,
539 amount: u32,
540 material_cost: Option<f32>,
541 input: Vec<(ItemDefinitionIdOwned, u32)>,
542}
543
544fn get_scaling(contents: &AssetReadGuard<TradingPriceFile>, good: Good) -> f32 {
545 contents
546 .good_scaling
547 .iter()
548 .find(|(good_kind, _)| *good_kind == good)
549 .map_or(1.0, |(_, scaling)| *scaling)
550}
551
552#[cfg(test)]
553impl PartialOrd for ItemDefinitionIdOwned {
554 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
555}
556
557#[cfg(test)]
558impl Ord for ItemDefinitionIdOwned {
559 fn cmp(&self, other: &Self) -> Ordering {
560 match self {
561 ItemDefinitionIdOwned::Simple(na) => match other {
562 ItemDefinitionIdOwned::Simple(nb) => na.cmp(nb),
563 _ => Ordering::Less,
564 },
565 ItemDefinitionIdOwned::Modular {
566 pseudo_base,
567 components,
568 } => match other {
569 ItemDefinitionIdOwned::Simple(_) => Ordering::Greater,
570 ItemDefinitionIdOwned::Modular {
571 pseudo_base: pseudo_base2,
572 components: components2,
573 } => pseudo_base
574 .cmp(pseudo_base2)
575 .then_with(|| components.cmp(components2)),
576 _ => Ordering::Less,
577 },
578 ItemDefinitionIdOwned::Compound {
579 simple_base,
580 components,
581 } => match other {
582 ItemDefinitionIdOwned::Compound {
583 simple_base: simple_base2,
584 components: components2,
585 } => simple_base
586 .cmp(simple_base2)
587 .then_with(|| components.cmp(components2)),
588 _ => Ordering::Greater,
589 },
590 }
591 }
592}
593
594impl TradePricing {
595 const COIN_ITEM: &'static str = "common.items.utility.coins";
596 const CRAFTING_FACTOR: f32 = 0.95;
597 const INVEST_FACTOR: f32 = 0.33;
599
600 fn good_from_item(name: &ItemDefinitionIdOwned) -> Good {
601 match name {
602 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.armor.") => {
603 Good::Armor
604 },
605
606 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.weapons.") => {
607 Good::Tools
608 },
609 ItemDefinitionIdOwned::Simple(name)
610 if name.starts_with("common.items.modular.weapon.") =>
611 {
612 Good::Tools
613 },
614 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.tool.") => {
615 Good::Tools
616 },
617
618 ItemDefinitionIdOwned::Simple(name)
619 if name.starts_with("common.items.crafting_ing.") =>
620 {
621 Good::Ingredients
622 },
623 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.mineral.") => {
624 Good::Ingredients
625 },
626 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.log.") => {
627 Good::Wood
628 },
629 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.flowers.") => {
630 Good::Ingredients
631 },
632 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.consumable.") => {
633 Good::Potions
634 },
635 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.food.") => {
636 Good::Food
637 },
638 ItemDefinitionIdOwned::Simple(name) if name.as_str() == Self::COIN_ITEM => Good::Coin,
639 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.recipes.") => {
640 Good::Recipe
641 },
642 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.glider.") => {
643 Good::Tools
644 },
645 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.utility.") => {
646 Good::default()
647 },
648 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.boss_drops.") => {
649 Good::Tools
650 },
651 ItemDefinitionIdOwned::Simple(name)
652 if name.starts_with("common.items.crafting_tools.") =>
653 {
654 Good::default()
655 },
656 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.lantern.") => {
657 Good::Tools
658 },
659 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.keys.") => {
660 Good::Tools
661 },
662 ItemDefinitionIdOwned::Modular {
663 pseudo_base: _,
664 components: _,
665 } => Good::Tools,
666 ItemDefinitionIdOwned::Compound {
667 simple_base: _,
668 components: _,
669 } => Good::Ingredients,
670 _ => {
671 warn!("unknown loot item {:?}", name);
672 Good::default()
673 },
674 }
675 }
676
677 fn price_lookup(&self, requested_name: &ItemDefinitionIdOwned) -> Option<&MaterialUse> {
679 let canonical_name = self.equality_set.canonical(requested_name);
680 self.items
681 .0
682 .iter()
683 .find(|e| &e.name == canonical_name)
684 .map(|e| &e.price)
685 }
686
687 fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
688 r.input
689 .iter()
690 .map(|(name, amount)| {
691 self.price_lookup(name).map(|x| {
692 x.clone()
693 * (if *amount > 0 {
694 *amount as f32
695 } else {
696 Self::INVEST_FACTOR
697 })
698 })
699 })
700 .try_fold(MaterialUse::default(), |acc, elem| Some(acc + elem?))
701 }
702
703 fn calculate_material_cost_sum(&self, r: &RememberedRecipe) -> Option<f32> {
704 self.calculate_material_cost(r)?
705 .iter()
706 .fold(None, |acc, elem| Some(acc.unwrap_or_default() + elem.0))
707 }
708
709 fn sort_by_price(&self, recipes: &mut [RememberedRecipe]) -> bool {
712 for recipe in recipes.iter_mut() {
713 recipe.material_cost = self.calculate_material_cost_sum(recipe);
714 }
715 recipes.sort_by(|a, b| {
717 if a.material_cost.is_some() {
718 if b.material_cost.is_some() {
719 a.material_cost
720 .partial_cmp(&b.material_cost)
721 .unwrap_or(Ordering::Equal)
722 } else {
723 Ordering::Less
724 }
725 } else if b.material_cost.is_some() {
726 Ordering::Greater
727 } else {
728 Ordering::Equal
729 }
730 });
731 if PRICING_DEBUG {
732 for i in recipes.iter() {
733 tracing::debug!("{:?}", *i);
734 }
735 }
736 recipes
738 .first()
739 .filter(|recipe| recipe.material_cost.is_some())
740 .is_some()
741 }
742
743 fn read() -> Self {
744 let mut result = Self::default();
745 let mut freq = FreqEntries::default();
746 let price_config =
747 TradingPriceFile::load_expect("common.trading.item_price_calculation").read();
748 result.equality_set = EqualitySet::load_expect("common.trading.item_price_equality")
749 .read()
750 .clone();
751 for table in &price_config.loot_tables {
752 if PRICING_DEBUG {
753 info!(?table);
754 }
755 let (frequency, can_sell, asset_path) = table;
756 let loot = ProbabilityFile::load_expect(asset_path);
757 for (p, item_asset, amount) in &loot.read().content {
758 let good = Self::good_from_item(item_asset);
759 let scaling = get_scaling(&price_config, good);
760 freq.add(
761 &result.equality_set,
762 item_asset,
763 good,
764 frequency * p * *amount * scaling,
765 *can_sell,
766 );
767 }
768 }
769 freq.add(
770 &result.equality_set,
771 &ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
772 Good::Coin,
773 get_scaling(&price_config, Good::Coin),
774 true,
775 );
776 result.items.0.extend(freq.0.iter().map(|elem| {
778 if elem.freq.0.is_empty() {
779 let canonical_name = result.equality_set.canonical(&elem.name);
781 let can_freq = freq.0.iter().find(|i| &i.name == canonical_name);
782 can_freq
783 .map(|e| PriceEntry {
784 name: elem.name.clone(),
785 price: MaterialUse::from(e.freq.clone()),
786 sell: elem.sell && e.sell,
787 stackable: elem.stackable,
788 })
789 .unwrap_or(PriceEntry {
790 name: elem.name.clone(),
791 price: MaterialUse::from(elem.freq.clone()),
792 sell: elem.sell,
793 stackable: elem.stackable,
794 })
795 } else {
796 PriceEntry {
797 name: elem.name.clone(),
798 price: MaterialUse::from(elem.freq.clone()),
799 sell: elem.sell,
800 stackable: elem.stackable,
801 }
802 }
803 }));
804 if PRICING_DEBUG {
805 for i in result.items.0.iter() {
806 tracing::debug!("before recipes {:?}", *i);
807 }
808 }
809
810 let mut secondaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
812 let book = complete_recipe_book().read();
813 let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
814 for (_, recipe) in book.iter() {
815 let (ref asset_path, amount) = recipe.output;
816 if let ItemKind::ModularComponent(
817 inventory::item::modular::ModularComponent::ToolSecondaryComponent {
818 toolkind,
819 stats: _,
820 hand_restriction: _,
821 },
822 ) = asset_path.kind
823 {
824 secondaries
825 .entry(toolkind)
826 .or_default()
827 .push(ItemDefinitionIdOwned::Simple(asset_path.id().into()));
828 }
829 ordered_recipes.push(RememberedRecipe {
830 output: ItemDefinitionIdOwned::Simple(asset_path.id().into()),
831 amount,
832 material_cost: None,
833 input: recipe
834 .inputs
835 .iter()
836 .filter_map(|&(ref recipe_input, count, _)| {
837 if let RecipeInput::Item(it) = recipe_input {
838 if count == 0 {
840 None
841 } else {
842 Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
843 }
844 } else {
845 None
846 }
847 })
848 .collect(),
849 });
850 }
851
852 let mut primaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
854 let comp_book = default_component_recipe_book().read();
855 for (key, recipe) in comp_book.iter() {
856 primaries
857 .entry(key.toolkind)
858 .or_default()
859 .push(recipe.itemdef_output());
860 ordered_recipes.push(RememberedRecipe {
861 output: recipe.itemdef_output(),
862 amount: 1,
863 material_cost: None,
864 input: recipe
865 .inputs()
866 .filter_map(|(ref recipe_input, count)| {
867 if count == 0 {
868 None
869 } else {
870 match recipe_input {
871 RecipeInput::Item(it) => {
872 Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
873 },
874 RecipeInput::Tag(_) => todo!(),
875 RecipeInput::TagSameItem(_) => todo!(),
876 RecipeInput::ListSameItem(_) => todo!(),
877 }
878 }
879 })
880 .collect(),
881 });
882 }
883
884 for (kind, mut primary_vec) in primaries.drain() {
886 for primary in primary_vec.drain(0..) {
887 for secondary in secondaries[&kind].iter() {
888 let input = vec![(primary.clone(), 1), (secondary.clone(), 1)];
889 let components = vec![primary.clone(), secondary.clone()];
890 let output = ItemDefinitionIdOwned::Modular {
891 pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
892 components,
893 };
894 ordered_recipes.push(RememberedRecipe {
895 output,
896 amount: 1,
897 material_cost: None,
898 input,
899 });
900 }
901 }
902 }
903 drop(secondaries);
904
905 let ability_map = &AbilityMap::load().read();
908 let msm = &MaterialStatManifest::load().read();
909 while result.sort_by_price(&mut ordered_recipes) {
910 ordered_recipes.retain(|recipe| {
911 if recipe.material_cost.is_some_and(|p| p < 1e-5) || recipe.amount == 0 {
912 false
914 } else if recipe.material_cost.is_some() {
915 let actual_cost = result.calculate_material_cost(recipe);
916 if let Some(usage) = actual_cost {
917 let output_tradeable = recipe.input.iter().all(|(input, _)| {
918 result
919 .items
920 .0
921 .iter()
922 .find(|item| item.name == *input)
923 .is_some_and(|item| item.sell)
924 });
925 let stackable = Item::new_from_item_definition_id(
926 recipe.output.as_ref(),
927 ability_map,
928 msm,
929 )
930 .is_ok_and(|i| i.is_stackable());
931 let new_entry = PriceEntry {
932 name: recipe.output.clone(),
933 price: usage * (1.0 / (recipe.amount as f32 * Self::CRAFTING_FACTOR)),
934 sell: output_tradeable,
935 stackable,
936 };
937 if PRICING_DEBUG {
938 tracing::trace!("Recipe {:?}", new_entry);
939 }
940 result.items.add_alternative(new_entry);
941 } else {
942 error!("Recipe {:?} incomplete confusion", recipe);
943 }
944 false
945 } else {
946 true
948 }
949 });
950 }
952 result
953 }
954
955 fn random_items_impl(
957 &self,
958 stockmap: &mut HashMap<Good, f32>,
959 mut number: u32,
960 selling: bool,
961 always_coin: bool,
962 limit: u32,
963 ) -> Vec<(ItemDefinitionIdOwned, u32)> {
964 let mut candidates: Vec<&PriceEntry> = self
965 .items
966 .0
967 .iter()
968 .filter(|i| {
969 let excess = i
970 .price
971 .iter()
972 .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
973 excess.is_none()
974 && (!selling || i.sell)
975 && (!always_coin
976 || i.name != ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()))
977 })
978 .collect();
979 let mut result = Vec::new();
980 if always_coin && number > 0 {
981 let amount = stockmap.get(&Good::Coin).copied().unwrap_or_default() as u32;
982 if amount > 0 {
983 result.push((
984 ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
985 amount,
986 ));
987 number -= 1;
988 }
989 }
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 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 ) -> Vec<(ItemDefinitionIdOwned, u32)> {
1041 TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit)
1042 }
1043
1044 #[must_use]
1045 pub fn get_materials(item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1046 TRADE_PRICING.get_materials_impl(item)
1047 }
1048
1049 #[cfg(test)]
1050 fn instance() -> &'static Self { &TRADE_PRICING }
1051
1052 #[cfg(test)]
1053 fn print_sorted(&self) {
1054 use crate::comp::item::{DurabilityMultiplier, armor}; println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
1057
1058 fn more_information(i: &Item, p: f32) -> (String, &'static str) {
1059 let msm = &MaterialStatManifest::load().read();
1060 let durability_multiplier = DurabilityMultiplier(1.0);
1061
1062 if let ItemKind::Armor(a) = &*i.kind() {
1063 (
1064 match a.stats(msm, durability_multiplier).protection {
1065 Some(armor::Protection::Invincible) => "Invincible".into(),
1066 Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
1067 None => "0.0".into(),
1068 },
1069 "prot/val",
1070 )
1071 } else if let ItemKind::Tool(t) = &*i.kind() {
1072 let stats = t.stats(durability_multiplier);
1073 (format!("{:.4}", stats.power * stats.speed * p), "dps/val")
1074 } else if let ItemKind::Consumable { kind: _, effects } = &*i.kind() {
1075 (
1076 effects
1077 .effects()
1078 .iter()
1079 .map(|e| {
1080 if let crate::effect::Effect::Buff(b) = e {
1081 format!("{:.2}", b.data.strength * p)
1082 } else {
1083 format!("{:?}", e)
1084 }
1085 })
1086 .collect::<Vec<String>>()
1087 .join(" "),
1088 "str/val",
1089 )
1090 } else {
1091 (Default::default(), "")
1092 }
1093 }
1094 let mut sorted: Vec<(f32, &PriceEntry)> = self
1095 .items
1096 .0
1097 .iter()
1098 .map(|e| (e.price.iter().map(|i| i.0.to_owned()).sum(), e))
1099 .collect();
1100 sorted.sort_by(|(p, e), (p2, e2)| {
1101 p2.partial_cmp(p)
1102 .unwrap_or(Ordering::Equal)
1103 .then(e.name.cmp(&e2.name))
1104 });
1105
1106 for (
1107 pricesum,
1108 PriceEntry {
1109 name: item_id,
1110 price: mat_use,
1111 sell: can_sell,
1112 stackable: _,
1113 },
1114 ) in sorted.iter()
1115 {
1116 Item::new_from_item_definition_id(
1117 item_id.as_ref(),
1118 &AbilityMap::load().read(),
1119 &MaterialStatManifest::load().read(),
1120 )
1121 .ok()
1122 .map(|it| {
1123 let prob = 1.0 / pricesum;
1125 let (info, unit) = more_information(&it, prob);
1126 let materials = mat_use
1127 .iter()
1128 .fold(String::new(), |agg, i| agg + &format!("{:?}.", i.1));
1129 println!(
1130 "{:?}, {}, {:>4.2}, {}, {:?}, {}, {},",
1131 &item_id,
1132 if *can_sell { "yes" } else { "no" },
1133 pricesum,
1134 materials,
1135 it.quality(),
1136 info,
1137 unit,
1138 );
1139 });
1140 }
1141 }
1142}
1143
1144#[must_use]
1146pub fn expand_loot_table(loot_table: &str) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
1147 ProbabilityFile::from(vec![(1.0, LootSpec::LootTable(loot_table.into()))]).content
1148}
1149
1150#[cfg(test)]
1153mod tests {
1154 use crate::{comp::inventory::trade_pricing::TradePricing, trade::Good};
1155 use tracing::{Level, info};
1156 use tracing_subscriber::{FmtSubscriber, filter::EnvFilter};
1157
1158 fn init() {
1159 FmtSubscriber::builder()
1160 .with_max_level(Level::ERROR)
1161 .with_env_filter(EnvFilter::from_default_env())
1162 .try_init()
1163 .unwrap_or(());
1164 }
1165
1166 #[test]
1167 fn test_prices1() {
1168 init();
1169 info!("init");
1170
1171 TradePricing::instance().print_sorted();
1172 }
1173
1174 #[test]
1175 fn test_prices2() {
1176 init();
1177 info!("init");
1178
1179 let mut stock: hashbrown::HashMap<Good, f32> = [
1180 (Good::Ingredients, 50.0),
1181 (Good::Tools, 10.0),
1182 (Good::Armor, 10.0),
1183 ]
1185 .iter()
1186 .copied()
1187 .collect();
1188
1189 let loadout = TradePricing::random_items(&mut stock, 20, false, false, 999);
1190 for i in loadout.iter() {
1191 info!("Random item {:?}*{}", i.0, i.1);
1192 }
1193 }
1194}