veloren_common/comp/inventory/item/
modular.rs

1use super::{
2    DurabilityMultiplier, Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, Material, Quality,
3    ToolKind, armor,
4    tool::{self, AbilityMap, AbilitySpec, Hands, Tool},
5};
6use crate::{
7    assets::{self, Asset, AssetExt, AssetHandle},
8    recipe,
9};
10use common_base::dev_panic;
11use hashbrown::HashMap;
12use lazy_static::lazy_static;
13use rand::{Rng, prelude::SliceRandom};
14use serde::{Deserialize, Serialize};
15use std::{borrow::Cow, sync::Arc};
16
17// Macro instead of constant to work with concat! macro.
18// DO NOT CHANGE. THIS PREFIX AFFECTS PERSISTENCE AND IF CHANGED A MIGRATION
19// MUST BE PERFORMED.
20#[macro_export]
21macro_rules! modular_item_id_prefix {
22    () => {
23        "veloren.core.pseudo_items.modular."
24    };
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct MaterialStatManifest {
29    tool_stats: HashMap<String, tool::Stats>,
30    armor_stats: HashMap<String, armor::Stats>,
31}
32
33impl MaterialStatManifest {
34    pub fn load() -> AssetHandle<Self> { Self::load_expect("common.material_stats_manifest") }
35
36    pub fn armor_stats(&self, key: &str) -> Option<armor::Stats> {
37        self.armor_stats.get(key).copied()
38    }
39
40    #[doc(hidden)]
41    /// needed for tests to load it without actual assets
42    pub fn with_empty() -> Self {
43        Self {
44            tool_stats: HashMap::default(),
45            armor_stats: HashMap::default(),
46        }
47    }
48}
49
50// This could be a Compound that also loads the keys, but the RecipeBook
51// Compound impl already does that, so checking for existence here is redundant.
52impl Asset for MaterialStatManifest {
53    type Loader = assets::RonLoader;
54
55    const EXTENSION: &'static str = "ron";
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
59pub enum ModularBase {
60    Tool,
61}
62
63impl ModularBase {
64    // DO NOT CHANGE. THIS IS A PERSISTENCE RELATED FUNCTION. MUST MATCH THE
65    // FUNCTION BELOW.
66    pub fn pseudo_item_id(&self) -> &str {
67        match self {
68            ModularBase::Tool => concat!(modular_item_id_prefix!(), "tool"),
69        }
70    }
71
72    // DO NOT CHANGE. THIS IS A PERSISTENCE RELATED FUNCTION. MUST MATCH THE
73    // FUNCTION ABOVE.
74    pub fn load_from_pseudo_id(id: &str) -> Self {
75        match id {
76            concat!(modular_item_id_prefix!(), "tool") => ModularBase::Tool,
77            _ => panic!("Attempted to load a non existent pseudo item: {}", id),
78        }
79    }
80
81    fn resolve_hands(components: &[Item]) -> Hands {
82        // Checks if weapon has components that restrict hands to two. Restrictions to
83        // one hand or no restrictions default to one-handed weapon.
84        // Note: Hand restriction here will never conflict on components
85        // TODO: Maybe look into adding an assert at some point?
86        let hand_restriction = components.iter().find_map(|comp| match &*comp.kind() {
87            ItemKind::ModularComponent(mc) => match mc {
88                ModularComponent::ToolPrimaryComponent {
89                    hand_restriction, ..
90                }
91                | ModularComponent::ToolSecondaryComponent {
92                    hand_restriction, ..
93                } => *hand_restriction,
94            },
95            _ => None,
96        });
97        // In the event of no hand restrictions on the components, default to one handed
98        hand_restriction.unwrap_or(Hands::One)
99    }
100
101    #[inline(never)]
102    pub(super) fn kind(
103        &self,
104        components: &[Item],
105        msm: &MaterialStatManifest,
106        durability_multiplier: DurabilityMultiplier,
107    ) -> Cow<ItemKind> {
108        let toolkind = components
109            .iter()
110            .find_map(|comp| match &*comp.kind() {
111                ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
112                    toolkind,
113                    ..
114                }) => Some(*toolkind),
115                _ => None,
116            })
117            .unwrap_or(ToolKind::Empty);
118
119        let stats: tool::Stats = components
120            .iter()
121            .filter_map(|comp| {
122                if let ItemKind::ModularComponent(mod_comp) = &*comp.kind() {
123                    mod_comp.tool_stats(comp.components(), msm)
124                } else {
125                    None
126                }
127            })
128            .fold(tool::Stats::one(), |a, b| a * b)
129            * durability_multiplier;
130
131        match self {
132            ModularBase::Tool => Cow::Owned(ItemKind::Tool(Tool::new(
133                toolkind,
134                Self::resolve_hands(components),
135                stats,
136            ))),
137        }
138    }
139
140    /// Modular weapons are named as "{Material} {Weapon}" where {Weapon} is
141    /// from the damage component used and {Material} is from the material
142    /// the damage component is created from.
143    pub fn generate_name(&self, components: &[Item]) -> Cow<str> {
144        match self {
145            ModularBase::Tool => {
146                let name = components
147                    .iter()
148                    .find_map(|comp| match &*comp.kind() {
149                        ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
150                            weapon_name,
151                            ..
152                        }) => {
153                            let material_name = comp
154                                .components()
155                                .iter()
156                                .find_map(|mat| match mat.kind() {
157                                    #[expect(deprecated)]
158                                    Cow::Owned(ItemKind::Ingredient { descriptor, .. }) => {
159                                        Some(Cow::Owned(descriptor))
160                                    },
161                                    #[expect(deprecated)]
162                                    Cow::Borrowed(ItemKind::Ingredient { descriptor, .. }) => {
163                                        Some(Cow::Borrowed(descriptor.as_str()))
164                                    },
165                                    _ => None,
166                                })
167                                .unwrap_or_else(|| "Modular".into());
168                            Some(format!(
169                                "{} {}",
170                                material_name,
171                                weapon_name.resolve_name(Self::resolve_hands(components))
172                            ))
173                        },
174                        _ => None,
175                    })
176                    .unwrap_or_else(|| "Modular Weapon".to_owned());
177                Cow::Owned(name)
178            },
179        }
180    }
181
182    pub fn compute_quality(&self, components: &[Item]) -> Quality {
183        components
184            .iter()
185            .fold(Quality::MIN, |a, b| a.max(b.quality()))
186    }
187
188    pub fn ability_spec(&self, components: &[Item]) -> Option<Cow<AbilitySpec>> {
189        match self {
190            ModularBase::Tool => components.iter().find_map(|comp| match &*comp.kind() {
191                ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
192                    toolkind,
193                    ..
194                }) => Some(Cow::Owned(AbilitySpec::Tool(*toolkind))),
195                _ => None,
196            }),
197        }
198    }
199
200    pub fn generate_tags(&self, components: &[Item]) -> Vec<ItemTag> {
201        match self {
202            ModularBase::Tool => {
203                if let Some(comp) = components.iter().find(|comp| {
204                    matches!(
205                        &*comp.kind(),
206                        ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. })
207                    )
208                }) {
209                    if let Some(material) =
210                        comp.components()
211                            .iter()
212                            .find_map(|comp| match &*comp.kind() {
213                                ItemKind::Ingredient { .. } => {
214                                    comp.tags().into_iter().find_map(|tag| match tag {
215                                        ItemTag::Material(material) => Some(material),
216                                        _ => None,
217                                    })
218                                },
219                                _ => None,
220                            })
221                    {
222                        vec![
223                            ItemTag::Material(material),
224                            ItemTag::SalvageInto(material, 1),
225                        ]
226                    } else {
227                        Vec::new()
228                    }
229                } else {
230                    Vec::new()
231                }
232            },
233        }
234    }
235}
236
237#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
238#[serde(deny_unknown_fields)]
239pub enum ModularComponent {
240    ToolPrimaryComponent {
241        toolkind: ToolKind,
242        stats: tool::Stats,
243        hand_restriction: Option<Hands>,
244        weapon_name: WeaponName,
245    },
246    ToolSecondaryComponent {
247        toolkind: ToolKind,
248        stats: tool::Stats,
249        hand_restriction: Option<Hands>,
250    },
251}
252
253#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
254pub enum WeaponName {
255    Universal(String),
256    HandednessDependent {
257        one_handed: String,
258        two_handed: String,
259    },
260}
261
262impl WeaponName {
263    fn resolve_name(&self, handedness: Hands) -> &str {
264        match self {
265            Self::Universal(name) => name,
266            Self::HandednessDependent {
267                one_handed: name1,
268                two_handed: name2,
269            } => match handedness {
270                Hands::One => name1,
271                Hands::Two => name2,
272            },
273        }
274    }
275}
276
277impl ModularComponent {
278    pub fn tool_stats(
279        &self,
280        components: &[Item],
281        msm: &MaterialStatManifest,
282    ) -> Option<tool::Stats> {
283        match self {
284            Self::ToolPrimaryComponent { stats, .. } => {
285                let average_material_mult = components
286                    .iter()
287                    .filter_map(|comp| {
288                        comp.item_definition_id()
289                            .itemdef_id()
290                            .and_then(|id| msm.tool_stats.get(id))
291                            .copied()
292                            .zip(Some(1))
293                    })
294                    .reduce(|(stats_a, count_a), (stats_b, count_b)| {
295                        (stats_a + stats_b, count_a + count_b)
296                    })
297                    .map_or_else(tool::Stats::one, |(stats_sum, count)| {
298                        stats_sum / (count as f32)
299                    });
300
301                Some(*stats * average_material_mult)
302            },
303            Self::ToolSecondaryComponent { stats, .. } => Some(*stats),
304        }
305    }
306
307    pub fn toolkind(&self) -> Option<ToolKind> {
308        match self {
309            Self::ToolPrimaryComponent { toolkind, .. }
310            | Self::ToolSecondaryComponent { toolkind, .. } => Some(*toolkind),
311        }
312    }
313}
314
315const SUPPORTED_TOOLKINDS: [ToolKind; 6] = [
316    ToolKind::Sword,
317    ToolKind::Axe,
318    ToolKind::Hammer,
319    ToolKind::Bow,
320    ToolKind::Staff,
321    ToolKind::Sceptre,
322];
323
324type PrimaryComponentPool = HashMap<(ToolKind, String), Vec<(Item, Option<Hands>)>>;
325type SecondaryComponentPool = HashMap<ToolKind, Vec<(Arc<ItemDef>, Option<Hands>)>>;
326
327lazy_static! {
328    pub static ref PRIMARY_COMPONENT_POOL: PrimaryComponentPool = {
329        let mut component_pool = HashMap::new();
330
331        // Load recipe book
332        // (done to check that material is valid for a particular component)
333        use crate::recipe::ComponentKey;
334        let recipes = recipe::default_component_recipe_book().read();
335        let ability_map = &AbilityMap::load().read();
336        let msm = &MaterialStatManifest::load().read();
337
338        recipes.iter().for_each(
339            |(
340                ComponentKey {
341                    toolkind, material, ..
342                },
343                recipe,
344            )| {
345                let component = recipe.item_output(ability_map, msm);
346                let hand_restriction =
347                    if let ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
348                        hand_restriction,
349                        ..
350                    }) = &*component.kind()
351                    {
352                        *hand_restriction
353                    } else {
354                        return;
355                    };
356                let entry: &mut Vec<_> = component_pool
357                    .entry((*toolkind, String::from(material)))
358                    .or_default();
359                entry.push((component, hand_restriction));
360            },
361        );
362
363        component_pool
364    };
365
366    static ref SECONDARY_COMPONENT_POOL: SecondaryComponentPool = {
367        let mut component_pool = HashMap::new();
368
369        const ASSET_PREFIX: &str = "common.items.modular.weapon.secondary";
370
371        for toolkind in SUPPORTED_TOOLKINDS {
372            let directory = format!("{}.{}", ASSET_PREFIX, toolkind.identifier_name());
373            if let Ok(items) = Item::new_from_asset_glob(&directory) {
374                items
375                    .into_iter()
376                    .filter_map(|comp| Some(comp.item_definition_id().itemdef_id()?.to_owned()))
377                    .filter_map(|id| Arc::<ItemDef>::load_cloned(&id).ok())
378                    .for_each(|comp_def| {
379                        if let ItemKind::ModularComponent(
380                            ModularComponent::ToolSecondaryComponent {
381                                hand_restriction, ..
382                            },
383                        ) = comp_def.kind
384                        {
385                            let entry: &mut Vec<_> = component_pool.entry(toolkind).or_default();
386                            entry.push((Arc::clone(&comp_def), hand_restriction));
387                        }
388                    });
389            }
390        }
391
392        component_pool
393    };
394}
395
396#[derive(Debug)]
397pub enum ModularWeaponCreationError {
398    MaterialNotFound,
399    PrimaryComponentNotFound,
400    SecondaryComponentNotFound,
401}
402
403/// Check if hand restrictions are compatible.
404///
405/// If at least on of them is omitted, check is passed.
406pub fn compatible_handedness(a: Option<Hands>, b: Option<Hands>) -> bool {
407    match (a, b) {
408        (Some(a), Some(b)) => a == b,
409        _ => true,
410    }
411}
412
413/// Generate all primary components for specific tool and material.
414///
415/// Read [random_weapon_primary_component] for more.
416pub fn generate_weapon_primary_components(
417    tool: ToolKind,
418    material: Material,
419    hand_restriction: Option<Hands>,
420) -> Result<Vec<(Item, Option<Hands>)>, ModularWeaponCreationError> {
421    if let Some(material_id) = material.asset_identifier() {
422        // Loads default ability map and material stat manifest for later use
423        let ability_map = &AbilityMap::load().read();
424        let msm = &MaterialStatManifest::load().read();
425
426        Ok(PRIMARY_COMPONENT_POOL
427            .get(&(tool, material_id.to_owned()))
428            .into_iter()
429            .flatten()
430            .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
431            .map(|(c, h)| (c.duplicate(ability_map, msm), hand_restriction.or(*h)))
432            .collect())
433    } else {
434        Err(ModularWeaponCreationError::MaterialNotFound)
435    }
436}
437
438/// Creates a random modular weapon primary component when provided with a
439/// toolkind, material, and optionally the handedness
440///
441/// NOTE: The component produced is not necessarily restricted to that
442/// handedness, but rather is able to produce a weapon of that handedness
443/// depending on what secondary component is used
444///
445/// Returns the compatible handednesses that can be used with provided
446/// restriction and generated component (useful for cases where no restriction
447/// was passed in, but generated component has a restriction)
448pub fn random_weapon_primary_component(
449    tool: ToolKind,
450    material: Material,
451    hand_restriction: Option<Hands>,
452    mut rng: &mut impl Rng,
453) -> Result<(Item, Option<Hands>), ModularWeaponCreationError> {
454    let result = {
455        if let Some(material_id) = material.asset_identifier() {
456            // Loads default ability map and material stat manifest for later use
457            let ability_map = &AbilityMap::load().read();
458            let msm = &MaterialStatManifest::load().read();
459
460            let primary_components = PRIMARY_COMPONENT_POOL
461                .get(&(tool, material_id.to_owned()))
462                .into_iter()
463                .flatten()
464                .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
465                .collect::<Vec<_>>();
466
467            let (comp, hand) = primary_components
468                .choose(&mut rng)
469                .ok_or(ModularWeaponCreationError::PrimaryComponentNotFound)?;
470            let comp = comp.duplicate(ability_map, msm);
471            Ok((comp, hand_restriction.or(*hand)))
472        } else {
473            Err(ModularWeaponCreationError::MaterialNotFound)
474        }
475    };
476
477    if let Err(err) = &result {
478        let error_str = format!(
479            "Failed to synthesize a primary component for a modular {tool:?} made of {material:?} \
480             that had a hand restriction of {hand_restriction:?}. Error: {err:?}"
481        );
482        dev_panic!(error_str)
483    }
484    result
485}
486
487pub fn generate_weapons(
488    tool: ToolKind,
489    material: Material,
490    hand_restriction: Option<Hands>,
491) -> Result<Vec<Item>, ModularWeaponCreationError> {
492    // Loads default ability map and material stat manifest for later use
493    let ability_map = &AbilityMap::load().read();
494    let msm = &MaterialStatManifest::load().read();
495
496    let primaries = generate_weapon_primary_components(tool, material, hand_restriction)?;
497    let mut weapons = Vec::new();
498
499    for (comp, comp_hand) in primaries {
500        let secondaries = SECONDARY_COMPONENT_POOL
501            .get(&tool)
502            .into_iter()
503            .flatten()
504            .filter(|(_def, hand)| compatible_handedness(hand_restriction, *hand))
505            .filter(|(_def, hand)| compatible_handedness(comp_hand, *hand));
506
507        for (def, _hand) in secondaries {
508            let secondary = Item::new_from_item_base(
509                ItemBase::Simple(Arc::clone(def)),
510                Vec::new(),
511                ability_map,
512                msm,
513            );
514            let it = Item::new_from_item_base(
515                ItemBase::Modular(ModularBase::Tool),
516                vec![comp.duplicate(ability_map, msm), secondary],
517                ability_map,
518                msm,
519            );
520            weapons.push(it);
521        }
522    }
523
524    Ok(weapons)
525}
526
527/// Creates a random modular weapon when provided with a toolkind, material, and
528/// optionally the handedness
529pub fn random_weapon(
530    tool: ToolKind,
531    material: Material,
532    hand_restriction: Option<Hands>,
533    mut rng: &mut impl Rng,
534) -> Result<Item, ModularWeaponCreationError> {
535    let result = {
536        // Loads default ability map and material stat manifest for later use
537        let ability_map = &AbilityMap::load().read();
538        let msm = &MaterialStatManifest::load().read();
539
540        let (primary_component, hand_restriction) =
541            random_weapon_primary_component(tool, material, hand_restriction, rng)?;
542
543        let secondary_components = SECONDARY_COMPONENT_POOL
544            .get(&tool)
545            .into_iter()
546            .flatten()
547            .filter(|(_def, hand)| compatible_handedness(hand_restriction, *hand))
548            .collect::<Vec<_>>();
549
550        let secondary_component = {
551            let def = &secondary_components
552                .choose(&mut rng)
553                .ok_or(ModularWeaponCreationError::SecondaryComponentNotFound)?
554                .0;
555
556            Item::new_from_item_base(
557                ItemBase::Simple(Arc::clone(def)),
558                Vec::new(),
559                ability_map,
560                msm,
561            )
562        };
563
564        // Create modular weapon
565        Ok(Item::new_from_item_base(
566            ItemBase::Modular(ModularBase::Tool),
567            vec![primary_component, secondary_component],
568            ability_map,
569            msm,
570        ))
571    };
572    if let Err(err) = &result {
573        let error_str = format!(
574            "Failed to synthesize a modular {tool:?} made of {material:?} that had a hand \
575             restriction of {hand_restriction:?}. Error: {err:?}"
576        );
577        dev_panic!(error_str)
578    }
579    result
580}
581
582pub fn modify_name<'a>(item_name: &'a str, item: &'a Item) -> Cow<'a, str> {
583    if let ItemKind::ModularComponent(_) = &*item.kind() {
584        if let Some(material_name) = item
585            .components()
586            .iter()
587            .find_map(|comp| match &*comp.kind() {
588                #[expect(deprecated)]
589                ItemKind::Ingredient { descriptor, .. } => Some(descriptor.to_owned()),
590                _ => None,
591            })
592        {
593            Cow::Owned(format!("{} {}", material_name, item_name))
594        } else {
595            Cow::Borrowed(item_name)
596        }
597    } else {
598        Cow::Borrowed(item_name)
599    }
600}
601
602/// This is used as a key to uniquely identify the modular weapon in asset
603/// manifests in voxygen (Main component, material, hands)
604pub type ModularWeaponKey = (String, String, Hands);
605
606pub fn weapon_to_key(mod_weap: impl ItemDesc) -> ModularWeaponKey {
607    let hands = if let ItemKind::Tool(tool) = &*mod_weap.kind() {
608        tool.hands
609    } else {
610        Hands::One
611    };
612
613    match mod_weap
614        .components()
615        .iter()
616        .find_map(|comp| match &*comp.kind() {
617            ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. }) => {
618                let component_id = comp.item_definition_id().itemdef_id()?.to_owned();
619                let material_id = comp.components().iter().find_map(|mat| match &*mat.kind() {
620                    ItemKind::Ingredient { .. } => {
621                        Some(mat.item_definition_id().itemdef_id()?.to_owned())
622                    },
623                    _ => None,
624                });
625                Some((component_id, material_id))
626            },
627            _ => None,
628        }) {
629        Some((component_id, Some(material_id))) => (component_id, material_id, hands),
630        Some((component_id, None)) => (component_id, String::new(), hands),
631        None => (String::new(), String::new(), hands),
632    }
633}
634
635/// This is used as a key to uniquely identify the modular weapon in asset
636/// manifests in voxygen (Main component, material)
637pub type ModularWeaponComponentKey = (String, String);
638
639pub enum ModularWeaponComponentKeyError {
640    MaterialNotFound,
641}
642
643pub fn weapon_component_to_key(
644    item_def_id: &str,
645    components: &[Item],
646) -> Result<ModularWeaponComponentKey, ModularWeaponComponentKeyError> {
647    match components.iter().find_map(|mat| match &*mat.kind() {
648        ItemKind::Ingredient { .. } => Some(mat.item_definition_id().itemdef_id()?.to_owned()),
649        _ => None,
650    }) {
651        Some(material_id) => Ok((item_def_id.to_owned(), material_id)),
652        None => Err(ModularWeaponComponentKeyError::MaterialNotFound),
653    }
654}