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::{AssetExt, AssetHandle, BoxedError, FileAsset, load_ron},
8    recipe,
9};
10use common_base::dev_panic;
11use hashbrown::HashMap;
12use lazy_static::lazy_static;
13use rand::{Rng, prelude::IndexedRandom};
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 an Asset that also loads the keys, but the RecipeBook
51// Asset impl already does that, so checking for existence here is redundant.
52impl FileAsset for MaterialStatManifest {
53    const EXTENSION: &'static str = "ron";
54
55    fn from_bytes(bytes: Cow<[u8]>) -> Result<Self, BoxedError> { load_ron(&bytes) }
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    #[deprecated = "this function doesn't localize"]
144    pub fn generate_name(&self, components: &[Item]) -> Cow<'_, str> {
145        match self {
146            ModularBase::Tool => {
147                let name = components
148                    .iter()
149                    .find_map(|comp| match &*comp.kind() {
150                        ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
151                            weapon_name,
152                            ..
153                        }) => {
154                            // "Iron", "Cobalt", what have you
155                            //
156                            // fallback to just "Modular"
157                            let material_name = comp
158                                .components()
159                                .iter()
160                                .find_map(|mat| match mat.kind() {
161                                    #[expect(deprecated)]
162                                    Cow::Owned(ItemKind::Ingredient { descriptor, .. }) => {
163                                        Some(Cow::Owned(descriptor))
164                                    },
165                                    #[expect(deprecated)]
166                                    Cow::Borrowed(ItemKind::Ingredient { descriptor, .. }) => {
167                                        Some(Cow::Borrowed(descriptor.as_str()))
168                                    },
169                                    _ => None,
170                                })
171                                .unwrap_or_else(|| "Modular".into());
172
173                            // this whole thing is deprecated anyway
174                            #[expect(deprecated)]
175                            let weapon_name = match weapon_name {
176                                WeaponName::Universal(name) => name,
177                                WeaponName::HandednessDependent {
178                                    one_handed: name1,
179                                    two_handed: name2,
180                                } => match Self::resolve_hands(components) {
181                                    Hands::One => name1,
182                                    Hands::Two => name2,
183                                },
184                            };
185
186                            // turns into likes of "Iron Poleaxe"
187                            Some(format!("{material_name} {weapon_name}"))
188                        },
189                        _ => None,
190                    })
191                    .unwrap_or_else(|| "Modular Weapon".to_owned());
192                Cow::Owned(name)
193            },
194        }
195    }
196
197    pub fn compute_quality(&self, components: &[Item]) -> Quality {
198        components
199            .iter()
200            .fold(Quality::MIN, |a, b| a.max(b.quality()))
201    }
202
203    pub fn ability_spec(&self, components: &[Item]) -> Option<Cow<'_, AbilitySpec>> {
204        match self {
205            ModularBase::Tool => components.iter().find_map(|comp| match &*comp.kind() {
206                ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
207                    toolkind,
208                    ..
209                }) => Some(Cow::Owned(AbilitySpec::Tool(*toolkind))),
210                _ => None,
211            }),
212        }
213    }
214
215    pub fn generate_tags(&self, components: &[Item]) -> Vec<ItemTag> {
216        match self {
217            ModularBase::Tool => {
218                if let Some(comp) = components.iter().find(|comp| {
219                    matches!(
220                        &*comp.kind(),
221                        ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. })
222                    )
223                }) {
224                    if let Some(material) =
225                        comp.components()
226                            .iter()
227                            .find_map(|comp| match &*comp.kind() {
228                                ItemKind::Ingredient { .. } => {
229                                    comp.tags().into_iter().find_map(|tag| match tag {
230                                        ItemTag::Material(material) => Some(material),
231                                        _ => None,
232                                    })
233                                },
234                                _ => None,
235                            })
236                    {
237                        vec![
238                            ItemTag::Material(material),
239                            ItemTag::SalvageInto(material, 1),
240                        ]
241                    } else {
242                        Vec::new()
243                    }
244                } else {
245                    Vec::new()
246                }
247            },
248        }
249    }
250}
251
252#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
253#[serde(deny_unknown_fields)]
254pub enum ModularComponent {
255    ToolPrimaryComponent {
256        toolkind: ToolKind,
257        stats: tool::Stats,
258        hand_restriction: Option<Hands>,
259        weapon_name: WeaponName,
260    },
261    ToolSecondaryComponent {
262        toolkind: ToolKind,
263        stats: tool::Stats,
264        hand_restriction: Option<Hands>,
265    },
266}
267
268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub enum WeaponName {
270    #[deprecated = "since item i18n"]
271    Universal(String),
272    HandednessDependent {
273        #[deprecated = "since item i18n"]
274        one_handed: String,
275        #[deprecated = "since item i18n"]
276        two_handed: String,
277    },
278}
279
280impl ModularComponent {
281    pub fn tool_stats(
282        &self,
283        components: &[Item],
284        msm: &MaterialStatManifest,
285    ) -> Option<tool::Stats> {
286        match self {
287            Self::ToolPrimaryComponent { stats, .. } => {
288                let average_material_mult = components
289                    .iter()
290                    .filter_map(|comp| {
291                        comp.item_definition_id()
292                            .itemdef_id()
293                            .and_then(|id| msm.tool_stats.get(id))
294                            .copied()
295                            .zip(Some(1))
296                    })
297                    .reduce(|(stats_a, count_a), (stats_b, count_b)| {
298                        (stats_a + stats_b, count_a + count_b)
299                    })
300                    .map_or_else(tool::Stats::one, |(stats_sum, count)| {
301                        stats_sum / (count as f32)
302                    });
303
304                Some(*stats * average_material_mult)
305            },
306            Self::ToolSecondaryComponent { stats, .. } => Some(*stats),
307        }
308    }
309
310    pub fn toolkind(&self) -> Option<ToolKind> {
311        match self {
312            Self::ToolPrimaryComponent { toolkind, .. }
313            | Self::ToolSecondaryComponent { toolkind, .. } => Some(*toolkind),
314        }
315    }
316}
317
318const SUPPORTED_TOOLKINDS: [ToolKind; 6] = [
319    ToolKind::Sword,
320    ToolKind::Axe,
321    ToolKind::Hammer,
322    ToolKind::Bow,
323    ToolKind::Staff,
324    ToolKind::Sceptre,
325];
326
327type PrimaryComponentPool = HashMap<(ToolKind, String), Vec<(Item, Option<Hands>)>>;
328type SecondaryComponentPool = HashMap<ToolKind, Vec<(Arc<ItemDef>, Option<Hands>)>>;
329
330lazy_static! {
331    pub static ref PRIMARY_COMPONENT_POOL: PrimaryComponentPool = {
332        let mut component_pool = HashMap::new();
333
334        // Load recipe book
335        // (done to check that material is valid for a particular component)
336        use crate::recipe::ComponentKey;
337        let recipes = recipe::default_component_recipe_book().read();
338        let ability_map = &AbilityMap::load().read();
339        let msm = &MaterialStatManifest::load().read();
340
341        recipes.iter().for_each(
342            |(
343                ComponentKey {
344                    toolkind, material, ..
345                },
346                recipe,
347            )| {
348                let component = recipe.item_output(ability_map, msm);
349                let hand_restriction =
350                    if let ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent {
351                        hand_restriction,
352                        ..
353                    }) = &*component.kind()
354                    {
355                        *hand_restriction
356                    } else {
357                        return;
358                    };
359                let entry: &mut Vec<_> = component_pool
360                    .entry((*toolkind, String::from(material)))
361                    .or_default();
362                entry.push((component, hand_restriction));
363            },
364        );
365
366        component_pool
367    };
368
369    static ref SECONDARY_COMPONENT_POOL: SecondaryComponentPool = {
370        let mut component_pool = HashMap::new();
371
372        const ASSET_PREFIX: &str = "common.items.modular.weapon.secondary";
373
374        for toolkind in SUPPORTED_TOOLKINDS {
375            let directory = format!("{}.{}", ASSET_PREFIX, toolkind.identifier_name());
376            if let Ok(items) = Item::new_from_asset_glob(&directory) {
377                items
378                    .into_iter()
379                    .filter_map(|comp| Some(comp.item_definition_id().itemdef_id()?.to_owned()))
380                    .filter_map(|id| Arc::<ItemDef>::load_cloned(&id).ok())
381                    .for_each(|comp_def| {
382                        if let ItemKind::ModularComponent(
383                            ModularComponent::ToolSecondaryComponent {
384                                hand_restriction, ..
385                            },
386                        ) = comp_def.kind
387                        {
388                            let entry: &mut Vec<_> = component_pool.entry(toolkind).or_default();
389                            entry.push((Arc::clone(&comp_def), hand_restriction));
390                        }
391                    });
392            }
393        }
394
395        component_pool
396    };
397}
398
399#[derive(Debug)]
400pub enum ModularWeaponCreationError {
401    MaterialNotFound,
402    PrimaryComponentNotFound,
403    SecondaryComponentNotFound,
404    WeaponHandednessNotFound,
405}
406
407/// Check if hand restrictions are compatible.
408///
409/// If at least on of them is omitted, check is passed.
410pub fn compatible_handedness(a: Option<Hands>, b: Option<Hands>) -> bool {
411    match (a, b) {
412        (Some(a), Some(b)) => a == b,
413        _ => true,
414    }
415}
416
417/// Check if hand combination is matching.
418///
419/// Uses primary and secondary hands, as well as optional restriction
420pub fn matching_handedness(
421    primary: Option<Hands>,
422    secondary: Option<Hands>,
423    restriction: Option<Hands>,
424) -> bool {
425    match (primary, secondary, restriction) {
426        (Some(a), Some(b), Some(c)) => (a == b) && (b == c),
427
428        (None, None, Some(Hands::Two)) => false,
429        (None, None, Some(Hands::One)) => true,
430
431        (Some(a), None, Some(c)) => a == c,
432        (None, Some(b), Some(c)) => b == c,
433        (Some(a), Some(b), None) => a == b,
434
435        (_, _, None) => true,
436    }
437}
438
439/// Generate all primary components for specific tool and material.
440///
441/// Read [random_weapon_primary_component] for more.
442pub fn generate_weapon_primary_components(
443    tool: ToolKind,
444    material: Material,
445    hand_restriction: Option<Hands>,
446) -> Result<Vec<(Item, Option<Hands>)>, ModularWeaponCreationError> {
447    if let Some(material_id) = material.asset_identifier() {
448        // Loads default ability map and material stat manifest for later use
449        let ability_map = &AbilityMap::load().read();
450        let msm = &MaterialStatManifest::load().read();
451
452        Ok(PRIMARY_COMPONENT_POOL
453            .get(&(tool, material_id.to_owned()))
454            .into_iter()
455            .flatten()
456            .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
457            .map(|(c, h)| (c.duplicate(ability_map, msm), *h))
458            .collect())
459    } else {
460        Err(ModularWeaponCreationError::MaterialNotFound)
461    }
462}
463
464/// Creates a random modular weapon primary component when provided with a
465/// toolkind, material, and optionally the handedness
466///
467/// NOTE: The component produced is not necessarily restricted to that
468/// handedness, but rather is able to produce a weapon of that handedness
469/// depending on what secondary component is used
470///
471/// Returns the compatible handednesses that can be used with provided
472/// restriction and generated component (useful for cases where no restriction
473/// was passed in, but generated component has a restriction)
474pub fn random_weapon_primary_component(
475    tool: ToolKind,
476    material: Material,
477    hand_restriction: Option<Hands>,
478    mut rng: &mut impl Rng,
479) -> Result<(Item, Option<Hands>), ModularWeaponCreationError> {
480    let result = {
481        if let Some(material_id) = material.asset_identifier() {
482            // Loads default ability map and material stat manifest for later use
483            let ability_map = &AbilityMap::load().read();
484            let msm = &MaterialStatManifest::load().read();
485
486            let primary_components = PRIMARY_COMPONENT_POOL
487                .get(&(tool, material_id.to_owned()))
488                .into_iter()
489                .flatten()
490                .filter(|(_comp, hand)| compatible_handedness(hand_restriction, *hand))
491                .collect::<Vec<_>>();
492
493            let (comp, hand) = primary_components
494                .choose(&mut rng)
495                .ok_or(ModularWeaponCreationError::PrimaryComponentNotFound)?;
496            let comp = comp.duplicate(ability_map, msm);
497            Ok((comp, (*hand)))
498        } else {
499            Err(ModularWeaponCreationError::MaterialNotFound)
500        }
501    };
502
503    if let Err(err) = &result {
504        let error_str = format!(
505            "Failed to synthesize a primary component for a modular {tool:?} made of {material:?} \
506             that had a hand restriction of {hand_restriction:?}. Error: {err:?}"
507        );
508        dev_panic!(error_str)
509    }
510    result
511}
512
513pub fn generate_weapons(
514    tool: ToolKind,
515    material: Material,
516    hand_restriction: Option<Hands>,
517) -> Result<Vec<Item>, ModularWeaponCreationError> {
518    // Loads default ability map and material stat manifest for later use
519    let ability_map = &AbilityMap::load().read();
520    let msm = &MaterialStatManifest::load().read();
521
522    let primaries = generate_weapon_primary_components(tool, material, hand_restriction)?;
523    let mut weapons = Vec::new();
524
525    for (comp, comp_hand) in primaries {
526        let secondaries = SECONDARY_COMPONENT_POOL
527            .get(&tool)
528            .into_iter()
529            .flatten()
530            .filter(|(_def, hand)| matching_handedness(comp_hand, *hand, hand_restriction));
531
532        for (def, _hand) in secondaries {
533            let secondary = Item::new_from_item_base(
534                ItemBase::Simple(Arc::clone(def)),
535                Vec::new(),
536                ability_map,
537                msm,
538            );
539            weapons.push(Item::new_from_item_base(
540                ItemBase::Modular(ModularBase::Tool),
541                vec![comp.duplicate(ability_map, msm), secondary],
542                ability_map,
543                msm,
544            ));
545        }
546    }
547
548    Ok(weapons)
549}
550
551/// Creates a random modular weapon when provided with a toolkind, material, and
552/// optionally the handedness
553pub fn random_weapon(
554    tool: ToolKind,
555    material: Material,
556    hand_restriction: Option<Hands>,
557    mut rng: &mut impl Rng,
558) -> Result<Item, ModularWeaponCreationError> {
559    let result = {
560        // Loads default ability map and material stat manifest for later use
561        let ability_map = &AbilityMap::load().read();
562        let msm = &MaterialStatManifest::load().read();
563
564        let (primary_component, primary_hands) =
565            random_weapon_primary_component(tool, material, hand_restriction, rng)?;
566
567        let secondary_components = SECONDARY_COMPONENT_POOL
568            .get(&tool)
569            .into_iter()
570            .flatten()
571            .filter(|(_def, hand)| matching_handedness(primary_hands, *hand, hand_restriction))
572            .collect::<Vec<_>>();
573
574        let secondary_component = {
575            let def = &secondary_components
576                .choose(&mut rng)
577                .ok_or(ModularWeaponCreationError::SecondaryComponentNotFound)?
578                .0;
579
580            Item::new_from_item_base(
581                ItemBase::Simple(Arc::clone(def)),
582                Vec::new(),
583                ability_map,
584                msm,
585            )
586        };
587
588        // Create modular weapon
589        Ok(Item::new_from_item_base(
590            ItemBase::Modular(ModularBase::Tool),
591            vec![primary_component, secondary_component],
592            ability_map,
593            msm,
594        ))
595    };
596    if let Err(err) = &result {
597        let error_str = format!(
598            "Failed to synthesize a modular {tool:?} made of {material:?} that had a hand \
599             restriction of {hand_restriction:?}. Error: {err:?}"
600        );
601        dev_panic!(error_str)
602    }
603    result
604}
605
606/// Adds a (modular) ingredient prefix
607#[deprecated = "since item i18n"]
608pub fn modify_name<'a>(item_name: &'a str, item: &'a Item) -> Cow<'a, str> {
609    if let ItemKind::ModularComponent(_) = &*item.kind() {
610        if let Some(material_name) = item
611            .components()
612            .iter()
613            .find_map(|comp| match &*comp.kind() {
614                #[expect(deprecated)]
615                ItemKind::Ingredient { descriptor, .. } => Some(descriptor.to_owned()),
616                _ => None,
617            })
618        {
619            // "Wooden" + "Brand Shaft"
620            // =>
621            // "Wooden Brand Shaft"
622            Cow::Owned(format!("{} {}", material_name, item_name))
623        } else {
624            Cow::Borrowed(item_name)
625        }
626    } else {
627        Cow::Borrowed(item_name)
628    }
629}
630
631/// This is used as a key to uniquely identify the modular weapon in asset
632/// manifests in voxygen (Main component, material, hands)
633///
634/// # Example:
635/// ```ignore
636/// (
637///     "common.items.modular.weapon.primary.axe.labrys",
638///     "common.items.mineral.ingot.iron",
639///     Two,
640/// )
641/// ```
642pub type ModularWeaponKey = (String, String, Hands);
643
644pub fn weapon_to_key(mod_weap: impl ItemDesc) -> ModularWeaponKey {
645    let hands = if let ItemKind::Tool(tool) = &*mod_weap.kind() {
646        tool.hands
647    } else {
648        Hands::One
649    };
650
651    match mod_weap
652        .components()
653        .iter()
654        .find_map(|comp| match &*comp.kind() {
655            ItemKind::ModularComponent(ModularComponent::ToolPrimaryComponent { .. }) => {
656                let component_id = comp.item_definition_id().itemdef_id()?.to_owned();
657                let material_id = comp.components().iter().find_map(|mat| match &*mat.kind() {
658                    ItemKind::Ingredient { .. } => {
659                        Some(mat.item_definition_id().itemdef_id()?.to_owned())
660                    },
661                    _ => None,
662                });
663                Some((component_id, material_id))
664            },
665            _ => None,
666        }) {
667        Some((component_id, Some(material_id))) => (component_id, material_id, hands),
668        Some((component_id, None)) => (component_id, String::new(), hands),
669        None => (String::new(), String::new(), hands),
670    }
671}
672
673/// This is used as a key to uniquely identify the modular weapon in asset
674/// manifests in voxygen (Main component, material)
675///
676/// # Example:
677/// ```ignore
678/// (
679///     "common.items.modular.weapon.primary.axe.labrys",
680///     "common.items.mineral.ingot.iron",
681/// )
682/// ```
683pub type ModularWeaponComponentKey = (String, String);
684
685pub enum ModularWeaponComponentKeyError {
686    MaterialNotFound,
687}
688
689pub fn weapon_component_to_key(
690    item_def_id: &str,
691    components: &[Item],
692) -> Result<ModularWeaponComponentKey, ModularWeaponComponentKeyError> {
693    match components.iter().find_map(|mat| match &*mat.kind() {
694        ItemKind::Ingredient { .. } => Some(mat.item_definition_id().itemdef_id()?.to_owned()),
695        _ => None,
696    }) {
697        Some(material_id) => Ok((item_def_id.to_owned(), material_id)),
698        None => Err(ModularWeaponComponentKeyError::MaterialNotFound),
699    }
700}