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#[derive(Default, Debug, Clone)]
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
51fn 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#[derive(Default, Debug, Clone)]
100pub struct MaterialFrequency(Vec<(f32, Good)>);
101
102fn 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
128impl 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 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 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 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 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)]
255pub 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 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; 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 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
324pub 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 pub loot_tables: Vec<(f32, bool, String)>,
472 pub good_scaling: Vec<(Good, f32)>,
474}
475
476#[derive(Clone, Debug, Default)]
477struct EqualitySet {
478 equivalence_class: HashMap<ItemDefinitionIdOwned, ItemDefinitionIdOwned>,
480}
481
482impl EqualitySet {
483 fn canonical<'a>(&'a self, item_name: &'a ItemDefinitionIdOwned) -> &'a ItemDefinitionIdOwned {
484 (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 const INVEST_FACTOR: f32 = 0.33;
594
595 fn good_from_item(name: &ItemDefinitionIdOwned) -> Good {
596 match name {
597 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.armor.") => {
598 Good::Armor
599 },
600
601 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.weapons.") => {
602 Good::Tools
603 },
604 ItemDefinitionIdOwned::Simple(name)
605 if name.starts_with("common.items.modular.weapon.") =>
606 {
607 Good::Tools
608 },
609 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.tool.") => {
610 Good::Tools
611 },
612
613 ItemDefinitionIdOwned::Simple(name)
614 if name.starts_with("common.items.crafting_ing.") =>
615 {
616 Good::Ingredients
617 },
618 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.mineral.") => {
619 Good::Ingredients
620 },
621 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.log.") => {
622 Good::Wood
623 },
624 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.flowers.") => {
625 Good::Ingredients
626 },
627 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.consumable.") => {
628 Good::Potions
629 },
630 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.food.") => {
631 Good::Food
632 },
633 ItemDefinitionIdOwned::Simple(name) if name.as_str() == Self::COIN_ITEM => Good::Coin,
634 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.recipes.") => {
635 Good::Recipe
636 },
637 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.glider.") => {
638 Good::Tools
639 },
640 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.utility.") => {
641 Good::default()
642 },
643 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.boss_drops.") => {
644 Good::Tools
645 },
646 ItemDefinitionIdOwned::Simple(name)
647 if name.starts_with("common.items.crafting_tools.") =>
648 {
649 Good::default()
650 },
651 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.lantern.") => {
652 Good::Tools
653 },
654 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.keys.") => {
655 Good::Tools
656 },
657 ItemDefinitionIdOwned::Modular {
658 pseudo_base: _,
659 components: _,
660 } => Good::Tools,
661 ItemDefinitionIdOwned::Compound {
662 simple_base: _,
663 components: _,
664 } => Good::Ingredients,
665 _ => {
666 warn!("unknown loot item {:?}", name);
667 Good::default()
668 },
669 }
670 }
671
672 fn price_lookup(&self, requested_name: &ItemDefinitionIdOwned) -> Option<&MaterialUse> {
674 let canonical_name = self.equality_set.canonical(requested_name);
675 self.items
676 .0
677 .iter()
678 .find(|e| &e.name == canonical_name)
679 .map(|e| &e.price)
680 }
681
682 fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
683 r.input
684 .iter()
685 .map(|(name, amount)| {
686 self.price_lookup(name).map(|x| {
687 x.clone()
688 * (if *amount > 0 {
689 *amount as f32
690 } else {
691 Self::INVEST_FACTOR
692 })
693 })
694 })
695 .try_fold(MaterialUse::default(), |acc, elem| Some(acc + elem?))
696 }
697
698 fn calculate_material_cost_sum(&self, r: &RememberedRecipe) -> Option<f32> {
699 self.calculate_material_cost(r)?
700 .iter()
701 .fold(None, |acc, elem| Some(acc.unwrap_or_default() + elem.0))
702 }
703
704 fn sort_by_price(&self, recipes: &mut [RememberedRecipe]) -> bool {
707 for recipe in recipes.iter_mut() {
708 recipe.material_cost = self.calculate_material_cost_sum(recipe);
709 }
710 recipes.sort_by(|a, b| {
712 if a.material_cost.is_some() {
713 if b.material_cost.is_some() {
714 a.material_cost
715 .partial_cmp(&b.material_cost)
716 .unwrap_or(Ordering::Equal)
717 } else {
718 Ordering::Less
719 }
720 } else if b.material_cost.is_some() {
721 Ordering::Greater
722 } else {
723 Ordering::Equal
724 }
725 });
726 if PRICING_DEBUG {
727 for i in recipes.iter() {
728 tracing::debug!("{:?}", *i);
729 }
730 }
731 recipes
733 .first()
734 .filter(|recipe| recipe.material_cost.is_some())
735 .is_some()
736 }
737
738 fn read() -> Self {
739 let mut result = Self::default();
740 let mut freq = FreqEntries::default();
741 let price_config =
742 Ron::<TradingPriceFile>::load_expect("common.trading.item_price_calculation").read();
743 result.equality_set = EqualitySet::load_expect("common.trading.item_price_equality")
744 .read()
745 .clone();
746 for table in &price_config.0.loot_tables {
747 if PRICING_DEBUG {
748 info!(?table);
749 }
750 let (frequency, can_sell, asset_path) = table;
751 let loot = ProbabilityFile::load_expect(asset_path);
752 for (p, item_asset, amount) in &loot.read().content {
753 let good = Self::good_from_item(item_asset);
754 let scaling = get_scaling(&price_config, good);
755 freq.add(
756 &result.equality_set,
757 item_asset,
758 good,
759 frequency * p * *amount * scaling,
760 *can_sell,
761 );
762 }
763 }
764 freq.add(
765 &result.equality_set,
766 &ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
767 Good::Coin,
768 get_scaling(&price_config, Good::Coin),
769 true,
770 );
771 result.items.0.extend(freq.0.iter().map(|elem| {
773 if elem.freq.0.is_empty() {
774 let canonical_name = result.equality_set.canonical(&elem.name);
776 let can_freq = freq.0.iter().find(|i| &i.name == canonical_name);
777 can_freq
778 .map(|e| PriceEntry {
779 name: elem.name.clone(),
780 price: MaterialUse::from(e.freq.clone()),
781 sell: elem.sell && e.sell,
782 stackable: elem.stackable,
783 })
784 .unwrap_or(PriceEntry {
785 name: elem.name.clone(),
786 price: MaterialUse::from(elem.freq.clone()),
787 sell: elem.sell,
788 stackable: elem.stackable,
789 })
790 } else {
791 PriceEntry {
792 name: elem.name.clone(),
793 price: MaterialUse::from(elem.freq.clone()),
794 sell: elem.sell,
795 stackable: elem.stackable,
796 }
797 }
798 }));
799 if PRICING_DEBUG {
800 for i in result.items.0.iter() {
801 tracing::debug!("before recipes {:?}", *i);
802 }
803 }
804
805 let mut secondaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
807 let book = complete_recipe_book().read();
808 let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
809 for (_, recipe) in book.iter() {
810 let (ref asset_path, amount) = recipe.output;
811 if let ItemKind::ModularComponent(
812 inventory::item::modular::ModularComponent::ToolSecondaryComponent {
813 toolkind,
814 stats: _,
815 hand_restriction: _,
816 },
817 ) = asset_path.kind
818 {
819 secondaries
820 .entry(toolkind)
821 .or_default()
822 .push(ItemDefinitionIdOwned::Simple(asset_path.id().into()));
823 }
824 ordered_recipes.push(RememberedRecipe {
825 output: ItemDefinitionIdOwned::Simple(asset_path.id().into()),
826 amount,
827 material_cost: None,
828 input: recipe
829 .inputs
830 .iter()
831 .filter_map(|&(ref recipe_input, count, _)| {
832 if let RecipeInput::Item(it) = recipe_input {
833 if count == 0 {
835 None
836 } else {
837 Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
838 }
839 } else {
840 None
841 }
842 })
843 .collect(),
844 });
845 }
846
847 let mut primaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
849 let comp_book = default_component_recipe_book().read();
850 for (key, recipe) in comp_book.iter() {
851 primaries
852 .entry(key.toolkind)
853 .or_default()
854 .push(recipe.itemdef_output());
855 ordered_recipes.push(RememberedRecipe {
856 output: recipe.itemdef_output(),
857 amount: 1,
858 material_cost: None,
859 input: recipe
860 .inputs()
861 .filter_map(|(ref recipe_input, count)| {
862 if count == 0 {
863 None
864 } else {
865 match recipe_input {
866 RecipeInput::Item(it) => {
867 Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
868 },
869 RecipeInput::Tag(_) => todo!(),
870 RecipeInput::TagSameItem(_) => todo!(),
871 RecipeInput::ListSameItem(_) => todo!(),
872 }
873 }
874 })
875 .collect(),
876 });
877 }
878
879 for (kind, mut primary_vec) in primaries.drain() {
881 for primary in primary_vec.drain(0..) {
882 for secondary in secondaries[&kind].iter() {
883 let input = vec![(primary.clone(), 1), (secondary.clone(), 1)];
884 let components = vec![primary.clone(), secondary.clone()];
885 let output = ItemDefinitionIdOwned::Modular {
886 pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
887 components,
888 };
889 ordered_recipes.push(RememberedRecipe {
890 output,
891 amount: 1,
892 material_cost: None,
893 input,
894 });
895 }
896 }
897 }
898 drop(secondaries);
899
900 let ability_map = &AbilityMap::load().read();
903 let msm = &MaterialStatManifest::load().read();
904 while result.sort_by_price(&mut ordered_recipes) {
905 ordered_recipes.retain(|recipe| {
906 if recipe.material_cost.is_some_and(|p| p < 1e-5) || recipe.amount == 0 {
907 false
909 } else if recipe.material_cost.is_some() {
910 let actual_cost = result.calculate_material_cost(recipe);
911 if let Some(usage) = actual_cost {
912 let output_tradeable = recipe.input.iter().all(|(input, _)| {
913 result
914 .items
915 .0
916 .iter()
917 .find(|item| item.name == *input)
918 .is_some_and(|item| item.sell)
919 });
920 let stackable = Item::new_from_item_definition_id(
921 recipe.output.as_ref(),
922 ability_map,
923 msm,
924 )
925 .is_ok_and(|i| i.is_stackable());
926 let new_entry = PriceEntry {
927 name: recipe.output.clone(),
928 price: usage * (1.0 / (recipe.amount as f32 * Self::CRAFTING_FACTOR)),
929 sell: output_tradeable,
930 stackable,
931 };
932 if PRICING_DEBUG {
933 tracing::trace!("Recipe {:?}", new_entry);
934 }
935 result.items.add_alternative(new_entry);
936 } else {
937 error!("Recipe {:?} incomplete confusion", recipe);
938 }
939 false
940 } else {
941 true
943 }
944 });
945 }
947 result
948 }
949
950 fn random_items_impl(
952 &self,
953 stockmap: &mut HashMap<Good, f32>,
954 mut number: u32,
955 selling: bool,
956 always_coin: bool,
957 limit: u32,
958 ) -> Vec<(ItemDefinitionIdOwned, u32)> {
959 let mut candidates: Vec<&PriceEntry> = self
960 .items
961 .0
962 .iter()
963 .filter(|i| {
964 let excess = i
965 .price
966 .iter()
967 .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
968 excess.is_none()
969 && (!selling || i.sell)
970 && (!always_coin
971 || i.name != ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()))
972 })
973 .collect();
974 let mut result = Vec::new();
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 for _ in 0..number {
986 candidates.retain(|i| {
987 let excess = i
988 .price
989 .iter()
990 .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
991 excess.is_none()
992 });
993 if candidates.is_empty() {
994 break;
995 }
996 let index = (rand::random::<f32>() * candidates.len() as f32).floor() as usize;
997 let result2 = candidates[index];
998 let amount: u32 = if result2.stackable {
999 let max_amount = result2
1000 .price
1001 .iter()
1002 .map(|e| {
1003 stockmap
1004 .get_mut(&e.1)
1005 .map(|stock| *stock / e.0.max(0.001))
1006 .unwrap_or_default()
1007 })
1008 .fold(f32::INFINITY, f32::min)
1009 .min(limit as f32);
1010 (rand::random::<f32>() * (max_amount - 1.0)).floor() as u32 + 1
1011 } else {
1012 1
1013 };
1014 for i in result2.price.iter() {
1015 stockmap.get_mut(&i.1).map(|v| *v -= i.0 * (amount as f32));
1016 }
1017 result.push((result2.name.clone(), amount));
1018 candidates.remove(index);
1020 }
1021 result
1022 }
1023
1024 fn get_materials_impl(&self, item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1025 self.price_lookup(&item.to_owned()).cloned()
1026 }
1027
1028 #[must_use]
1029 pub fn random_items(
1030 stock: &mut HashMap<Good, f32>,
1031 number: u32,
1032 selling: bool,
1033 always_coin: bool,
1034 limit: u32,
1035 ) -> Vec<(ItemDefinitionIdOwned, u32)> {
1036 TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit)
1037 }
1038
1039 #[must_use]
1040 pub fn get_materials(item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1041 TRADE_PRICING.get_materials_impl(item)
1042 }
1043
1044 #[cfg(test)]
1045 fn instance() -> &'static Self { &TRADE_PRICING }
1046
1047 #[cfg(test)]
1048 fn print_sorted(&self) {
1049 use crate::comp::item::{DurabilityMultiplier, armor}; println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
1052
1053 fn more_information(i: &Item, p: f32) -> (String, &'static str) {
1054 let msm = &MaterialStatManifest::load().read();
1055 let durability_multiplier = DurabilityMultiplier(1.0);
1056
1057 if let ItemKind::Armor(a) = &*i.kind() {
1058 (
1059 match a.stats(msm, durability_multiplier).protection {
1060 Some(armor::Protection::Invincible) => "Invincible".into(),
1061 Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
1062 None => "0.0".into(),
1063 },
1064 "prot/val",
1065 )
1066 } else if let ItemKind::Tool(t) = &*i.kind() {
1067 let stats = t.stats(durability_multiplier);
1068 (format!("{:.4}", stats.power * stats.speed * p), "dps/val")
1069 } else if let ItemKind::Consumable {
1070 kind: _,
1071 effects,
1072 container: _,
1073 } = &*i.kind()
1074 {
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}