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    WeaponHandednessNotFound,
402}
403
404/// Check if hand restrictions are compatible.
405///
406/// If at least on of them is omitted, check is passed.
407pub fn compatible_handedness(a: Option<Hands>, b: Option<Hands>) -> bool {
408    match (a, b) {
409        (Some(a), Some(b)) => a == b,
410        _ => true,
411    }
412}
413
414/// Check if hand combination is matching.
415///
416/// Uses primary and secondary hands, as well as optional restriction
417pub fn matching_handedness(
418    primary: Option<Hands>,
419    secondary: Option<Hands>,
420    restriction: Option<Hands>,
421) -> bool {
422    match (primary, secondary, restriction) {
423        (Some(a), Some(b), Some(c)) => (a == b) && (b == c),
424
425        (None, None, Some(Hands::Two)) => false,
426        (None, None, Some(Hands::One)) => true,
427
428        (Some(a), None, Some(c)) => a == c,
429        (None, Some(b), Some(c)) => b == c,
430        (Some(a), Some(b), None) => a == b,
431
432        (_, _, None) => true,
433    }
434}
435
436/// Generate all primary components for specific tool and material.
437///
438/// Read [random_weapon_primary_component] for more.
439pub fn generate_weapon_primary_components(
440    tool: ToolKind,
441    material: Material,
442    hand_restriction: Option<Hands>,
443) -> Result<Vec<(Item, Option<Hands>)>, ModularWeaponCreationError> {
444    if let Some(material_id) = material.asset_identifier() {
445        // Loads default ability map and material stat manifest for later use
446        let ability_map = &AbilityMap::load().read();
447        let msm = &MaterialStatManifest::load().read();
448
449        Ok(PRIMARY_COMPONENT_POOL
450            .get(&(tool, material_id.to_owned()))
451            .into_iter()
452            .flatten()
453            .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
454            .map(|(c, h)| (c.duplicate(ability_map, msm), *h))
455            .collect())
456    } else {
457        Err(ModularWeaponCreationError::MaterialNotFound)
458    }
459}
460
461/// Creates a random modular weapon primary component when provided with a
462/// toolkind, material, and optionally the handedness
463///
464/// NOTE: The component produced is not necessarily restricted to that
465/// handedness, but rather is able to produce a weapon of that handedness
466/// depending on what secondary component is used
467///
468/// Returns the compatible handednesses that can be used with provided
469/// restriction and generated component (useful for cases where no restriction
470/// was passed in, but generated component has a restriction)
471pub fn random_weapon_primary_component(
472    tool: ToolKind,
473    material: Material,
474    hand_restriction: Option<Hands>,
475    mut rng: &mut impl Rng,
476) -> Result<(Item, Option<Hands>), ModularWeaponCreationError> {
477    let result = {
478        if let Some(material_id) = material.asset_identifier() {
479            // Loads default ability map and material stat manifest for later use
480            let ability_map = &AbilityMap::load().read();
481            let msm = &MaterialStatManifest::load().read();
482
483            let primary_components = PRIMARY_COMPONENT_POOL
484                .get(&(tool, material_id.to_owned()))
485                .into_iter()
486                .flatten()
487                .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
488                .collect::<Vec<_>>();
489
490            let (comp, hand) = primary_components
491                .choose(&mut rng)
492                .ok_or(ModularWeaponCreationError::PrimaryComponentNotFound)?;
493            let comp = comp.duplicate(ability_map, msm);
494            Ok((comp, (*hand)))
495        } else {
496            Err(ModularWeaponCreationError::MaterialNotFound)
497        }
498    };
499
500    if let Err(err) = &result {
501        let error_str = format!(
502            "Failed to synthesize a primary component for a modular {tool:?} made of {material:?} \
503             that had a hand restriction of {hand_restriction:?}. Error: {err:?}"
504        );
505        dev_panic!(error_str)
506    }
507    result
508}
509
510pub fn generate_weapons(
511    tool: ToolKind,
512    material: Material,
513    hand_restriction: Option<Hands>,
514) -> Result<Vec<Item>, ModularWeaponCreationError> {
515    // Loads default ability map and material stat manifest for later use
516    let ability_map = &AbilityMap::load().read();
517    let msm = &MaterialStatManifest::load().read();
518
519    let primaries = generate_weapon_primary_components(tool, material, hand_restriction)?;
520    let mut weapons = Vec::new();
521
522    for (comp, comp_hand) in primaries {
523        let secondaries = SECONDARY_COMPONENT_POOL
524            .get(&tool)
525            .into_iter()
526            .flatten()
527            .filter(|(_def, hand)| matching_handedness(comp_hand, *hand, hand_restriction));
528
529        for (def, _hand) in secondaries {
530            let secondary = Item::new_from_item_base(
531                ItemBase::Simple(Arc::clone(def)),
532                Vec::new(),
533                ability_map,
534                msm,
535            );
536            weapons.push(Item::new_from_item_base(
537                ItemBase::Modular(ModularBase::Tool),
538                vec![comp.duplicate(ability_map, msm), secondary],
539                ability_map,
540                msm,
541            ));
542        }
543    }
544
545    Ok(weapons)
546}
547
548/// Creates a random modular weapon when provided with a toolkind, material, and
549/// optionally the handedness
550pub fn random_weapon(
551    tool: ToolKind,
552    material: Material,
553    hand_restriction: Option<Hands>,
554    mut rng: &mut impl Rng,
555) -> Result<Item, ModularWeaponCreationError> {
556    let result = {
557        // Loads default ability map and material stat manifest for later use
558        let ability_map = &AbilityMap::load().read();
559        let msm = &MaterialStatManifest::load().read();
560
561        let (primary_component, primary_hands) =
562            random_weapon_primary_component(tool, material, hand_restriction, rng)?;
563
564        let secondary_components = SECONDARY_COMPONENT_POOL
565            .get(&tool)
566            .into_iter()
567            .flatten()
568            .filter(|(_def, hand)| matching_handedness(primary_hands, *hand, hand_restriction))
569            .collect::<Vec<_>>();
570
571        let secondary_component = {
572            let def = &secondary_components
573                .choose(&mut rng)
574                .ok_or(ModularWeaponCreationError::SecondaryComponentNotFound)?
575                .0;
576
577            Item::new_from_item_base(
578                ItemBase::Simple(Arc::clone(def)),
579                Vec::new(),
580                ability_map,
581                msm,
582            )
583        };
584
585        // Create modular weapon
586        Ok(Item::new_from_item_base(
587            ItemBase::Modular(ModularBase::Tool),
588            vec![primary_component, secondary_component],
589            ability_map,
590            msm,
591        ))
592    };
593    if let Err(err) = &result {
594        let error_str = format!(
595            "Failed to synthesize a modular {tool:?} made of {material:?} that had a hand \
596             restriction of {hand_restriction:?}. Error: {err:?}"
597        );
598        dev_panic!(error_str)
599    }
600    result
601}
602
603pub fn modify_name<'a>(item_name: &'a str, item: &'a Item) -> Cow<'a, str> {
604    if let ItemKind::ModularComponent(_) = &*item.kind() {
605        if let Some(material_name) = item
606            .components()
607            .iter()
608            .find_map(|comp| match &*comp.kind() {
609                #[expect(deprecated)]
610                ItemKind::Ingredient { descriptor, .. } => Some(descriptor.to_owned()),
611                _ => None,
612            })
613        {
614            Cow::Owned(format!("{} {}", material_name, item_name))
615        } else {
616            Cow::Borrowed(item_name)
617        }
618    } else {
619        Cow::Borrowed(item_name)
620    }
621}
622
623/// This is used as a key to uniquely identify the modular weapon in asset
624/// manifests in voxygen (Main component, material, hands)
625pub type ModularWeaponKey = (String, String, Hands);
626
627pub fn weapon_to_key(mod_weap: impl ItemDesc) -> ModularWeaponKey {
628    let hands = if let ItemKind::Tool(tool) = &*mod_weap.kind() {
629        tool.hands
630    } else {
631        Hands::One
632    };
633
634    match mod_weap
635        .components()
636        .iter()
637        .find_map(|comp| match &*comp.kind() {
638            ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. }) => {
639                let component_id = comp.item_definition_id().itemdef_id()?.to_owned();
640                let material_id = comp.components().iter().find_map(|mat| match &*mat.kind() {
641                    ItemKind::Ingredient { .. } => {
642                        Some(mat.item_definition_id().itemdef_id()?.to_owned())
643                    },
644                    _ => None,
645                });
646                Some((component_id, material_id))
647            },
648            _ => None,
649        }) {
650        Some((component_id, Some(material_id))) => (component_id, material_id, hands),
651        Some((component_id, None)) => (component_id, String::new(), hands),
652        None => (String::new(), String::new(), hands),
653    }
654}
655
656/// This is used as a key to uniquely identify the modular weapon in asset
657/// manifests in voxygen (Main component, material)
658pub type ModularWeaponComponentKey = (String, String);
659
660pub enum ModularWeaponComponentKeyError {
661    MaterialNotFound,
662}
663
664pub fn weapon_component_to_key(
665    item_def_id: &str,
666    components: &[Item],
667) -> Result<ModularWeaponComponentKey, ModularWeaponComponentKeyError> {
668    match components.iter().find_map(|mat| match &*mat.kind() {
669        ItemKind::Ingredient { .. } => Some(mat.item_definition_id().itemdef_id()?.to_owned()),
670        _ => None,
671    }) {
672        Some(material_id) => Ok((item_def_id.to_owned(), material_id)),
673        None => Err(ModularWeaponComponentKeyError::MaterialNotFound),
674    }
675}