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