veloren_common/
recipe.rs

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    /// Only an item with a matching ItemDef can be used to satisfy this input
21    Item(Arc<ItemDef>),
22    /// Any items with this tag can be used to satisfy this input
23    Tag(ItemTag),
24    /// Similar to RecipeInput::Tag(_), but all items must be the same.
25    /// Specifically this means that a mix of different items with the tag
26    /// cannot be used.
27    /// TODO: Currently requires that all items must be in the same slot.
28    /// Eventually should be reworked so that items can be spread over multiple
29    /// slots.
30    TagSameItem(ItemTag),
31    /// List is similar to tag, but has items defined in centralized file
32    /// Similar to RecipeInput::TagSameItem(_), all items must be the same, they
33    /// cannot be a mix of different items defined in the list.
34    // Intent of using List over Tag is to make it harder for tag to be innocuously added to an
35    // item breaking a recipe
36    /// TODO: Currently requires that all items must be in the same slot.
37    /// Eventually should be reworked so that items can be spread over multiple
38    /// slots.
39    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        // contains_any check used for recipes that have an input that is not consumed,
53        // e.g. craftsman hammer
54        // Goes through each slot and marks some amount from each slot as claimed
55        let contains_any = input_slots.into_iter().all(|slot| {
56            // Checks that the item in the slot can be used for the input
57            if let Some(item) = inv
58                .get(slot)
59                .filter(|item| item.matches_recipe_input(self, amount))
60            {
61                // Gets the number of items claimed from the slot, or sets to 0 if slot has
62                // not been claimed by another input yet
63                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 there were not sufficient items to cover requirement between all provided
74        // slots, or if non-consumed item was not present, mark input as not satisfied
75        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    /// Input required for recipe, amount of input needed, whether input should
85    /// be tracked as a modular component
86    pub inputs: Vec<(RecipeInput, u32, bool)>,
87    pub craft_sprite: Option<SpriteKind>,
88}
89
90impl Recipe {
91    /// Perform a recipe, returning a list of missing items on failure
92    pub fn craft_simple(
93        &self,
94        inv: &mut Inventory,
95        // Vec tying an input to a slot
96        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        // Checks each input against slots in the inventory. If the slots contain an
105        // item that fulfills the need of the input, marks some of the item as claimed
106        // up to quantity needed for the crafting input. If the item either
107        // cannot be used, or there is insufficient quantity, adds input and
108        // number of materials needed to unsatisfied requirements.
109        self.inputs
110            .iter()
111            .enumerate()
112            .for_each(|(i, (input, amount, mut is_component))| {
113                let mut required = *amount;
114                // Check used for recipes that have an input that is not consumed, e.g.
115                // craftsman hammer
116                let mut contains_any = false;
117                // Gets all slots provided for this input by the frontend
118                let input_slots = slots
119                    .iter()
120                    .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None });
121                // Goes through each slot and marks some amount from each slot as claimed
122                for slot in input_slots {
123                    // Checks that the item in the slot can be used for the input
124                    if let Some(item) = inv
125                        .get(*slot)
126                        .filter(|item| item.matches_recipe_input(input, *amount))
127                    {
128                        // Gets the number of items claimed from the slot, or sets to 0 if slot has
129                        // not been claimed by another input yet
130                        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 input is a component and provided amount from this slot at least 1,
136                        // mark 1 piece as coming from that slot and set is_component to false to
137                        // indicate it has been claimed.
138                        if provided > 0 && is_component {
139                            component_slots.push(*slot);
140                            is_component = false;
141                        }
142                        contains_any = true;
143                    }
144                }
145                // If there were not sufficient items to cover requirement between all provided
146                // slots, or if non-consumed item was not present, mark input as not satisfied
147                if required > 0 || !contains_any {
148                    unsatisfied_requirements.push((input, required));
149                }
150            });
151
152        // If there are no unsatisfied requirements, create the items produced by the
153        // recipe in the necessary quantity and remove the items that the recipe
154        // consumes
155        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    /// Determine whether the inventory contains the ingredients for a recipe.
199    /// If it does, return a vec of inventory slots that contain the
200    /// ingredients needed, whose positions correspond to particular recipe
201    /// inputs. If items are missing, return the missing items, and how many
202    /// are missing.
203    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    /// Calculates the maximum number of items craftable given the current
217    /// inventory state.
218    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            // Checks through every slot, filtering to only those that contain items that
229            // can satisfy the input.
230            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            // Updates maximum craftable amount based on least recipe-proportional
240            // availability.
241            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
254/// Determine whether the inventory contains the ingredients for a recipe.
255/// If it does, return a vec of inventory slots that contain the
256/// ingredients needed, whose positions correspond to particular recipe
257/// inputs. If items are missing, return the missing items, and how many
258/// are missing.
259// Note: Doc comment duplicated on two public functions that call this function
260fn 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    // Hashmap tracking the quantity that needs to be removed from each slot (so
266    // that it doesn't think a slot can provide more items than it contains)
267    let mut slot_claims = HashMap::<InvSlotId, u32>::new();
268    // Important to be a vec and to remain separate from slot_claims as it must
269    // remain ordered, unlike the hashmap
270    let mut slots = Vec::<(u32, InvSlotId)>::new();
271    // The inputs to a recipe that have missing items, and the amount missing
272    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        // Checks through every slot, filtering to only those that contain items that
278        // can satisfy the input
279        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            // If no output items, assume salvaging was a failure
325            // TODO: If we ever change salvaging to have a percent chance, remove the check
326            // of outputs being empty (requires assets to exist for rock and wood materials
327            // so that salvaging doesn't silently fail)
328            Err(SalvageError::NotSalvageable)
329        } else {
330            // Remove item that is being salvaged
331            let _ = inv
332                .take(slot, ability_map, msm)
333                .expect("Expected item to exist in inventory");
334            // Return the salvaging output
335            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    // Closure to get inner modular component info from item in a given slot
358    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    // Checks if both components are compatible, and if so returns the toolkind to
367    // make weapon of
368    let compatiblity = if let (Some(primary_component), Some(secondary_component)) = (
369        unwrap_modular(inv, primary_component),
370        unwrap_modular(inv, secondary_component),
371    ) {
372        // Checks that damage and held component slots each contain a damage and held
373        // modular component respectively
374        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            // Checks that both components are of the same tool kind
388            if tool_a == tool_b {
389                // Checks that if both components have a hand restriction, they are the same
390                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            // Remove components from inventory
409            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            // Create modular weapon
417            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    /// Input required for recipe, amount of input needed, whether input should
495    /// be tracked as a modular component
496    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    // Can't use ItemDef here because hash needed, item definition id used instead
611    // TODO: Make more general for other things that have component inputs that should be tracked
612    // after item creation
613    pub toolkind: ToolKind,
614    /// Refers to the item definition id of the material
615    pub material: String,
616    /// Refers to the item definition id of the modifier
617    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    /// Craft an item that has components, returning a list of missing items on
632    /// failure
633    pub fn craft_component(
634        &self,
635        inv: &mut Inventory,
636        material_slot: InvSlotId,
637        modifier_slot: Option<InvSlotId>,
638        // Vec tying an input to a slot
639        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        // Checks each input against slots in the inventory. If the slots contain an
647        // item that fulfills the need of the input, marks some of the item as claimed
648        // up to quantity needed for the crafting input. If the item either
649        // cannot be used, or there is insufficient quantity, adds input and
650        // number of materials needed to unsatisfied requirements.
651        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            // TODO: Better way to get slot to use that ensures this requirement fails if no
660            // slot provided?
661            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                // Gets all slots provided for this input by the frontend
674                let input_slots = slots
675                    .iter()
676                    .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
677                    .copied();
678                // Checks if requirement is met, and if not marks it as unsatisfied
679                input.handle_requirement(
680                    *amount,
681                    &mut slot_claims,
682                    &mut unsatisfied_requirements,
683                    inv,
684                    input_slots,
685                );
686            });
687
688        // If there are no unsatisfied requirements, create the items produced by the
689        // recipe in the necessary quantity and remove the items that the recipe
690        // consumes
691        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    /// Determine whether the inventory contains the additional ingredients for
709    /// a component recipe. If it does, return a vec of inventory slots that
710    /// contain the ingredients needed, whose positions correspond to particular
711    /// recipe are missing.
712    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    /// String refers to an item definition id
818    material: (String, u32),
819    /// String refers to an item definition id
820    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    // TODO: Don't store list of components here in case we ever want components in future to have
828    // state to them (e.g. a component having sub-components)
829    ItemComponents {
830        item: Arc<ItemDef>,
831        components: Vec<Arc<ItemDef>>,
832    },
833}
834
835#[derive(Clone, Debug, Serialize, Deserialize)]
836enum RawComponentOutput {
837    /// Creates the primary component of a modular tool. Assumes that the
838    /// material used is the only component in the item.
839    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    /// Determine whether the inventory contains the ingredients for a repair.
981    /// If it does, return a vec of inventory slots that contain the
982    /// ingredients needed, whose positions correspond to particular repair
983    /// inputs. If items are missing, return the missing items, and how many
984    /// are missing.
985    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 repair recipe consumed ingredients, but item not damaged enough
1000                // to actually need to consume item, remove item as requirement.
1001                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            // Items in overflow slots cannot be repaired until item is moved to a real slot
1039            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                        // Gets all slots provided for this input by the frontend
1047                        let input_slots = slots
1048                            .iter()
1049                            .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
1050                            .copied();
1051                        // Checks if requirement is met, and if not marks it as unsatisfied
1052                        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}