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, 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
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 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 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 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 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 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 result.items.0.extend(freq.0.iter().map(|elem| {
765 if elem.freq.0.is_empty() {
766 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 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 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 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 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 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 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 true
935 }
936 });
937 }
939 result
940 }
941
942 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 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 let mut result = Vec::new();
974 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 {
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 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}; 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 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#[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#[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 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 vec![]
1207 },
1208 LootSpec::ModularWeaponPrimaryComponent { .. } => {
1209 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 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 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 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 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 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 ]
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}