1use crate::{
2 assets::{self, AssetExt, AssetHandle, CacheCombined, Concatenate},
3 comp::{
4 Inventory, Item,
5 inventory::slot::{InvSlotId, Slot},
6 item::{
7 ItemBase, ItemDef, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, ItemTag,
8 MaterialStatManifest, modular,
9 tool::{AbilityMap, ToolKind},
10 },
11 },
12 terrain::SpriteKind,
13};
14use hashbrown::HashMap;
15use serde::{Deserialize, Serialize};
16use std::{borrow::Cow, sync::Arc};
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub enum RecipeInput {
20 Item(Arc<ItemDef>),
22 Tag(ItemTag),
24 TagSameItem(ItemTag),
31 ListSameItem(Vec<Arc<ItemDef>>),
40}
41
42impl RecipeInput {
43 fn handle_requirement<'a, I: Iterator<Item = InvSlotId>>(
44 &'a self,
45 amount: u32,
46 slot_claims: &mut HashMap<InvSlotId, u32>,
47 unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
48 inv: &Inventory,
49 input_slots: I,
50 ) {
51 let mut required = amount;
52 let contains_any = input_slots.into_iter().all(|slot| {
56 if let Some(item) = inv
58 .get(slot)
59 .filter(|item| item.matches_recipe_input(self, amount))
60 {
61 let claimed = slot_claims.entry(slot).or_insert(0);
64 let available = item.amount().saturating_sub(*claimed);
65 let provided = available.min(required);
66 required -= provided;
67 *claimed += provided;
68 true
69 } else {
70 false
71 }
72 });
73 if required > 0 || !contains_any {
76 unsatisfied_requirements.push((self, required));
77 }
78 }
79}
80
81#[derive(Clone, Debug, Serialize, Deserialize)]
82pub struct Recipe {
83 pub output: (Arc<ItemDef>, u32),
84 pub inputs: Vec<(RecipeInput, u32, bool)>,
87 pub craft_sprite: Option<SpriteKind>,
88}
89
90impl Recipe {
91 pub fn craft_simple(
93 &self,
94 inv: &mut Inventory,
95 slots: Vec<(u32, InvSlotId)>,
97 ability_map: &AbilityMap,
98 msm: &MaterialStatManifest,
99 ) -> Result<Vec<Item>, Vec<(&RecipeInput, u32)>> {
100 let mut slot_claims = HashMap::new();
101 let mut unsatisfied_requirements = Vec::new();
102 let mut component_slots = Vec::new();
103
104 self.inputs.iter().enumerate().for_each(
110 |(i, &(ref input, ref amount, mut is_component))| {
111 let mut required = *amount;
112 let mut contains_any = false;
115 let input_slots = slots
117 .iter()
118 .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None });
119 for slot in input_slots {
121 if let Some(item) = inv
123 .get(*slot)
124 .filter(|item| item.matches_recipe_input(input, *amount))
125 {
126 let claimed = slot_claims.entry(*slot).or_insert(0);
129 let available = item.amount().saturating_sub(*claimed);
130 let provided = available.min(required);
131 required -= provided;
132 *claimed += provided;
133 if provided > 0 && is_component {
137 component_slots.push(*slot);
138 is_component = false;
139 }
140 contains_any = true;
141 }
142 }
143 if required > 0 || !contains_any {
146 unsatisfied_requirements.push((input, required));
147 }
148 },
149 );
150
151 if unsatisfied_requirements.is_empty() {
155 let mut components = Vec::new();
156 for slot in component_slots.iter() {
157 let component = inv
158 .take(*slot, ability_map, msm)
159 .expect("Expected item to exist in the inventory");
160 components.push(component);
161 let to_remove = slot_claims
162 .get_mut(slot)
163 .expect("If marked in component slots, should be in slot claims");
164 *to_remove -= 1;
165 }
166 for (slot, to_remove) in slot_claims.iter() {
167 for _ in 0..*to_remove {
168 let _ = inv
169 .take(*slot, ability_map, msm)
170 .expect("Expected item to exist in the inventory");
171 }
172 }
173 let (item_def, quantity) = &self.output;
174
175 let crafted_item = Item::new_from_item_base(
176 ItemBase::Simple(Arc::clone(item_def)),
177 components,
178 ability_map,
179 msm,
180 );
181 let mut crafted_items = Vec::with_capacity(*quantity as usize);
182 for _ in 0..*quantity {
183 crafted_items.push(crafted_item.duplicate(ability_map, msm));
184 }
185 Ok(crafted_items)
186 } else {
187 Err(unsatisfied_requirements)
188 }
189 }
190
191 pub fn inputs(&self) -> impl ExactSizeIterator<Item = (&RecipeInput, u32, bool)> {
192 self.inputs
193 .iter()
194 .map(|(item_def, amount, is_mod_comp)| (item_def, *amount, *is_mod_comp))
195 }
196
197 pub fn inventory_contains_ingredients(
203 &self,
204 inv: &Inventory,
205 recipe_amount: u32,
206 ) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
207 inventory_contains_ingredients(
208 self.inputs()
209 .map(|(input, amount, _is_modular)| (input, amount)),
210 inv,
211 recipe_amount,
212 )
213 }
214
215 pub fn max_from_ingredients(&self, inv: &Inventory) -> u32 {
218 let mut max_recipes = None;
219
220 for (input, amount) in self
221 .inputs()
222 .map(|(input, amount, _is_modular)| (input, amount))
223 {
224 let needed = amount as f32;
225 let mut input_max = HashMap::<InvSlotId, u32>::new();
226
227 for (inv_slot_id, slot) in inv.slots_with_id() {
230 if let Some(item) = slot
231 .as_ref()
232 .filter(|item| item.matches_recipe_input(input, amount))
233 {
234 *input_max.entry(inv_slot_id).or_insert(0) += item.amount();
235 }
236 }
237
238 let max_item_proportion =
241 ((input_max.values().sum::<u32>() as f32) / needed).floor() as u32;
242 max_recipes = Some(match max_recipes {
243 None => max_item_proportion,
244 Some(max_recipes) if (max_item_proportion < max_recipes) => max_item_proportion,
245 Some(n) => n,
246 });
247 }
248
249 max_recipes.unwrap_or(0)
250 }
251}
252
253fn inventory_contains_ingredients<'a, I: Iterator<Item = (&'a RecipeInput, u32)>>(
260 ingredients: I,
261 inv: &Inventory,
262 recipe_amount: u32,
263) -> Result<Vec<(u32, InvSlotId)>, Vec<(&'a RecipeInput, u32)>> {
264 let mut slot_claims = HashMap::<InvSlotId, u32>::new();
267 let mut slots = Vec::<(u32, InvSlotId)>::new();
270 let mut missing = Vec::<(&RecipeInput, u32)>::new();
272
273 for (i, (input, amount)) in ingredients.enumerate() {
274 let mut needed = amount * recipe_amount;
275 let mut contains_any = false;
276 for (inv_slot_id, slot) in inv.slots_with_id() {
279 if let Some(item) = slot
280 .as_ref()
281 .filter(|item| item.matches_recipe_input(input, amount))
282 {
283 let claim = slot_claims.entry(inv_slot_id).or_insert(0);
284 slots.push((i as u32, inv_slot_id));
285 let can_claim = (item.amount().saturating_sub(*claim)).min(needed);
286 *claim += can_claim;
287 needed -= can_claim;
288 contains_any = true;
289 }
290 }
291
292 if needed > 0 || !contains_any {
293 missing.push((input, needed));
294 }
295 }
296
297 if missing.is_empty() {
298 Ok(slots)
299 } else {
300 Err(missing)
301 }
302}
303
304pub enum SalvageError {
305 NotSalvageable,
306}
307
308pub fn try_salvage(
309 inv: &mut Inventory,
310 slot: InvSlotId,
311 ability_map: &AbilityMap,
312 msm: &MaterialStatManifest,
313) -> Result<Vec<Item>, SalvageError> {
314 if inv.get(slot).is_some_and(|item| item.is_salvageable()) {
315 let salvage_item = inv.get(slot).expect("Expected item to exist in inventory");
316 let salvage_output: Vec<_> = salvage_item
317 .salvage_output()
318 .flat_map(|(material, quantity)| {
319 std::iter::repeat_n(Item::new_from_asset_expect(material), quantity as usize)
320 })
321 .collect();
322 if salvage_output.is_empty() {
323 Err(SalvageError::NotSalvageable)
328 } else {
329 let _ = inv
331 .take(slot, ability_map, msm)
332 .expect("Expected item to exist in inventory");
333 Ok(salvage_output)
335 }
336 } else {
337 Err(SalvageError::NotSalvageable)
338 }
339}
340
341pub enum ModularWeaponError {
342 InvalidSlot,
343 ComponentMismatch,
344 DifferentTools,
345 DifferentHands,
346}
347
348pub fn modular_weapon(
349 inv: &mut Inventory,
350 primary_component: InvSlotId,
351 secondary_component: InvSlotId,
352 ability_map: &AbilityMap,
353 msm: &MaterialStatManifest,
354) -> Result<Item, ModularWeaponError> {
355 use modular::ModularComponent;
356 fn unwrap_modular(inv: &Inventory, slot: InvSlotId) -> Option<Cow<ModularComponent>> {
358 inv.get(slot).and_then(|item| match item.kind() {
359 Cow::Owned(ItemKind::ModularComponent(mod_comp)) => Some(Cow::Owned(mod_comp)),
360 Cow::Borrowed(ItemKind::ModularComponent(mod_comp)) => Some(Cow::Borrowed(mod_comp)),
361 _ => None,
362 })
363 }
364
365 let compatiblity = if let (Some(primary_component), Some(secondary_component)) = (
368 unwrap_modular(inv, primary_component),
369 unwrap_modular(inv, secondary_component),
370 ) {
371 if let (
374 ModularComponent::ToolPrimaryComponent {
375 toolkind: tool_a,
376 hand_restriction: hands_a,
377 ..
378 },
379 ModularComponent::ToolSecondaryComponent {
380 toolkind: tool_b,
381 hand_restriction: hands_b,
382 ..
383 },
384 ) = (&*primary_component, &*secondary_component)
385 {
386 if tool_a == tool_b {
388 let hands_check = hands_a.zip(*hands_b).is_none_or(|(a, b)| a == b);
390 if hands_check {
391 Ok(())
392 } else {
393 Err(ModularWeaponError::DifferentHands)
394 }
395 } else {
396 Err(ModularWeaponError::DifferentTools)
397 }
398 } else {
399 Err(ModularWeaponError::ComponentMismatch)
400 }
401 } else {
402 Err(ModularWeaponError::InvalidSlot)
403 };
404
405 match compatiblity {
406 Ok(()) => {
407 let primary_component = inv
409 .take(primary_component, ability_map, msm)
410 .expect("Expected component to exist");
411 let secondary_component = inv
412 .take(secondary_component, ability_map, msm)
413 .expect("Expected component to exist");
414
415 Ok(Item::new_from_item_base(
417 ItemBase::Modular(modular::ModularBase::Tool),
418 vec![primary_component, secondary_component],
419 ability_map,
420 msm,
421 ))
422 },
423 Err(err) => Err(err),
424 }
425}
426
427#[derive(Clone, Debug, Serialize, Deserialize)]
428pub struct RecipeBookManifest {
429 recipes: HashMap<String, Recipe>,
430}
431
432impl RecipeBookManifest {
433 pub fn load() -> AssetHandle<Self> { Self::load_expect("common.recipe_book_manifest") }
434
435 pub fn get(&self, recipe: &str) -> Option<&Recipe> { self.recipes.get(recipe) }
436
437 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&String, &Recipe)> { self.recipes.iter() }
438
439 pub fn keys(&self) -> impl ExactSizeIterator<Item = &String> { self.recipes.keys() }
440
441 pub fn get_available(&self, inv: &Inventory) -> Vec<(String, Recipe)> {
442 self.recipes
443 .iter()
444 .filter(|(_, recipe)| recipe.inventory_contains_ingredients(inv, 1).is_ok())
445 .map(|(name, recipe)| (name.clone(), recipe.clone()))
446 .collect()
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn complete_recipe_book_valid_key_check() {
456 let recipe_book = complete_recipe_book().read();
457 let is_invalid_key =
458 |input: &str| input.chars().any(|c| c.is_uppercase() || c.is_whitespace());
459 assert!(!recipe_book.iter().any(|(k, _)| is_invalid_key(k)));
460 }
461}
462
463#[derive(Clone, Debug, Serialize, Deserialize)]
464pub enum RawRecipeInput {
465 Item(String),
466 Tag(ItemTag),
467 TagSameItem(ItemTag),
468 ListSameItem(String),
469}
470
471impl RawRecipeInput {
472 fn load_recipe_input(&self) -> Result<RecipeInput, assets::Error> {
473 let input = match self {
474 RawRecipeInput::Item(name) => RecipeInput::Item(Arc::<ItemDef>::load_cloned(name)?),
475 RawRecipeInput::Tag(tag) => RecipeInput::Tag(*tag),
476 RawRecipeInput::TagSameItem(tag) => RecipeInput::TagSameItem(*tag),
477 RawRecipeInput::ListSameItem(list) => {
478 let assets = &ItemList::load_expect(list).read().0;
479 let items = assets
480 .iter()
481 .map(|asset| Arc::<ItemDef>::load_expect_cloned(asset))
482 .collect();
483 RecipeInput::ListSameItem(items)
484 },
485 };
486 Ok(input)
487 }
488}
489
490#[derive(Clone, Deserialize)]
491pub(crate) struct RawRecipe {
492 pub(crate) output: (String, u32),
493 pub(crate) inputs: Vec<(RawRecipeInput, u32, bool)>,
496 pub(crate) craft_sprite: Option<SpriteKind>,
497}
498
499#[derive(Clone, Deserialize)]
500#[serde(transparent)]
501pub(crate) struct RawRecipeBook(pub(crate) HashMap<String, RawRecipe>);
502
503impl assets::Asset for RawRecipeBook {
504 type Loader = assets::RonLoader;
505
506 const EXTENSION: &'static str = "ron";
507}
508impl Concatenate for RawRecipeBook {
509 fn concatenate(self, b: Self) -> Self { RawRecipeBook(self.0.concatenate(b.0)) }
510}
511
512#[derive(Deserialize, Clone)]
513struct ItemList(Vec<String>);
514
515impl assets::Asset for ItemList {
516 type Loader = assets::RonLoader;
517
518 const EXTENSION: &'static str = "ron";
519}
520
521impl assets::Compound for RecipeBookManifest {
522 fn load(
523 cache: assets::AnyCache,
524 specifier: &assets::SharedString,
525 ) -> Result<Self, assets::BoxedError> {
526 fn load_item_def(spec: &(String, u32)) -> Result<(Arc<ItemDef>, u32), assets::Error> {
527 let def = Arc::<ItemDef>::load_cloned(&spec.0)?;
528 Ok((def, spec.1))
529 }
530
531 fn load_recipe_input(
532 (input, amount, is_mod_comp): &(RawRecipeInput, u32, bool),
533 ) -> Result<(RecipeInput, u32, bool), assets::Error> {
534 let def = input.load_recipe_input()?;
535 Ok((def, *amount, *is_mod_comp))
536 }
537
538 let raw = cache.load_and_combine::<RawRecipeBook>(specifier)?.cloned();
539
540 let recipes = raw
541 .0
542 .iter()
543 .map(
544 |(
545 name,
546 RawRecipe {
547 output,
548 inputs,
549 craft_sprite,
550 },
551 )| {
552 let inputs = inputs
553 .iter()
554 .map(load_recipe_input)
555 .collect::<Result<Vec<_>, _>>()?;
556 let output = load_item_def(output)?;
557 Ok((name.clone(), Recipe {
558 output,
559 inputs,
560 craft_sprite: *craft_sprite,
561 }))
562 },
563 )
564 .collect::<Result<_, assets::Error>>()?;
565
566 Ok(RecipeBookManifest { recipes })
567 }
568}
569
570#[derive(Clone, Debug, Serialize, Deserialize)]
571pub struct ComponentRecipeBook {
572 recipes: HashMap<ComponentKey, ComponentRecipe>,
573}
574
575#[derive(Clone, Debug)]
576pub struct ReverseComponentRecipeBook {
577 recipes: HashMap<ItemDefinitionIdOwned, ComponentRecipe>,
578}
579
580impl ComponentRecipeBook {
581 pub fn get(&self, key: &ComponentKey) -> Option<&ComponentRecipe> { self.recipes.get(key) }
582
583 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&ComponentKey, &ComponentRecipe)> {
584 self.recipes.iter()
585 }
586}
587
588impl ReverseComponentRecipeBook {
589 pub fn get(&self, key: &ItemDefinitionIdOwned) -> Option<&ComponentRecipe> {
590 self.recipes.get(key)
591 }
592}
593
594#[derive(Clone, Deserialize)]
595#[serde(transparent)]
596struct RawComponentRecipeBook(Vec<RawComponentRecipe>);
597
598impl assets::Asset for RawComponentRecipeBook {
599 type Loader = assets::RonLoader;
600
601 const EXTENSION: &'static str = "ron";
602}
603impl Concatenate for RawComponentRecipeBook {
604 fn concatenate(self, b: Self) -> Self { RawComponentRecipeBook(self.0.concatenate(b.0)) }
605}
606
607#[derive(Clone, Debug, Serialize, Deserialize, Hash, Eq, PartialEq)]
608pub struct ComponentKey {
609 pub toolkind: ToolKind,
613 pub material: String,
615 pub modifier: Option<String>,
617}
618
619#[derive(Clone, Debug, Serialize, Deserialize)]
620pub struct ComponentRecipe {
621 pub recipe_book_key: String,
622 output: ComponentOutput,
623 material: (RecipeInput, u32),
624 modifier: Option<(RecipeInput, u32)>,
625 additional_inputs: Vec<(RecipeInput, u32)>,
626 pub craft_sprite: Option<SpriteKind>,
627}
628
629impl ComponentRecipe {
630 pub fn craft_component(
633 &self,
634 inv: &mut Inventory,
635 material_slot: InvSlotId,
636 modifier_slot: Option<InvSlotId>,
637 slots: Vec<(u32, InvSlotId)>,
639 ability_map: &AbilityMap,
640 msm: &MaterialStatManifest,
641 ) -> Result<Vec<Item>, Vec<(&RecipeInput, u32)>> {
642 let mut slot_claims = HashMap::new();
643 let mut unsatisfied_requirements = Vec::new();
644
645 self.material.0.handle_requirement(
651 self.material.1,
652 &mut slot_claims,
653 &mut unsatisfied_requirements,
654 inv,
655 core::iter::once(material_slot),
656 );
657 if let Some((modifier_input, modifier_amount)) = &self.modifier {
658 modifier_input.handle_requirement(
661 *modifier_amount,
662 &mut slot_claims,
663 &mut unsatisfied_requirements,
664 inv,
665 core::iter::once(modifier_slot.unwrap_or(InvSlotId::new(0, 0))),
666 );
667 }
668 self.additional_inputs
669 .iter()
670 .enumerate()
671 .for_each(|(i, (input, amount))| {
672 let input_slots = slots
674 .iter()
675 .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
676 .copied();
677 input.handle_requirement(
679 *amount,
680 &mut slot_claims,
681 &mut unsatisfied_requirements,
682 inv,
683 input_slots,
684 );
685 });
686
687 if unsatisfied_requirements.is_empty() {
691 for (slot, to_remove) in slot_claims.iter() {
692 for _ in 0..*to_remove {
693 let _ = inv
694 .take(*slot, ability_map, msm)
695 .expect("Expected item to exist in the inventory");
696 }
697 }
698
699 let crafted_item = self.item_output(ability_map, msm);
700
701 Ok(vec![crafted_item])
702 } else {
703 Err(unsatisfied_requirements)
704 }
705 }
706
707 pub fn inventory_contains_additional_ingredients(
712 &self,
713 inv: &Inventory,
714 ) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
715 inventory_contains_ingredients(
716 self.additional_inputs
717 .iter()
718 .map(|(input, amount)| (input, *amount)),
719 inv,
720 1,
721 )
722 }
723
724 pub fn itemdef_output(&self) -> ItemDefinitionIdOwned {
725 match &self.output {
726 ComponentOutput::ItemComponents {
727 item: item_def,
728 components,
729 } => {
730 let components = components
731 .iter()
732 .map(|item_def| ItemDefinitionIdOwned::Simple(item_def.id().to_owned()))
733 .collect::<Vec<_>>();
734 ItemDefinitionIdOwned::Compound {
735 simple_base: item_def.id().to_owned(),
736 components,
737 }
738 },
739 }
740 }
741
742 pub fn item_output(&self, ability_map: &AbilityMap, msm: &MaterialStatManifest) -> Item {
743 match &self.output {
744 ComponentOutput::ItemComponents {
745 item: item_def,
746 components,
747 } => {
748 let components = components
749 .iter()
750 .map(|item_def| {
751 Item::new_from_item_base(
752 ItemBase::Simple(Arc::clone(item_def)),
753 Vec::new(),
754 ability_map,
755 msm,
756 )
757 })
758 .collect::<Vec<_>>();
759 Item::new_from_item_base(
760 ItemBase::Simple(Arc::clone(item_def)),
761 components,
762 ability_map,
763 msm,
764 )
765 },
766 }
767 }
768
769 pub fn inputs(&self) -> impl ExactSizeIterator<Item = (&RecipeInput, u32)> {
770 self.into_iter().map(|(recipe, amount)| (recipe, *amount))
771 }
772}
773
774pub struct ComponentRecipeInputsIterator<'a> {
775 material: Option<&'a (RecipeInput, u32)>,
776 modifier: Option<&'a (RecipeInput, u32)>,
777 additional_inputs: std::slice::Iter<'a, (RecipeInput, u32)>,
778}
779
780impl<'a> Iterator for ComponentRecipeInputsIterator<'a> {
781 type Item = &'a (RecipeInput, u32);
782
783 fn next(&mut self) -> Option<&'a (RecipeInput, u32)> {
784 self.material
785 .take()
786 .or_else(|| self.modifier.take())
787 .or_else(|| self.additional_inputs.next())
788 }
789}
790
791impl<'a> IntoIterator for &'a ComponentRecipe {
792 type IntoIter = ComponentRecipeInputsIterator<'a>;
793 type Item = &'a (RecipeInput, u32);
794
795 fn into_iter(self) -> Self::IntoIter {
796 ComponentRecipeInputsIterator {
797 material: Some(&self.material),
798 modifier: self.modifier.as_ref(),
799 additional_inputs: self.additional_inputs.as_slice().iter(),
800 }
801 }
802}
803
804impl ExactSizeIterator for ComponentRecipeInputsIterator<'_> {
805 fn len(&self) -> usize {
806 self.material.is_some() as usize
807 + self.modifier.is_some() as usize
808 + self.additional_inputs.len()
809 }
810}
811
812#[derive(Clone, Deserialize)]
813struct RawComponentRecipe {
814 recipe_book_key: String,
815 output: RawComponentOutput,
816 material: (String, u32),
818 modifier: Option<(String, u32)>,
820 additional_inputs: Vec<(RawRecipeInput, u32)>,
821 craft_sprite: Option<SpriteKind>,
822}
823
824#[derive(Clone, Debug, Serialize, Deserialize)]
825enum ComponentOutput {
826 ItemComponents {
829 item: Arc<ItemDef>,
830 components: Vec<Arc<ItemDef>>,
831 },
832}
833
834#[derive(Clone, Debug, Serialize, Deserialize)]
835enum RawComponentOutput {
836 ToolPrimaryComponent { toolkind: ToolKind, item: String },
839}
840
841impl assets::Compound for ComponentRecipeBook {
842 fn load(
843 cache: assets::AnyCache,
844 specifier: &assets::SharedString,
845 ) -> Result<Self, assets::BoxedError> {
846 fn create_recipe_key(raw_recipe: &RawComponentRecipe) -> ComponentKey {
847 match &raw_recipe.output {
848 RawComponentOutput::ToolPrimaryComponent { toolkind, item: _ } => {
849 let material = String::from(&raw_recipe.material.0);
850 let modifier = raw_recipe
851 .modifier
852 .as_ref()
853 .map(|(modifier, _amount)| String::from(modifier));
854 ComponentKey {
855 toolkind: *toolkind,
856 material,
857 modifier,
858 }
859 },
860 }
861 }
862
863 fn load_recipe(raw_recipe: &RawComponentRecipe) -> Result<ComponentRecipe, assets::Error> {
864 let output = match &raw_recipe.output {
865 RawComponentOutput::ToolPrimaryComponent { toolkind: _, item } => {
866 let item = Arc::<ItemDef>::load_cloned(item)?;
867 let components = vec![Arc::<ItemDef>::load_cloned(&raw_recipe.material.0)?];
868 ComponentOutput::ItemComponents { item, components }
869 },
870 };
871 let material = (
872 RecipeInput::Item(Arc::<ItemDef>::load_cloned(&raw_recipe.material.0)?),
873 raw_recipe.material.1,
874 );
875 let modifier = if let Some((modifier, amount)) = &raw_recipe.modifier {
876 let modifier = Arc::<ItemDef>::load_cloned(modifier)?;
877 Some((RecipeInput::Item(modifier), *amount))
878 } else {
879 None
880 };
881 let additional_inputs = raw_recipe
882 .additional_inputs
883 .iter()
884 .map(|(input, amount)| input.load_recipe_input().map(|input| (input, *amount)))
885 .collect::<Result<Vec<_>, _>>()?;
886 let recipe_book_key = String::from(&raw_recipe.recipe_book_key);
887 Ok(ComponentRecipe {
888 recipe_book_key,
889 output,
890 material,
891 modifier,
892 additional_inputs,
893 craft_sprite: raw_recipe.craft_sprite,
894 })
895 }
896
897 let raw = cache
898 .load_and_combine::<RawComponentRecipeBook>(specifier)?
899 .cloned();
900
901 let recipes = raw
902 .0
903 .iter()
904 .map(|raw_recipe| {
905 load_recipe(raw_recipe).map(|recipe| (create_recipe_key(raw_recipe), recipe))
906 })
907 .collect::<Result<_, assets::Error>>()?;
908
909 Ok(ComponentRecipeBook { recipes })
910 }
911}
912
913#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Debug)]
914enum RepairKey {
915 ItemDefId(String),
916 ModularWeapon { material: String },
917}
918
919impl RepairKey {
920 fn from_item(item: &Item) -> Option<Self> {
921 match item.item_definition_id() {
922 ItemDefinitionId::Simple(item_id) => Some(Self::ItemDefId(String::from(item_id))),
923 ItemDefinitionId::Compound { .. } => None,
924 ItemDefinitionId::Modular { pseudo_base, .. } => match pseudo_base {
925 "veloren.core.pseudo_items.modular.tool" => {
926 if let Some(ItemDefinitionId::Simple(material)) = item
927 .components()
928 .iter()
929 .find(|comp| {
930 matches!(
931 &*comp.kind(),
932 ItemKind::ModularComponent(
933 modular::ModularComponent::ToolPrimaryComponent { .. }
934 )
935 )
936 })
937 .and_then(|comp| {
938 comp.components()
939 .iter()
940 .next()
941 .map(|comp| comp.item_definition_id())
942 })
943 {
944 let material = String::from(material);
945 Some(Self::ModularWeapon { material })
946 } else {
947 None
948 }
949 },
950 _ => None,
951 },
952 }
953 }
954}
955
956#[derive(Serialize, Deserialize, Clone)]
957struct RawRepairRecipe {
958 inputs: Vec<(RawRecipeInput, u32)>,
959}
960
961#[derive(Serialize, Deserialize, Clone)]
962struct RawRepairRecipeBook {
963 recipes: HashMap<RepairKey, RawRepairRecipe>,
964 fallback: RawRepairRecipe,
965}
966
967impl assets::Asset for RawRepairRecipeBook {
968 type Loader = assets::RonLoader;
969
970 const EXTENSION: &'static str = "ron";
971}
972
973#[derive(Serialize, Deserialize, Clone, Debug)]
974pub struct RepairRecipe {
975 inputs: Vec<(RecipeInput, u32)>,
976}
977
978impl RepairRecipe {
979 pub fn inventory_contains_ingredients(
985 &self,
986 item: &Item,
987 inv: &Inventory,
988 ) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
989 inventory_contains_ingredients(self.inputs(item), inv, 1)
990 }
991
992 pub fn inputs(&self, item: &Item) -> impl Iterator<Item = (&RecipeInput, u32)> + use<'_> {
993 let item_durability = item.durability_lost().unwrap_or(0);
994 self.inputs
995 .iter()
996 .filter_map(move |(input, original_amount)| {
997 let amount = (original_amount * item_durability) / Item::MAX_DURABILITY;
998 if *original_amount > 0 && amount == 0 {
1001 None
1002 } else {
1003 Some((input, amount))
1004 }
1005 })
1006 }
1007}
1008
1009#[derive(Serialize, Deserialize, Clone, Debug)]
1010pub struct RepairRecipeBook {
1011 recipes: HashMap<RepairKey, RepairRecipe>,
1012 fallback: RepairRecipe,
1013}
1014
1015impl RepairRecipeBook {
1016 pub fn repair_recipe(&self, item: &Item) -> Option<&RepairRecipe> {
1017 RepairKey::from_item(item)
1018 .as_ref()
1019 .and_then(|key| self.recipes.get(key))
1020 .or_else(|| item.has_durability().then_some(&self.fallback))
1021 }
1022
1023 pub fn repair_item(
1024 &self,
1025 inv: &mut Inventory,
1026 item: Slot,
1027 slots: Vec<(u32, InvSlotId)>,
1028 ability_map: &AbilityMap,
1029 msm: &MaterialStatManifest,
1030 ) -> Result<(), Vec<(&RecipeInput, u32)>> {
1031 let mut slot_claims = HashMap::new();
1032 let mut unsatisfied_requirements = Vec::new();
1033
1034 if let Some(item) = match item {
1035 Slot::Equip(slot) => inv.equipped(slot),
1036 Slot::Inventory(slot) => inv.get(slot),
1037 Slot::Overflow(_) => None,
1039 } {
1040 if let Some(repair_recipe) = self.repair_recipe(item) {
1041 repair_recipe
1042 .inputs(item)
1043 .enumerate()
1044 .for_each(|(i, (input, amount))| {
1045 let input_slots = slots
1047 .iter()
1048 .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
1049 .copied();
1050 input.handle_requirement(
1052 amount,
1053 &mut slot_claims,
1054 &mut unsatisfied_requirements,
1055 inv,
1056 input_slots,
1057 );
1058 })
1059 }
1060 }
1061
1062 if unsatisfied_requirements.is_empty() {
1063 for (slot, to_remove) in slot_claims.iter() {
1064 for _ in 0..*to_remove {
1065 let _ = inv
1066 .take(*slot, ability_map, msm)
1067 .expect("Expected item to exist in the inventory");
1068 }
1069 }
1070
1071 inv.repair_item_at_slot(item, ability_map, msm);
1072
1073 Ok(())
1074 } else {
1075 Err(unsatisfied_requirements)
1076 }
1077 }
1078}
1079
1080impl assets::Compound for RepairRecipeBook {
1081 fn load(
1082 cache: assets::AnyCache,
1083 specifier: &assets::SharedString,
1084 ) -> Result<Self, assets::BoxedError> {
1085 fn load_recipe_input(
1086 (input, amount): &(RawRecipeInput, u32),
1087 ) -> Result<(RecipeInput, u32), assets::Error> {
1088 let input = input.load_recipe_input()?;
1089 Ok((input, *amount))
1090 }
1091
1092 let raw = cache.load::<RawRepairRecipeBook>(specifier)?.cloned();
1093
1094 let recipes = raw
1095 .recipes
1096 .iter()
1097 .map(|(key, RawRepairRecipe { inputs })| {
1098 let inputs = inputs
1099 .iter()
1100 .map(load_recipe_input)
1101 .collect::<Result<Vec<_>, _>>()?;
1102 Ok((key.clone(), RepairRecipe { inputs }))
1103 })
1104 .collect::<Result<_, assets::Error>>()?;
1105
1106 let fallback = RepairRecipe {
1107 inputs: raw
1108 .fallback
1109 .inputs
1110 .iter()
1111 .map(load_recipe_input)
1112 .collect::<Result<Vec<_>, _>>()?,
1113 };
1114
1115 Ok(RepairRecipeBook { recipes, fallback })
1116 }
1117}
1118
1119pub fn complete_recipe_book() -> AssetHandle<RecipeBookManifest> {
1120 RecipeBookManifest::load_expect("common.recipe_book_manifest")
1121}
1122
1123pub fn default_component_recipe_book() -> AssetHandle<ComponentRecipeBook> {
1124 ComponentRecipeBook::load_expect("common.component_recipe_book")
1125}
1126
1127pub fn default_repair_recipe_book() -> AssetHandle<RepairRecipeBook> {
1128 RepairRecipeBook::load_expect("common.repair_recipe_book")
1129}
1130
1131impl assets::Compound for ReverseComponentRecipeBook {
1132 fn load(
1133 cache: assets::AnyCache,
1134 specifier: &assets::SharedString,
1135 ) -> Result<Self, assets::BoxedError> {
1136 let forward = cache.load::<ComponentRecipeBook>(specifier)?.cloned();
1137 let mut recipes = HashMap::new();
1138 for (_, recipe) in forward.iter() {
1139 recipes.insert(recipe.itemdef_output(), recipe.clone());
1140 }
1141 Ok(ReverseComponentRecipeBook { recipes })
1142 }
1143}