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