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 pub 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 ItemDefinitionIdOwned::Simple(name)
613 if name.starts_with("common.items.crafting_ing.") =>
614 {
615 Good::Ingredients
616 },
617 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.mineral.") => {
618 Good::Ingredients
619 },
620 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.log.") => {
621 Good::Wood
622 },
623 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.flowers.") => {
624 Good::Ingredients
625 },
626 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.consumable.") => {
627 Good::Potions
628 },
629 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.charms.") => {
630 Good::Potions
631 },
632 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.food.") => {
633 Good::Food
634 },
635 ItemDefinitionIdOwned::Simple(name) if name.as_str() == Self::COIN_ITEM => Good::Coin,
636 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.recipes.") => {
637 Good::Recipe
638 },
639 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.glider.") => {
640 Good::Tools
641 },
642 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.utility.") => {
643 Good::default()
644 },
645 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.boss_drops.") => {
646 Good::Tools
647 },
648 ItemDefinitionIdOwned::Simple(name)
649 if name.starts_with("common.items.crafting_tools.") =>
650 {
651 Good::default()
652 },
653 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.lantern.") => {
654 Good::Tools
655 },
656 ItemDefinitionIdOwned::Simple(name) if name.starts_with("common.items.keys.") => {
657 Good::Tools
658 },
659 ItemDefinitionIdOwned::Modular {
660 pseudo_base: _,
661 components: _,
662 } => Good::Tools,
663 ItemDefinitionIdOwned::Compound {
664 simple_base: _,
665 components: _,
666 } => Good::Tools,
667 _ => {
668 warn!("unknown loot item {:?}", name);
669 Good::default()
670 },
671 }
672 }
673
674 fn price_lookup(&self, requested_name: &ItemDefinitionIdOwned) -> Option<&MaterialUse> {
676 let canonical_name = self.equality_set.canonical(requested_name);
677 self.items
678 .0
679 .iter()
680 .find(|e| &e.name == canonical_name)
681 .map(|e| &e.price)
682 }
683
684 fn calculate_material_cost(&self, r: &RememberedRecipe) -> Option<MaterialUse> {
685 r.input
686 .iter()
687 .map(|(name, amount)| {
688 self.price_lookup(name).map(|x| {
689 x.clone()
690 * (if *amount > 0 {
691 *amount as f32
692 } else {
693 Self::INVEST_FACTOR
694 })
695 })
696 })
697 .try_fold(MaterialUse::default(), |acc, elem| Some(acc + elem?))
698 }
699
700 fn calculate_material_cost_sum(&self, r: &RememberedRecipe) -> Option<f32> {
701 self.calculate_material_cost(r)?
702 .iter()
703 .fold(None, |acc, elem| Some(acc.unwrap_or_default() + elem.0))
704 }
705
706 fn sort_by_price(&self, recipes: &mut [RememberedRecipe]) -> bool {
709 for recipe in recipes.iter_mut() {
710 recipe.material_cost = self.calculate_material_cost_sum(recipe);
711 }
712 recipes.sort_by(|a, b| {
714 if a.material_cost.is_some() {
715 if b.material_cost.is_some() {
716 a.material_cost
717 .partial_cmp(&b.material_cost)
718 .unwrap_or(Ordering::Equal)
719 } else {
720 Ordering::Less
721 }
722 } else if b.material_cost.is_some() {
723 Ordering::Greater
724 } else {
725 Ordering::Equal
726 }
727 });
728 if PRICING_DEBUG {
729 for i in recipes.iter() {
730 tracing::debug!("{:?}", *i);
731 }
732 }
733 recipes
735 .first()
736 .filter(|recipe| recipe.material_cost.is_some())
737 .is_some()
738 }
739
740 fn read() -> Self {
741 let mut result = Self::default();
742 let mut freq = FreqEntries::default();
743 let price_config =
744 Ron::<TradingPriceFile>::load_expect("common.trading.item_price_calculation").read();
745 result.equality_set = EqualitySet::load_expect("common.trading.item_price_equality")
746 .read()
747 .clone();
748 for table in &price_config.0.loot_tables {
749 if PRICING_DEBUG {
750 info!(?table);
751 }
752 let (frequency, can_sell, asset_path) = table;
753 let loot = ProbabilityFile::load_expect(asset_path);
754 for (p, item_asset, amount) in &loot.read().content {
755 let good = Self::good_from_item(item_asset);
756 let scaling = get_scaling(&price_config, good);
757 freq.add(
758 &result.equality_set,
759 item_asset,
760 good,
761 frequency * p * *amount * scaling,
762 *can_sell,
763 );
764 }
765 }
766 freq.add(
767 &result.equality_set,
768 &ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
769 Good::Coin,
770 get_scaling(&price_config, Good::Coin),
771 true,
772 );
773 result.items.0.extend(freq.0.iter().map(|elem| {
775 if elem.freq.0.is_empty() {
776 let canonical_name = result.equality_set.canonical(&elem.name);
778 let can_freq = freq.0.iter().find(|i| &i.name == canonical_name);
779 can_freq
780 .map(|e| PriceEntry {
781 name: elem.name.clone(),
782 price: MaterialUse::from(e.freq.clone()),
783 sell: elem.sell && e.sell,
784 stackable: elem.stackable,
785 })
786 .unwrap_or(PriceEntry {
787 name: elem.name.clone(),
788 price: MaterialUse::from(elem.freq.clone()),
789 sell: elem.sell,
790 stackable: elem.stackable,
791 })
792 } else {
793 PriceEntry {
794 name: elem.name.clone(),
795 price: MaterialUse::from(elem.freq.clone()),
796 sell: elem.sell,
797 stackable: elem.stackable,
798 }
799 }
800 }));
801 if PRICING_DEBUG {
802 for i in result.items.0.iter() {
803 tracing::debug!("before recipes {:?}", *i);
804 }
805 }
806
807 let mut secondaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
809 let book = complete_recipe_book().read();
810 let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
811 for (_, recipe) in book.iter() {
812 let (ref asset_path, amount) = recipe.output;
813 if let ItemKind::ModularComponent(
814 inventory::item::modular::ModularComponent::ToolSecondaryComponent {
815 toolkind,
816 stats: _,
817 hand_restriction: _,
818 },
819 ) = asset_path.kind
820 {
821 secondaries
822 .entry(toolkind)
823 .or_default()
824 .push(ItemDefinitionIdOwned::Simple(asset_path.id().into()));
825 }
826 ordered_recipes.push(RememberedRecipe {
827 output: ItemDefinitionIdOwned::Simple(asset_path.id().into()),
828 amount,
829 material_cost: None,
830 input: recipe
831 .inputs
832 .iter()
833 .filter_map(|&(ref recipe_input, count, _)| {
834 if let RecipeInput::Item(it) = recipe_input {
835 if count == 0 {
837 None
838 } else {
839 Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
840 }
841 } else {
842 None
843 }
844 })
845 .collect(),
846 });
847 }
848
849 let mut primaries: HashMap<ToolKind, Vec<ItemDefinitionIdOwned>> = HashMap::new();
851 let comp_book = default_component_recipe_book().read();
852 for (key, recipe) in comp_book.iter() {
853 primaries
854 .entry(key.toolkind)
855 .or_default()
856 .push(recipe.itemdef_output());
857 ordered_recipes.push(RememberedRecipe {
858 output: recipe.itemdef_output(),
859 amount: 1,
860 material_cost: None,
861 input: recipe
862 .inputs()
863 .filter_map(|(ref recipe_input, count)| {
864 if count == 0 {
865 None
866 } else {
867 match recipe_input {
868 RecipeInput::Item(it) => {
869 Some((ItemDefinitionIdOwned::Simple(it.id().into()), count))
870 },
871 RecipeInput::Tag(_) => todo!(),
872 RecipeInput::TagSameItem(_) => todo!(),
873 RecipeInput::ListSameItem(_) => todo!(),
874 }
875 }
876 })
877 .collect(),
878 });
879 }
880
881 for (kind, mut primary_vec) in primaries.drain() {
883 for primary in primary_vec.drain(0..) {
884 for secondary in secondaries[&kind].iter() {
885 let input = vec![(primary.clone(), 1), (secondary.clone(), 1)];
886 let components = vec![primary.clone(), secondary.clone()];
887 let output = ItemDefinitionIdOwned::Modular {
888 pseudo_base: ModularBase::Tool.pseudo_item_id().into(),
889 components,
890 };
891 ordered_recipes.push(RememberedRecipe {
892 output,
893 amount: 1,
894 material_cost: None,
895 input,
896 });
897 }
898 }
899 }
900 drop(secondaries);
901
902 let ability_map = &AbilityMap::load().read();
905 let msm = &MaterialStatManifest::load().read();
906 while result.sort_by_price(&mut ordered_recipes) {
907 ordered_recipes.retain(|recipe| {
908 if recipe.material_cost.is_some_and(|p| p < 1e-5) || recipe.amount == 0 {
909 false
911 } else if recipe.material_cost.is_some() {
912 let actual_cost = result.calculate_material_cost(recipe);
913 if let Some(usage) = actual_cost {
914 let output_tradeable = recipe.input.iter().all(|(input, _)| {
915 result
916 .items
917 .0
918 .iter()
919 .find(|item| item.name == *input)
920 .is_some_and(|item| item.sell)
921 });
922 let stackable = Item::new_from_item_definition_id(
923 recipe.output.as_ref(),
924 ability_map,
925 msm,
926 )
927 .is_ok_and(|i| i.is_stackable());
928 let new_entry = PriceEntry {
929 name: recipe.output.clone(),
930 price: usage * (1.0 / (recipe.amount as f32 * Self::CRAFTING_FACTOR)),
931 sell: output_tradeable,
932 stackable,
933 };
934 if PRICING_DEBUG {
935 tracing::trace!("Recipe {:?}", new_entry);
936 }
937 result.items.add_alternative(new_entry);
938 } else {
939 error!("Recipe {:?} incomplete confusion", recipe);
940 }
941 false
942 } else {
943 true
945 }
946 });
947 }
949 result
950 }
951
952 fn random_items_impl(
954 &self,
955 stockmap: &mut HashMap<Good, f32>,
956 mut number: u32,
957 selling: bool,
958 always_coin: bool,
959 limit: u32,
960 mut permitted: impl FnMut(Good) -> bool,
961 ) -> Vec<(ItemDefinitionIdOwned, u32)> {
962 let mut candidates: Vec<&PriceEntry> = self
967 .items
968 .0
969 .iter()
970 .filter(|i| {
971 let excess = i
972 .price
973 .iter()
974 .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
975 permitted(TradePricing::good_from_item(&i.name))
976 && excess.is_none()
977 && (!selling || i.sell)
978 && (!always_coin
979 || i.name != ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()))
980 })
981 .collect();
982 let mut result = Vec::new();
984 if always_coin && number > 0 {
986 let amount = stockmap.get(&Good::Coin).copied().unwrap_or_default() as u32;
987 if amount > 0 {
988 result.push((
989 ItemDefinitionIdOwned::Simple(Self::COIN_ITEM.into()),
990 amount,
991 ));
992 number -= 1;
993 }
994 }
995 for _ in 0..number {
1001 candidates.retain(|i| {
1002 let excess = i
1003 .price
1004 .iter()
1005 .find(|j| j.0 >= stockmap.get(&j.1).cloned().unwrap_or_default());
1006 excess.is_none()
1007 });
1008 if candidates.is_empty() {
1009 break;
1010 }
1011 let index = (rand::random::<f32>() * candidates.len() as f32).floor() as usize;
1012 let result2 = candidates[index];
1013 let amount: u32 = if result2.stackable {
1014 let max_amount = result2
1015 .price
1016 .iter()
1017 .map(|e| {
1018 stockmap
1019 .get_mut(&e.1)
1020 .map(|stock| *stock / e.0.max(0.001))
1021 .unwrap_or_default()
1022 })
1023 .fold(f32::INFINITY, f32::min)
1024 .min(limit as f32);
1025 (rand::random::<f32>() * (max_amount - 1.0)).floor() as u32 + 1
1026 } else {
1027 1
1028 };
1029 for i in result2.price.iter() {
1030 stockmap.get_mut(&i.1).map(|v| *v -= i.0 * (amount as f32));
1031 }
1032 result.push((result2.name.clone(), amount));
1033 candidates.remove(index);
1035 }
1036 result
1037 }
1038
1039 fn get_materials_impl(&self, item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1040 self.price_lookup(&item.to_owned()).cloned()
1041 }
1042
1043 #[must_use]
1044 pub fn random_items(
1045 stock: &mut HashMap<Good, f32>,
1046 number: u32,
1047 selling: bool,
1048 always_coin: bool,
1049 limit: u32,
1050 permitted: impl FnMut(Good) -> bool,
1051 ) -> Vec<(ItemDefinitionIdOwned, u32)> {
1052 TRADE_PRICING.random_items_impl(stock, number, selling, always_coin, limit, permitted)
1053 }
1054
1055 #[must_use]
1056 pub fn get_materials(item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
1057 TRADE_PRICING.get_materials_impl(item)
1058 }
1059
1060 #[cfg(test)]
1061 fn instance() -> &'static Self { &TRADE_PRICING }
1062
1063 #[cfg(test)]
1064 fn print_sorted(&self) {
1065 use crate::comp::item::{DurabilityMultiplier, armor}; println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
1068
1069 fn more_information(i: &Item, p: f32) -> (String, &'static str) {
1070 let msm = &MaterialStatManifest::load().read();
1071 let durability_multiplier = DurabilityMultiplier(1.0);
1072
1073 if let ItemKind::Armor(a) = &*i.kind() {
1074 (
1075 match a.stats(msm, durability_multiplier).protection {
1076 Some(armor::Protection::Invincible) => "Invincible".into(),
1077 Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
1078 None => "0.0".into(),
1079 },
1080 "prot/val",
1081 )
1082 } else if let ItemKind::Tool(t) = &*i.kind() {
1083 let stats = t.stats(durability_multiplier);
1084 (format!("{:.4}", stats.power * stats.speed * p), "dps/val")
1085 } else if let ItemKind::Consumable {
1086 kind: _,
1087 effects,
1088 container: _,
1089 } = &*i.kind()
1090 {
1091 (
1092 effects
1093 .effects()
1094 .iter()
1095 .map(|e| {
1096 if let crate::effect::Effect::Buff(b) = e {
1097 format!("{:.2}", b.data.strength * p)
1098 } else {
1099 format!("{:?}", e)
1100 }
1101 })
1102 .collect::<Vec<String>>()
1103 .join(" "),
1104 "str/val",
1105 )
1106 } else {
1107 (Default::default(), "")
1108 }
1109 }
1110 let mut sorted: Vec<(f32, &PriceEntry)> = self
1111 .items
1112 .0
1113 .iter()
1114 .map(|e| (e.price.iter().map(|i| i.0.to_owned()).sum(), e))
1115 .collect();
1116 sorted.sort_by(|(p, e), (p2, e2)| {
1117 p2.partial_cmp(p)
1118 .unwrap_or(Ordering::Equal)
1119 .then(e.name.cmp(&e2.name))
1120 });
1121
1122 for (
1123 pricesum,
1124 PriceEntry {
1125 name: item_id,
1126 price: mat_use,
1127 sell: can_sell,
1128 stackable: _,
1129 },
1130 ) in sorted.iter()
1131 {
1132 Item::new_from_item_definition_id(
1133 item_id.as_ref(),
1134 &AbilityMap::load().read(),
1135 &MaterialStatManifest::load().read(),
1136 )
1137 .ok()
1138 .map(|it| {
1139 let prob = 1.0 / pricesum;
1141 let (info, unit) = more_information(&it, prob);
1142 let materials = mat_use
1143 .iter()
1144 .fold(String::new(), |agg, i| agg + &format!("{:?}.", i.1));
1145 println!(
1146 "{:?}, {}, {:>4.2}, {}, {:?}, {}, {},",
1147 &item_id,
1148 if *can_sell { "yes" } else { "no" },
1149 pricesum,
1150 materials,
1151 it.quality(),
1152 info,
1153 unit,
1154 );
1155 });
1156 }
1157 }
1158}
1159
1160#[must_use]
1162pub fn expand_loot_table(loot_table: &str) -> Vec<(f32, ItemDefinitionIdOwned, f32)> {
1163 ProbabilityFile::from(vec![(1.0, LootSpec::LootTable(loot_table.into()))]).content
1164}
1165
1166#[cfg(test)]
1169mod tests {
1170 use crate::{comp::inventory::trade_pricing::TradePricing, trade::Good};
1171 use tracing::{Level, info};
1172 use tracing_subscriber::{FmtSubscriber, filter::EnvFilter};
1173
1174 fn init() {
1175 FmtSubscriber::builder()
1176 .with_max_level(Level::ERROR)
1177 .with_env_filter(EnvFilter::from_default_env())
1178 .try_init()
1179 .unwrap_or(());
1180 }
1181
1182 #[test]
1183 fn test_prices1() {
1184 init();
1185 info!("init");
1186
1187 TradePricing::instance().print_sorted();
1188 }
1189
1190 #[test]
1191 fn test_prices2() {
1192 init();
1193 info!("init");
1194
1195 let mut stock: hashbrown::HashMap<Good, f32> = [
1196 (Good::Ingredients, 50.0),
1197 (Good::Tools, 10.0),
1198 (Good::Armor, 10.0),
1199 ]
1201 .iter()
1202 .copied()
1203 .collect();
1204
1205 let loadout = TradePricing::random_items(&mut stock, 20, false, false, 999, |_| true);
1206 for i in loadout.iter() {
1207 info!("Random item {:?}*{}", i.0, i.1);
1208 }
1209 }
1210}