veloren_common/comp/inventory/
loadout_builder.rs

1use crate::{
2    assets::{self, AssetExt},
3    calendar::{Calendar, CalendarEvent},
4    comp::{
5        Body, arthropod, biped_large, biped_small, bird_large, bird_medium, crustacean, golem,
6        inventory::{
7            loadout::Loadout,
8            slot::{ArmorSlot, EquipSlot},
9        },
10        item::{self, Item},
11        object, quadruped_low, quadruped_medium, quadruped_small, theropod,
12    },
13    resources::{Time, TimeOfDay},
14    trade::SiteInformation,
15};
16use rand::{self, Rng, distributions::WeightedError, seq::SliceRandom};
17use serde::{Deserialize, Serialize};
18use strum::EnumIter;
19use tracing::warn;
20
21type Weight = u8;
22
23#[derive(Debug)]
24pub enum SpecError {
25    LoadoutAssetError(assets::Error),
26    ItemAssetError(assets::Error),
27    ItemChoiceError(WeightedError),
28    BaseChoiceError(WeightedError),
29    ModularWeaponCreationError(item::modular::ModularWeaponCreationError),
30}
31
32#[derive(Debug)]
33#[cfg(test)]
34pub enum ValidationError {
35    ItemAssetError(assets::Error),
36    LoadoutAssetError(assets::Error),
37    Loop(Vec<String>),
38    ModularWeaponCreationError(item::modular::ModularWeaponCreationError),
39}
40
41#[derive(Debug, Deserialize, Clone)]
42enum ItemSpec {
43    Item(String),
44    /// Parameters in this variant are used to randomly create a modular weapon
45    /// that meets the provided parameters
46    ModularWeapon {
47        tool: item::tool::ToolKind,
48        material: item::Material,
49        hands: Option<item::tool::Hands>,
50    },
51    Choice(Vec<(Weight, Option<ItemSpec>)>),
52    Seasonal(Vec<(Option<CalendarEvent>, ItemSpec)>),
53}
54
55impl ItemSpec {
56    fn try_to_item(
57        &self,
58        rng: &mut impl Rng,
59        time: Option<&(TimeOfDay, Calendar)>,
60    ) -> Result<Option<Item>, SpecError> {
61        match self {
62            ItemSpec::Item(item_asset) => {
63                let item = Item::new_from_asset(item_asset).map_err(SpecError::ItemAssetError)?;
64                Ok(Some(item))
65            },
66            ItemSpec::Choice(items) => {
67                let (_, item_spec) = items
68                    .choose_weighted(rng, |(weight, _)| *weight)
69                    .map_err(SpecError::ItemChoiceError)?;
70
71                let item = if let Some(item_spec) = item_spec {
72                    item_spec.try_to_item(rng, time)?
73                } else {
74                    None
75                };
76                Ok(item)
77            },
78            ItemSpec::ModularWeapon {
79                tool,
80                material,
81                hands,
82            } => item::modular::random_weapon(*tool, *material, *hands, rng)
83                .map(Some)
84                .map_err(SpecError::ModularWeaponCreationError),
85            ItemSpec::Seasonal(specs) => specs
86                .iter()
87                .find_map(|(season, spec)| match (season, time) {
88                    (Some(season), Some((_time, calendar))) => {
89                        if calendar.is_event(*season) {
90                            Some(spec.try_to_item(rng, time))
91                        } else {
92                            None
93                        }
94                    },
95                    (Some(_season), None) => None,
96                    (None, _) => Some(spec.try_to_item(rng, time)),
97                })
98                .unwrap_or(Ok(None)),
99        }
100    }
101
102    // Check if ItemSpec is valid and can be turned into Item
103    #[cfg(test)]
104    fn validate(&self) -> Result<(), ValidationError> {
105        let mut rng = rand::thread_rng();
106        match self {
107            ItemSpec::Item(item_asset) => Item::new_from_asset(item_asset)
108                .map(drop)
109                .map_err(ValidationError::ItemAssetError),
110            ItemSpec::Choice(choices) => {
111                // TODO: check for sanity of weights?
112                for (_weight, choice) in choices {
113                    if let Some(item) = choice {
114                        item.validate()?;
115                    }
116                }
117                Ok(())
118            },
119            ItemSpec::ModularWeapon {
120                tool,
121                material,
122                hands,
123            } => item::modular::random_weapon(*tool, *material, *hands, &mut rng)
124                .map(drop)
125                .map_err(ValidationError::ModularWeaponCreationError),
126            ItemSpec::Seasonal(specs) => {
127                specs.iter().try_for_each(|(_season, spec)| spec.validate())
128            },
129        }
130    }
131}
132
133#[derive(Debug, Deserialize, Clone)]
134enum Hands {
135    /// Allows to specify one pair
136    InHands((Option<ItemSpec>, Option<ItemSpec>)),
137    /// Allows specify range of choices
138    Choice(Vec<(Weight, Hands)>),
139}
140
141impl Hands {
142    fn try_to_pair(
143        &self,
144        rng: &mut impl Rng,
145        time: Option<&(TimeOfDay, Calendar)>,
146    ) -> Result<(Option<Item>, Option<Item>), SpecError> {
147        match self {
148            Hands::InHands((mainhand, offhand)) => {
149                let mut from_spec = |i: &ItemSpec| i.try_to_item(rng, time);
150
151                let mainhand = mainhand.as_ref().map(&mut from_spec).transpose()?.flatten();
152                let offhand = offhand.as_ref().map(&mut from_spec).transpose()?.flatten();
153                Ok((mainhand, offhand))
154            },
155            Hands::Choice(pairs) => {
156                let (_, pair_spec) = pairs
157                    .choose_weighted(rng, |(weight, _)| *weight)
158                    .map_err(SpecError::ItemChoiceError)?;
159
160                pair_spec.try_to_pair(rng, time)
161            },
162        }
163    }
164
165    // Check if items in Hand are valid and can be turned into Item
166    #[cfg(test)]
167    fn validate(&self) -> Result<(), ValidationError> {
168        match self {
169            Self::InHands((left, right)) => {
170                if let Some(hand) = left {
171                    hand.validate()?;
172                }
173                if let Some(hand) = right {
174                    hand.validate()?;
175                }
176                Ok(())
177            },
178            Self::Choice(choices) => {
179                // TODO: check for sanity of weights?
180                for (_weight, choice) in choices {
181                    choice.validate()?;
182                }
183                Ok(())
184            },
185        }
186    }
187}
188
189#[derive(Debug, Deserialize, Clone)]
190enum Base {
191    Asset(String),
192    /// NOTE: If you have the same item in multiple configs,
193    /// *first* one will have the priority
194    Combine(Vec<Base>),
195    Choice(Vec<(Weight, Base)>),
196}
197
198impl Base {
199    // Turns Base to LoadoutSpec
200    //
201    // NOTE: Don't expect it to be fully evaluated, but in some cases
202    // it may be so.
203    fn to_spec(&self, rng: &mut impl Rng) -> Result<LoadoutSpec, SpecError> {
204        match self {
205            Base::Asset(asset_specifier) => {
206                LoadoutSpec::load_cloned(asset_specifier).map_err(SpecError::LoadoutAssetError)
207            },
208            Base::Combine(bases) => {
209                let bases = bases.iter().map(|b| b.to_spec(rng)?.eval(rng));
210                // Get first base of combined
211                let mut current = LoadoutSpec::default();
212                for base in bases {
213                    current = current.merge(base?);
214                }
215
216                Ok(current)
217            },
218            Base::Choice(choice) => {
219                let (_, base) = choice
220                    .choose_weighted(rng, |(weight, _)| *weight)
221                    .map_err(SpecError::BaseChoiceError)?;
222
223                base.to_spec(rng)
224            },
225        }
226    }
227}
228
229// TODO: remove clone
230/// Core struct of loadout asset.
231///
232/// If you want programing API of loadout creation,
233/// use `LoadoutBuilder` instead.
234///
235/// For examples of assets, see `assets/test/loadout/ok` folder.
236#[derive(Debug, Deserialize, Clone, Default)]
237#[serde(deny_unknown_fields)]
238pub struct LoadoutSpec {
239    // Meta fields
240    inherit: Option<Base>,
241    // Armor
242    head: Option<ItemSpec>,
243    neck: Option<ItemSpec>,
244    shoulders: Option<ItemSpec>,
245    chest: Option<ItemSpec>,
246    gloves: Option<ItemSpec>,
247    ring1: Option<ItemSpec>,
248    ring2: Option<ItemSpec>,
249    back: Option<ItemSpec>,
250    belt: Option<ItemSpec>,
251    legs: Option<ItemSpec>,
252    feet: Option<ItemSpec>,
253    tabard: Option<ItemSpec>,
254    bag1: Option<ItemSpec>,
255    bag2: Option<ItemSpec>,
256    bag3: Option<ItemSpec>,
257    bag4: Option<ItemSpec>,
258    lantern: Option<ItemSpec>,
259    glider: Option<ItemSpec>,
260    // Weapons
261    active_hands: Option<Hands>,
262    inactive_hands: Option<Hands>,
263}
264
265impl LoadoutSpec {
266    /// Merges `self` with `base`.
267    /// If some field exists in `self` it will be used,
268    /// if no, it will be taken from `base`.
269    ///
270    /// NOTE: it uses only inheritance chain from `base`
271    /// without evaluating it.
272    /// Inheritance chain from `self` is discarded.
273    ///
274    /// # Examples
275    /// 1)
276    /// You have your asset, let's call it "a". In this asset, you have
277    /// inheritance from "b". In asset "b" you inherit from "c".
278    ///
279    /// If you load your "a" into LoadoutSpec A, and "b" into LoadoutSpec B,
280    /// and then merge A into B, you will get new LoadoutSpec that will inherit
281    /// from "c".
282    ///
283    /// 2)
284    /// You have two assets, let's call them "a" and "b".
285    /// "a" inherits from "n",
286    /// "b" inherits from "m".
287    ///
288    /// If you load "a" into A, "b" into B and then will try to merge them
289    /// you will get new LoadoutSpec that will inherit from "m".
290    /// It's error, because chain to "n" is lost!!!
291    ///
292    /// Correct way to do this will be first evaluating at least "a" and then
293    /// merge this new LoadoutSpec with "b".
294    fn merge(self, base: Self) -> Self {
295        Self {
296            inherit: base.inherit,
297            head: self.head.or(base.head),
298            neck: self.neck.or(base.neck),
299            shoulders: self.shoulders.or(base.shoulders),
300            chest: self.chest.or(base.chest),
301            gloves: self.gloves.or(base.gloves),
302            ring1: self.ring1.or(base.ring1),
303            ring2: self.ring2.or(base.ring2),
304            back: self.back.or(base.back),
305            belt: self.belt.or(base.belt),
306            legs: self.legs.or(base.legs),
307            feet: self.feet.or(base.feet),
308            tabard: self.tabard.or(base.tabard),
309            bag1: self.bag1.or(base.bag1),
310            bag2: self.bag2.or(base.bag2),
311            bag3: self.bag3.or(base.bag3),
312            bag4: self.bag4.or(base.bag4),
313            lantern: self.lantern.or(base.lantern),
314            glider: self.glider.or(base.glider),
315            active_hands: self.active_hands.or(base.active_hands),
316            inactive_hands: self.inactive_hands.or(base.inactive_hands),
317        }
318    }
319
320    /// Recursively evaluate all inheritance chain.
321    /// For example with following structure.
322    ///
323    /// ```text
324    /// A
325    /// inherit: B,
326    /// gloves: a,
327    ///
328    /// B
329    /// inherit: C,
330    /// ring1: b,
331    ///
332    /// C
333    /// inherit: None,
334    /// ring2: c
335    /// ```
336    ///
337    /// result will be
338    ///
339    /// ```text
340    /// inherit: None,
341    /// gloves: a,
342    /// ring1: b,
343    /// ring2: c,
344    /// ```
345    fn eval(self, rng: &mut impl Rng) -> Result<Self, SpecError> {
346        // Iherit loadout if needed
347        if let Some(ref base) = self.inherit {
348            let base = base.to_spec(rng)?.eval(rng);
349            Ok(self.merge(base?))
350        } else {
351            Ok(self)
352        }
353    }
354
355    // Validate loadout spec and check that it can be turned into real loadout.
356    // Checks for possible loops too.
357    //
358    // NOTE: It is stricter than needed, it will check all items
359    // even if they are overwritten.
360    // We can avoid these redundant checks by building set of all possible
361    // specs and then check them.
362    // This algorithm will be much more complex and require more memory,
363    // because if we Combine multiple Choice-s we will need to create
364    // cartesian product of specs.
365    //
366    // Also we probably don't want garbage entries anyway, even if they are
367    // unused.
368    #[cfg(test)]
369    pub fn validate(&self, history: Vec<String>) -> Result<(), ValidationError> {
370        // Helper function to traverse base.
371        //
372        // Important invariant to hold.
373        // Each time it finds new asset it appends it to history
374        // and calls spec.validate()
375        fn validate_base(base: &Base, mut history: Vec<String>) -> Result<(), ValidationError> {
376            match base {
377                Base::Asset(asset) => {
378                    // read the spec
379                    let based = LoadoutSpec::load_cloned(asset)
380                        .map_err(ValidationError::LoadoutAssetError)?;
381
382                    // expand history
383                    history.push(asset.to_owned());
384
385                    // validate our spec
386                    based.validate(history)
387                },
388                Base::Combine(bases) => {
389                    for base in bases {
390                        validate_base(base, history.clone())?;
391                    }
392                    Ok(())
393                },
394                Base::Choice(choices) => {
395                    // TODO: check for sanity of weights?
396                    for (_weight, base) in choices {
397                        validate_base(base, history.clone())?;
398                    }
399                    Ok(())
400                },
401            }
402        }
403
404        // Scarry logic
405        //
406        // We check for duplicates on each append, and because we append on each
407        // call we can be sure we don't have any duplicates unless it's a last
408        // element.
409        // So we can check for duplicates by comparing
410        // all elements with last element.
411        // And if we found duplicate in our history we found a loop.
412        if let Some((last, tail)) = history.split_last() {
413            for asset in tail {
414                if last == asset {
415                    return Err(ValidationError::Loop(history));
416                }
417            }
418        }
419
420        if let Some(base) = &self.inherit {
421            validate_base(base, history)?
422        }
423
424        self.validate_entries()
425    }
426
427    // Validate entries in loadout spec.
428    //
429    // NOTE: this only check for items, we assume that base
430    // is validated separately.
431    //
432    // TODO: add some intelligent checks,
433    // e.g. that `head` key corresponds to Item with ItemKind::Head(_)
434    #[cfg(test)]
435    fn validate_entries(&self) -> Result<(), ValidationError> {
436        // Armor
437        if let Some(item) = &self.head {
438            item.validate()?;
439        }
440        if let Some(item) = &self.neck {
441            item.validate()?;
442        }
443        if let Some(item) = &self.shoulders {
444            item.validate()?;
445        }
446        if let Some(item) = &self.chest {
447            item.validate()?;
448        }
449        if let Some(item) = &self.gloves {
450            item.validate()?;
451        }
452        if let Some(item) = &self.ring1 {
453            item.validate()?;
454        }
455        if let Some(item) = &self.ring2 {
456            item.validate()?;
457        }
458        if let Some(item) = &self.back {
459            item.validate()?;
460        }
461        if let Some(item) = &self.belt {
462            item.validate()?;
463        }
464        if let Some(item) = &self.legs {
465            item.validate()?;
466        }
467        if let Some(item) = &self.feet {
468            item.validate()?;
469        }
470        if let Some(item) = &self.tabard {
471            item.validate()?;
472        }
473        // Misc
474        if let Some(item) = &self.bag1 {
475            item.validate()?;
476        }
477        if let Some(item) = &self.bag2 {
478            item.validate()?;
479        }
480        if let Some(item) = &self.bag3 {
481            item.validate()?;
482        }
483        if let Some(item) = &self.bag4 {
484            item.validate()?;
485        }
486        if let Some(item) = &self.lantern {
487            item.validate()?;
488        }
489        if let Some(item) = &self.glider {
490            item.validate()?;
491        }
492        // Hands, tools and weapons
493        if let Some(hands) = &self.active_hands {
494            hands.validate()?;
495        }
496        if let Some(hands) = &self.inactive_hands {
497            hands.validate()?;
498        }
499
500        Ok(())
501    }
502}
503
504impl assets::Asset for LoadoutSpec {
505    type Loader = assets::RonLoader;
506
507    const EXTENSION: &'static str = "ron";
508}
509
510#[must_use]
511pub fn make_potion_bag(quantity: u32) -> Item {
512    let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.tiny_leather_pouch");
513    if let Some(i) = bag.slots_mut().iter_mut().next() {
514        let mut potions = Item::new_from_asset_expect("common.items.consumable.potion_big");
515        if let Err(e) = potions.set_amount(quantity) {
516            warn!("Failed to set potion quantity: {:?}", e);
517        }
518        *i = Some(potions);
519    }
520    bag
521}
522
523#[must_use]
524pub fn make_food_bag(quantity: u32) -> Item {
525    let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.tiny_leather_pouch");
526    if let Some(i) = bag.slots_mut().iter_mut().next() {
527        let mut food = Item::new_from_asset_expect("common.items.food.apple_stick");
528        if let Err(e) = food.set_amount(quantity) {
529            warn!("Failed to set food quantity: {:?}", e);
530        }
531        *i = Some(food);
532    }
533    bag
534}
535
536#[must_use]
537// We have many species so this function is long
538// Also we are using default tools for un-specified species so
539// it's fine to have wildcards
540#[expect(clippy::too_many_lines, clippy::match_wildcard_for_single_variants)]
541fn default_main_tool(body: &Body) -> Item {
542    let maybe_tool = match body {
543        Body::Golem(golem) => match golem.species {
544            golem::Species::StoneGolem => Some(Item::new_from_asset_expect(
545                "common.items.npc_weapons.unique.stone_golems_fist",
546            )),
547            golem::Species::ClayGolem => Some(Item::new_from_asset_expect(
548                "common.items.npc_weapons.unique.clay_golem_fist",
549            )),
550            golem::Species::Gravewarden => Some(Item::new_from_asset_expect(
551                "common.items.npc_weapons.unique.gravewarden_fist",
552            )),
553            golem::Species::WoodGolem => Some(Item::new_from_asset_expect(
554                "common.items.npc_weapons.unique.wood_golem_fist",
555            )),
556            golem::Species::CoralGolem => Some(Item::new_from_asset_expect(
557                "common.items.npc_weapons.unique.coral_golem_fist",
558            )),
559            golem::Species::AncientEffigy => Some(Item::new_from_asset_expect(
560                "common.items.npc_weapons.unique.ancient_effigy_eyes",
561            )),
562            golem::Species::Mogwai => Some(Item::new_from_asset_expect(
563                "common.items.npc_weapons.unique.mogwai",
564            )),
565            golem::Species::IronGolem => Some(Item::new_from_asset_expect(
566                "common.items.npc_weapons.unique.iron_golem_fist",
567            )),
568            _ => None,
569        },
570        Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
571            quadruped_medium::Species::Wolf => Some(Item::new_from_asset_expect(
572                "common.items.npc_weapons.unique.quadruped_medium.wolf",
573            )),
574            // Below uniques still follow quadmedhoof just with stat alterations
575            quadruped_medium::Species::Alpaca | quadruped_medium::Species::Llama => {
576                Some(Item::new_from_asset_expect(
577                    "common.items.npc_weapons.unique.quadruped_medium.alpaca",
578                ))
579            },
580            quadruped_medium::Species::Antelope | quadruped_medium::Species::Deer => {
581                Some(Item::new_from_asset_expect(
582                    "common.items.npc_weapons.unique.quadruped_medium.antelope",
583                ))
584            },
585            quadruped_medium::Species::Donkey | quadruped_medium::Species::Zebra => {
586                Some(Item::new_from_asset_expect(
587                    "common.items.npc_weapons.unique.quadruped_medium.donkey",
588                ))
589            },
590            // Provide Kelpie with unique water-centered abilities
591            quadruped_medium::Species::Horse | quadruped_medium::Species::Kelpie => {
592                Some(Item::new_from_asset_expect(
593                    "common.items.npc_weapons.unique.quadruped_medium.horse",
594                ))
595            },
596            quadruped_medium::Species::ClaySteed => Some(Item::new_from_asset_expect(
597                "common.items.npc_weapons.unique.claysteed",
598            )),
599            quadruped_medium::Species::Saber
600            | quadruped_medium::Species::Bonerattler
601            | quadruped_medium::Species::Lion
602            | quadruped_medium::Species::Snowleopard => Some(Item::new_from_asset_expect(
603                "common.items.npc_weapons.unique.quadmedjump",
604            )),
605            quadruped_medium::Species::Darkhound => Some(Item::new_from_asset_expect(
606                "common.items.npc_weapons.unique.darkhound",
607            )),
608            // Below uniques still follow quadmedcharge just with stat alterations
609            quadruped_medium::Species::Moose | quadruped_medium::Species::Tuskram => {
610                Some(Item::new_from_asset_expect(
611                    "common.items.npc_weapons.unique.quadruped_medium.moose",
612                ))
613            },
614            quadruped_medium::Species::Mouflon => Some(Item::new_from_asset_expect(
615                "common.items.npc_weapons.unique.quadruped_medium.mouflon",
616            )),
617            quadruped_medium::Species::Akhlut
618            | quadruped_medium::Species::Dreadhorn
619            | quadruped_medium::Species::Mammoth
620            | quadruped_medium::Species::Ngoubou => Some(Item::new_from_asset_expect(
621                "common.items.npc_weapons.unique.quadmedcharge",
622            )),
623            quadruped_medium::Species::Grolgar => Some(Item::new_from_asset_expect(
624                "common.items.npc_weapons.unique.quadruped_medium.grolgar",
625            )),
626            quadruped_medium::Species::Roshwalr => Some(Item::new_from_asset_expect(
627                "common.items.npc_weapons.unique.roshwalr",
628            )),
629            quadruped_medium::Species::Cattle => Some(Item::new_from_asset_expect(
630                "common.items.npc_weapons.unique.quadmedbasicgentle",
631            )),
632            quadruped_medium::Species::Highland | quadruped_medium::Species::Yak => {
633                Some(Item::new_from_asset_expect(
634                    "common.items.npc_weapons.unique.quadruped_medium.highland",
635                ))
636            },
637            quadruped_medium::Species::Frostfang => Some(Item::new_from_asset_expect(
638                "common.items.npc_weapons.unique.frostfang",
639            )),
640            _ => Some(Item::new_from_asset_expect(
641                "common.items.npc_weapons.unique.quadmedbasic",
642            )),
643        },
644        Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
645            quadruped_low::Species::Maneater => Some(Item::new_from_asset_expect(
646                "common.items.npc_weapons.unique.quadruped_low.maneater",
647            )),
648            quadruped_low::Species::Asp => Some(Item::new_from_asset_expect(
649                "common.items.npc_weapons.unique.quadruped_low.asp",
650            )),
651            quadruped_low::Species::Dagon => Some(Item::new_from_asset_expect(
652                "common.items.npc_weapons.unique.dagon",
653            )),
654            quadruped_low::Species::Snaretongue => Some(Item::new_from_asset_expect(
655                "common.items.npc_weapons.unique.snaretongue",
656            )),
657            quadruped_low::Species::Crocodile
658            | quadruped_low::Species::SeaCrocodile
659            | quadruped_low::Species::Alligator
660            | quadruped_low::Species::Salamander
661            | quadruped_low::Species::Elbst => Some(Item::new_from_asset_expect(
662                "common.items.npc_weapons.unique.quadlowtail",
663            )),
664            quadruped_low::Species::Monitor | quadruped_low::Species::Pangolin => Some(
665                Item::new_from_asset_expect("common.items.npc_weapons.unique.quadlowquick"),
666            ),
667            quadruped_low::Species::Lavadrake => Some(Item::new_from_asset_expect(
668                "common.items.npc_weapons.unique.quadruped_low.lavadrake",
669            )),
670            quadruped_low::Species::Deadwood => Some(Item::new_from_asset_expect(
671                "common.items.npc_weapons.unique.quadruped_low.deadwood",
672            )),
673            quadruped_low::Species::Basilisk => Some(Item::new_from_asset_expect(
674                "common.items.npc_weapons.unique.quadruped_low.basilisk",
675            )),
676            quadruped_low::Species::Icedrake => Some(Item::new_from_asset_expect(
677                "common.items.npc_weapons.unique.quadruped_low.icedrake",
678            )),
679            quadruped_low::Species::Hakulaq => Some(Item::new_from_asset_expect(
680                "common.items.npc_weapons.unique.quadruped_low.hakulaq",
681            )),
682            quadruped_low::Species::Tortoise => Some(Item::new_from_asset_expect(
683                "common.items.npc_weapons.unique.quadruped_low.tortoise",
684            )),
685            quadruped_low::Species::Driggle => Some(Item::new_from_asset_expect(
686                "common.items.npc_weapons.unique.driggle",
687            )),
688            quadruped_low::Species::Rocksnapper => Some(Item::new_from_asset_expect(
689                "common.items.npc_weapons.unique.rocksnapper",
690            )),
691            quadruped_low::Species::Hydra => Some(Item::new_from_asset_expect(
692                "common.items.npc_weapons.unique.quadruped_low.hydra",
693            )),
694            _ => Some(Item::new_from_asset_expect(
695                "common.items.npc_weapons.unique.quadlowbasic",
696            )),
697        },
698        Body::QuadrupedSmall(quadruped_small) => match quadruped_small.species {
699            quadruped_small::Species::TreantSapling => Some(Item::new_from_asset_expect(
700                "common.items.npc_weapons.unique.treantsapling",
701            )),
702            quadruped_small::Species::MossySnail => Some(Item::new_from_asset_expect(
703                "common.items.npc_weapons.unique.mossysnail",
704            )),
705            quadruped_small::Species::Boar | quadruped_small::Species::Truffler => Some(
706                Item::new_from_asset_expect("common.items.npc_weapons.unique.quadruped_small.boar"),
707            ),
708            quadruped_small::Species::Hyena => Some(Item::new_from_asset_expect(
709                "common.items.npc_weapons.unique.quadruped_small.hyena",
710            )),
711            quadruped_small::Species::Beaver
712            | quadruped_small::Species::Dog
713            | quadruped_small::Species::Cat
714            | quadruped_small::Species::Goat
715            | quadruped_small::Species::Holladon
716            | quadruped_small::Species::Sheep
717            | quadruped_small::Species::Seal => Some(Item::new_from_asset_expect(
718                "common.items.npc_weapons.unique.quadsmallbasic",
719            )),
720            _ => Some(Item::new_from_asset_expect(
721                "common.items.npc_weapons.unique.quadruped_small.rodent",
722            )),
723        },
724        Body::Theropod(theropod) => match theropod.species {
725            theropod::Species::Sandraptor
726            | theropod::Species::Snowraptor
727            | theropod::Species::Woodraptor
728            | theropod::Species::Axebeak
729            | theropod::Species::Sunlizard => Some(Item::new_from_asset_expect(
730                "common.items.npc_weapons.unique.theropodbird",
731            )),
732            theropod::Species::Yale => Some(Item::new_from_asset_expect(
733                "common.items.npc_weapons.unique.theropod.yale",
734            )),
735            theropod::Species::Dodarock => Some(Item::new_from_asset_expect(
736                "common.items.npc_weapons.unique.theropodsmall",
737            )),
738            _ => Some(Item::new_from_asset_expect(
739                "common.items.npc_weapons.unique.theropodbasic",
740            )),
741        },
742        Body::Arthropod(arthropod) => match arthropod.species {
743            arthropod::Species::Hornbeetle | arthropod::Species::Stagbeetle => {
744                Some(Item::new_from_asset_expect(
745                    "common.items.npc_weapons.unique.arthropods.hornbeetle",
746                ))
747            },
748            arthropod::Species::Emberfly => Some(Item::new_from_asset_expect(
749                "common.items.npc_weapons.unique.emberfly",
750            )),
751            arthropod::Species::Cavespider => Some(Item::new_from_asset_expect(
752                "common.items.npc_weapons.unique.arthropods.cavespider",
753            )),
754            arthropod::Species::Sandcrawler | arthropod::Species::Mosscrawler => {
755                Some(Item::new_from_asset_expect(
756                    "common.items.npc_weapons.unique.arthropods.mosscrawler",
757                ))
758            },
759            arthropod::Species::Moltencrawler => Some(Item::new_from_asset_expect(
760                "common.items.npc_weapons.unique.arthropods.moltencrawler",
761            )),
762            arthropod::Species::Weevil => Some(Item::new_from_asset_expect(
763                "common.items.npc_weapons.unique.arthropods.weevil",
764            )),
765            arthropod::Species::Blackwidow => Some(Item::new_from_asset_expect(
766                "common.items.npc_weapons.unique.arthropods.blackwidow",
767            )),
768            arthropod::Species::Tarantula => Some(Item::new_from_asset_expect(
769                "common.items.npc_weapons.unique.arthropods.tarantula",
770            )),
771            arthropod::Species::Antlion => Some(Item::new_from_asset_expect(
772                "common.items.npc_weapons.unique.arthropods.antlion",
773            )),
774            arthropod::Species::Dagonite => Some(Item::new_from_asset_expect(
775                "common.items.npc_weapons.unique.arthropods.dagonite",
776            )),
777            arthropod::Species::Leafbeetle => Some(Item::new_from_asset_expect(
778                "common.items.npc_weapons.unique.arthropods.leafbeetle",
779            )),
780        },
781        Body::BipedLarge(biped_large) => match (biped_large.species, biped_large.body_type) {
782            (biped_large::Species::Occultsaurok, _) => Some(Item::new_from_asset_expect(
783                "common.items.npc_weapons.staff.saurok_staff",
784            )),
785            (biped_large::Species::Mightysaurok, _) => Some(Item::new_from_asset_expect(
786                "common.items.npc_weapons.sword.saurok_sword",
787            )),
788            (biped_large::Species::Slysaurok, _) => Some(Item::new_from_asset_expect(
789                "common.items.npc_weapons.bow.saurok_bow",
790            )),
791            (biped_large::Species::Ogre, biped_large::BodyType::Male) => Some(
792                Item::new_from_asset_expect("common.items.npc_weapons.hammer.ogre_hammer"),
793            ),
794            (biped_large::Species::Ogre, biped_large::BodyType::Female) => Some(
795                Item::new_from_asset_expect("common.items.npc_weapons.staff.ogre_staff"),
796            ),
797            (
798                biped_large::Species::Mountaintroll
799                | biped_large::Species::Swamptroll
800                | biped_large::Species::Cavetroll,
801                _,
802            ) => Some(Item::new_from_asset_expect(
803                "common.items.npc_weapons.hammer.troll_hammer",
804            )),
805            (biped_large::Species::Wendigo, _) => Some(Item::new_from_asset_expect(
806                "common.items.npc_weapons.unique.wendigo_magic",
807            )),
808            (biped_large::Species::Werewolf, _) => Some(Item::new_from_asset_expect(
809                "common.items.npc_weapons.unique.beast_claws",
810            )),
811            (biped_large::Species::Tursus, _) => Some(Item::new_from_asset_expect(
812                "common.items.npc_weapons.unique.tursus_claws",
813            )),
814            (biped_large::Species::Cyclops, _) => Some(Item::new_from_asset_expect(
815                "common.items.npc_weapons.hammer.cyclops_hammer",
816            )),
817            (biped_large::Species::Dullahan, _) => Some(Item::new_from_asset_expect(
818                "common.items.npc_weapons.sword.dullahan_sword",
819            )),
820            (biped_large::Species::Mindflayer, _) => Some(Item::new_from_asset_expect(
821                "common.items.npc_weapons.staff.mindflayer_staff",
822            )),
823            (biped_large::Species::Minotaur, _) => Some(Item::new_from_asset_expect(
824                "common.items.npc_weapons.axe.minotaur_axe",
825            )),
826            (biped_large::Species::Tidalwarrior, _) => Some(Item::new_from_asset_expect(
827                "common.items.npc_weapons.unique.tidal_claws",
828            )),
829            (biped_large::Species::Yeti, _) => Some(Item::new_from_asset_expect(
830                "common.items.npc_weapons.hammer.yeti_hammer",
831            )),
832            (biped_large::Species::Harvester, _) => Some(Item::new_from_asset_expect(
833                "common.items.npc_weapons.hammer.harvester_scythe",
834            )),
835            (biped_large::Species::Blueoni, _) => Some(Item::new_from_asset_expect(
836                "common.items.npc_weapons.axe.oni_blue_axe",
837            )),
838            (biped_large::Species::Redoni, _) => Some(Item::new_from_asset_expect(
839                "common.items.npc_weapons.hammer.oni_red_hammer",
840            )),
841            (biped_large::Species::Cultistwarlord, _) => Some(Item::new_from_asset_expect(
842                "common.items.npc_weapons.sword.bipedlarge-cultist",
843            )),
844            (biped_large::Species::Cultistwarlock, _) => Some(Item::new_from_asset_expect(
845                "common.items.npc_weapons.staff.bipedlarge-cultist",
846            )),
847            (biped_large::Species::Huskbrute, _) => Some(Item::new_from_asset_expect(
848                "common.items.npc_weapons.unique.husk_brute",
849            )),
850            (biped_large::Species::Strigoi, _) => Some(Item::new_from_asset_expect(
851                "common.items.npc_weapons.unique.strigoi_claws",
852            )),
853            (biped_large::Species::Executioner, _) => Some(Item::new_from_asset_expect(
854                "common.items.npc_weapons.axe.executioner_axe",
855            )),
856            (biped_large::Species::Gigasfrost, _) => Some(Item::new_from_asset_expect(
857                "common.items.npc_weapons.axe.gigas_frost_axe",
858            )),
859            (biped_large::Species::AdletElder, _) => Some(Item::new_from_asset_expect(
860                "common.items.npc_weapons.sword.adlet_elder_sword",
861            )),
862            (biped_large::Species::SeaBishop, _) => Some(Item::new_from_asset_expect(
863                "common.items.npc_weapons.unique.sea_bishop_sceptre",
864            )),
865            (biped_large::Species::HaniwaGeneral, _) => Some(Item::new_from_asset_expect(
866                "common.items.npc_weapons.sword.haniwa_general_sword",
867            )),
868            (biped_large::Species::TerracottaBesieger, _) => Some(Item::new_from_asset_expect(
869                "common.items.npc_weapons.bow.terracotta_besieger_bow",
870            )),
871            (biped_large::Species::TerracottaDemolisher, _) => Some(Item::new_from_asset_expect(
872                "common.items.npc_weapons.unique.terracotta_demolisher_fist",
873            )),
874            (biped_large::Species::TerracottaPunisher, _) => Some(Item::new_from_asset_expect(
875                "common.items.npc_weapons.hammer.terracotta_punisher_club",
876            )),
877            (biped_large::Species::TerracottaPursuer, _) => Some(Item::new_from_asset_expect(
878                "common.items.npc_weapons.sword.terracotta_pursuer_sword",
879            )),
880            (biped_large::Species::Cursekeeper, _) => Some(Item::new_from_asset_expect(
881                "common.items.npc_weapons.unique.cursekeeper_sceptre",
882            )),
883            (biped_large::Species::Forgemaster, _) => Some(Item::new_from_asset_expect(
884                "common.items.npc_weapons.hammer.forgemaster_hammer",
885            )),
886        },
887        Body::Object(body) => match body {
888            object::Body::Crossbow => Some(Item::new_from_asset_expect(
889                "common.items.npc_weapons.unique.turret",
890            )),
891            object::Body::Flamethrower | object::Body::Lavathrower => Some(
892                Item::new_from_asset_expect("common.items.npc_weapons.unique.flamethrower"),
893            ),
894            object::Body::BarrelOrgan => Some(Item::new_from_asset_expect(
895                "common.items.npc_weapons.unique.organ",
896            )),
897            object::Body::TerracottaStatue => Some(Item::new_from_asset_expect(
898                "common.items.npc_weapons.unique.terracotta_statue",
899            )),
900            object::Body::HaniwaSentry => Some(Item::new_from_asset_expect(
901                "common.items.npc_weapons.unique.haniwa_sentry",
902            )),
903            object::Body::SeaLantern => Some(Item::new_from_asset_expect(
904                "common.items.npc_weapons.unique.tidal_totem",
905            )),
906            object::Body::Tornado => Some(Item::new_from_asset_expect(
907                "common.items.npc_weapons.unique.tornado",
908            )),
909            object::Body::FieryTornado => Some(Item::new_from_asset_expect(
910                "common.items.npc_weapons.unique.fiery_tornado",
911            )),
912            object::Body::GnarlingTotemRed => Some(Item::new_from_asset_expect(
913                "common.items.npc_weapons.biped_small.gnarling.redtotem",
914            )),
915            object::Body::GnarlingTotemGreen => Some(Item::new_from_asset_expect(
916                "common.items.npc_weapons.biped_small.gnarling.greentotem",
917            )),
918            object::Body::GnarlingTotemWhite => Some(Item::new_from_asset_expect(
919                "common.items.npc_weapons.biped_small.gnarling.whitetotem",
920            )),
921            _ => None,
922        },
923        Body::BipedSmall(biped_small) => match (biped_small.species, biped_small.body_type) {
924            (biped_small::Species::Gnome, _) => Some(Item::new_from_asset_expect(
925                "common.items.npc_weapons.biped_small.adlet.tracker",
926            )),
927            (biped_small::Species::Bushly, _) => Some(Item::new_from_asset_expect(
928                "common.items.npc_weapons.unique.bushly",
929            )),
930            (biped_small::Species::Cactid, _) => Some(Item::new_from_asset_expect(
931                "common.items.npc_weapons.unique.cactid",
932            )),
933            (biped_small::Species::Irrwurz, _) => Some(Item::new_from_asset_expect(
934                "common.items.npc_weapons.unique.irrwurz",
935            )),
936            (biped_small::Species::Husk, _) => Some(Item::new_from_asset_expect(
937                "common.items.npc_weapons.unique.husk",
938            )),
939            (biped_small::Species::Flamekeeper, _) => Some(Item::new_from_asset_expect(
940                "common.items.npc_weapons.unique.flamekeeper_staff",
941            )),
942            (biped_small::Species::IronDwarf, _) => Some(Item::new_from_asset_expect(
943                "common.items.npc_weapons.unique.iron_dwarf",
944            )),
945            (biped_small::Species::ShamanicSpirit, _) => Some(Item::new_from_asset_expect(
946                "common.items.npc_weapons.unique.shamanic_spirit",
947            )),
948            (biped_small::Species::Jiangshi, _) => Some(Item::new_from_asset_expect(
949                "common.items.npc_weapons.unique.jiangshi",
950            )),
951            (biped_small::Species::BloodmoonHeiress, _) => Some(Item::new_from_asset_expect(
952                "common.items.npc_weapons.biped_small.vampire.bloodmoon_heiress_sword",
953            )),
954            (biped_small::Species::Bloodservant, _) => Some(Item::new_from_asset_expect(
955                "common.items.npc_weapons.biped_small.vampire.bloodservant_axe",
956            )),
957            (biped_small::Species::Harlequin, _) => Some(Item::new_from_asset_expect(
958                "common.items.npc_weapons.biped_small.vampire.harlequin_dagger",
959            )),
960            (biped_small::Species::GoblinThug, _) => Some(Item::new_from_asset_expect(
961                "common.items.npc_weapons.unique.goblin_thug_club",
962            )),
963            (biped_small::Species::GoblinChucker, _) => Some(Item::new_from_asset_expect(
964                "common.items.npc_weapons.unique.goblin_chucker",
965            )),
966            (biped_small::Species::GoblinRuffian, _) => Some(Item::new_from_asset_expect(
967                "common.items.npc_weapons.unique.goblin_ruffian_knife",
968            )),
969            (biped_small::Species::GreenLegoom, _) => Some(Item::new_from_asset_expect(
970                "common.items.npc_weapons.unique.green_legoom_rake",
971            )),
972            (biped_small::Species::OchreLegoom, _) => Some(Item::new_from_asset_expect(
973                "common.items.npc_weapons.unique.ochre_legoom_spade",
974            )),
975            (biped_small::Species::PurpleLegoom, _) => Some(Item::new_from_asset_expect(
976                "common.items.npc_weapons.unique.purple_legoom_pitchfork",
977            )),
978            (biped_small::Species::RedLegoom, _) => Some(Item::new_from_asset_expect(
979                "common.items.npc_weapons.unique.red_legoom_hoe",
980            )),
981            _ => Some(Item::new_from_asset_expect(
982                "common.items.npc_weapons.biped_small.adlet.hunter",
983            )),
984        },
985        Body::BirdLarge(bird_large) => match (bird_large.species, bird_large.body_type) {
986            (bird_large::Species::Cockatrice, _) => Some(Item::new_from_asset_expect(
987                "common.items.npc_weapons.unique.birdlargebreathe",
988            )),
989            (bird_large::Species::Phoenix, _) => Some(Item::new_from_asset_expect(
990                "common.items.npc_weapons.unique.birdlargefire",
991            )),
992            (bird_large::Species::Roc, _) => Some(Item::new_from_asset_expect(
993                "common.items.npc_weapons.unique.birdlargebasic",
994            )),
995            (bird_large::Species::FlameWyvern, _) => Some(Item::new_from_asset_expect(
996                "common.items.npc_weapons.unique.flamewyvern",
997            )),
998            (bird_large::Species::FrostWyvern, _) => Some(Item::new_from_asset_expect(
999                "common.items.npc_weapons.unique.frostwyvern",
1000            )),
1001            (bird_large::Species::CloudWyvern, _) => Some(Item::new_from_asset_expect(
1002                "common.items.npc_weapons.unique.cloudwyvern",
1003            )),
1004            (bird_large::Species::SeaWyvern, _) => Some(Item::new_from_asset_expect(
1005                "common.items.npc_weapons.unique.seawyvern",
1006            )),
1007            (bird_large::Species::WealdWyvern, _) => Some(Item::new_from_asset_expect(
1008                "common.items.npc_weapons.unique.wealdwyvern",
1009            )),
1010        },
1011        Body::BirdMedium(bird_medium) => match bird_medium.species {
1012            bird_medium::Species::Cockatiel
1013            | bird_medium::Species::Bat
1014            | bird_medium::Species::Parrot
1015            | bird_medium::Species::Crow
1016            | bird_medium::Species::Parakeet => Some(Item::new_from_asset_expect(
1017                "common.items.npc_weapons.unique.simpleflyingbasic",
1018            )),
1019            bird_medium::Species::VampireBat => Some(Item::new_from_asset_expect(
1020                "common.items.npc_weapons.unique.vampire_bat",
1021            )),
1022            bird_medium::Species::BloodmoonBat => Some(Item::new_from_asset_expect(
1023                "common.items.npc_weapons.unique.bloodmoon_bat",
1024            )),
1025            _ => Some(Item::new_from_asset_expect(
1026                "common.items.npc_weapons.unique.birdmediumbasic",
1027            )),
1028        },
1029        Body::Crustacean(crustacean) => match crustacean.species {
1030            crustacean::Species::Crab | crustacean::Species::SoldierCrab => Some(
1031                Item::new_from_asset_expect("common.items.npc_weapons.unique.crab_pincer"),
1032            ),
1033            crustacean::Species::Karkatha => Some(Item::new_from_asset_expect(
1034                "common.items.npc_weapons.unique.karkatha_pincer",
1035            )),
1036        },
1037        _ => None,
1038    };
1039
1040    maybe_tool.unwrap_or_else(Item::empty)
1041}
1042
1043/// Builder for character Loadouts, containing weapon and armour items belonging
1044/// to a character, along with some helper methods for loading `Item`-s and
1045/// `ItemConfig`
1046///
1047/// ```
1048/// use veloren_common::{LoadoutBuilder, comp::Item};
1049///
1050/// // Build a loadout with character starter defaults
1051/// // and a specific sword with default sword abilities
1052/// let sword = Item::new_from_asset_expect("common.items.weapons.sword.starter");
1053/// let loadout = LoadoutBuilder::empty()
1054///     .defaults()
1055///     .active_mainhand(Some(sword))
1056///     .build();
1057/// ```
1058#[derive(Clone)]
1059pub struct LoadoutBuilder(Loadout);
1060
1061#[derive(Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Debug, EnumIter)]
1062pub enum Preset {
1063    HuskSummon,
1064    BorealSummon,
1065    IronDwarfSummon,
1066    ShamanicSpiritSummon,
1067    JiangshiSummon,
1068    BloodservantSummon,
1069}
1070
1071impl LoadoutBuilder {
1072    #[must_use]
1073    pub fn empty() -> Self { Self(Loadout::new_empty()) }
1074
1075    #[must_use]
1076    /// Construct new `LoadoutBuilder` from `asset_specifier`
1077    /// Will panic if asset is broken
1078    pub fn from_asset_expect(
1079        asset_specifier: &str,
1080        rng: &mut impl Rng,
1081        time: Option<&(TimeOfDay, Calendar)>,
1082    ) -> Self {
1083        Self::from_asset(asset_specifier, rng, time).expect("failed to load loadut config")
1084    }
1085
1086    /// Construct new `LoadoutBuilder` from `asset_specifier`
1087    pub fn from_asset(
1088        asset_specifier: &str,
1089        rng: &mut impl Rng,
1090        time: Option<&(TimeOfDay, Calendar)>,
1091    ) -> Result<Self, SpecError> {
1092        let loadout = Self::empty();
1093        loadout.with_asset(asset_specifier, rng, time)
1094    }
1095
1096    #[must_use]
1097    /// Construct new default `LoadoutBuilder` for corresponding `body`
1098    ///
1099    /// NOTE: make sure that you check what is default for this body
1100    /// Use it if you don't care much about it, for example in "/spawn" command
1101    pub fn from_default(body: &Body) -> Self {
1102        let loadout = Self::empty();
1103        loadout
1104            .with_default_maintool(body)
1105            .with_default_equipment(body)
1106    }
1107
1108    /// Construct new `LoadoutBuilder` from `asset_specifier`
1109    pub fn from_loadout_spec(
1110        loadout_spec: LoadoutSpec,
1111        rng: &mut impl Rng,
1112        time: Option<&(TimeOfDay, Calendar)>,
1113    ) -> Result<Self, SpecError> {
1114        let loadout = Self::empty();
1115        loadout.with_loadout_spec(loadout_spec, rng, time)
1116    }
1117
1118    #[must_use]
1119    /// Construct new `LoadoutBuilder` from `asset_specifier`
1120    ///
1121    /// Will panic if asset is broken
1122    pub fn from_loadout_spec_expect(
1123        loadout_spec: LoadoutSpec,
1124        rng: &mut impl Rng,
1125        time: Option<&(TimeOfDay, Calendar)>,
1126    ) -> Self {
1127        Self::from_loadout_spec(loadout_spec, rng, time).expect("failed to load loadout spec")
1128    }
1129
1130    #[must_use = "Method consumes builder and returns updated builder."]
1131    /// Set default active mainhand weapon based on `body`
1132    pub fn with_default_maintool(self, body: &Body) -> Self {
1133        self.active_mainhand(Some(default_main_tool(body)))
1134    }
1135
1136    #[must_use = "Method consumes builder and returns updated builder."]
1137    /// Set default equipement based on `body`
1138    pub fn with_default_equipment(self, body: &Body) -> Self {
1139        let chest = match body {
1140            Body::BipedLarge(body) => match body.species {
1141                biped_large::Species::Mindflayer => {
1142                    Some("common.items.npc_armor.biped_large.mindflayer")
1143                },
1144                biped_large::Species::Minotaur => {
1145                    Some("common.items.npc_armor.biped_large.minotaur")
1146                },
1147                biped_large::Species::Tidalwarrior => {
1148                    Some("common.items.npc_armor.biped_large.tidal_warrior")
1149                },
1150                biped_large::Species::Yeti => Some("common.items.npc_armor.biped_large.yeti"),
1151                biped_large::Species::Harvester => {
1152                    Some("common.items.npc_armor.biped_large.harvester")
1153                },
1154                biped_large::Species::Ogre
1155                | biped_large::Species::Blueoni
1156                | biped_large::Species::Redoni
1157                | biped_large::Species::Cavetroll
1158                | biped_large::Species::Mountaintroll
1159                | biped_large::Species::Swamptroll
1160                | biped_large::Species::Wendigo => {
1161                    Some("common.items.npc_armor.biped_large.generic")
1162                },
1163                biped_large::Species::Cyclops => Some("common.items.npc_armor.biped_large.cyclops"),
1164                biped_large::Species::Dullahan => {
1165                    Some("common.items.npc_armor.biped_large.dullahan")
1166                },
1167                biped_large::Species::Tursus => Some("common.items.npc_armor.biped_large.tursus"),
1168                biped_large::Species::Cultistwarlord => {
1169                    Some("common.items.npc_armor.biped_large.warlord")
1170                },
1171                biped_large::Species::Cultistwarlock => {
1172                    Some("common.items.npc_armor.biped_large.warlock")
1173                },
1174                biped_large::Species::Gigasfrost => {
1175                    Some("common.items.npc_armor.biped_large.gigas_frost")
1176                },
1177                biped_large::Species::HaniwaGeneral => {
1178                    Some("common.items.npc_armor.biped_large.haniwageneral")
1179                },
1180                biped_large::Species::TerracottaBesieger
1181                | biped_large::Species::TerracottaDemolisher
1182                | biped_large::Species::TerracottaPunisher
1183                | biped_large::Species::TerracottaPursuer
1184                | biped_large::Species::Cursekeeper => {
1185                    Some("common.items.npc_armor.biped_large.terracotta")
1186                },
1187                biped_large::Species::Forgemaster => {
1188                    Some("common.items.npc_armor.biped_large.forgemaster")
1189                },
1190                _ => None,
1191            },
1192            Body::BirdLarge(body) => match body.species {
1193                bird_large::Species::FlameWyvern
1194                | bird_large::Species::FrostWyvern
1195                | bird_large::Species::CloudWyvern
1196                | bird_large::Species::SeaWyvern
1197                | bird_large::Species::WealdWyvern => {
1198                    Some("common.items.npc_armor.bird_large.wyvern")
1199                },
1200                bird_large::Species::Phoenix => Some("common.items.npc_armor.bird_large.phoenix"),
1201                _ => None,
1202            },
1203            Body::BirdMedium(body) => match body.species {
1204                bird_medium::Species::BloodmoonBat => {
1205                    Some("common.items.npc_armor.bird_medium.bloodmoon_bat")
1206                },
1207                _ => None,
1208            },
1209            Body::Golem(body) => match body.species {
1210                golem::Species::ClayGolem => Some("common.items.npc_armor.golem.claygolem"),
1211                golem::Species::Gravewarden => Some("common.items.npc_armor.golem.gravewarden"),
1212                golem::Species::WoodGolem => Some("common.items.npc_armor.golem.woodgolem"),
1213                golem::Species::AncientEffigy => Some("common.items.npc_armor.golem.ancienteffigy"),
1214                golem::Species::Mogwai => Some("common.items.npc_armor.golem.mogwai"),
1215                golem::Species::IronGolem => Some("common.items.npc_armor.golem.irongolem"),
1216                _ => None,
1217            },
1218            Body::QuadrupedLow(body) => match body.species {
1219                quadruped_low::Species::Sandshark
1220                | quadruped_low::Species::Alligator
1221                | quadruped_low::Species::Crocodile
1222                | quadruped_low::Species::SeaCrocodile
1223                | quadruped_low::Species::Icedrake
1224                | quadruped_low::Species::Lavadrake
1225                | quadruped_low::Species::Mossdrake => Some("common.items.npc_armor.generic"),
1226                quadruped_low::Species::Reefsnapper
1227                | quadruped_low::Species::Rocksnapper
1228                | quadruped_low::Species::Rootsnapper
1229                | quadruped_low::Species::Tortoise
1230                | quadruped_low::Species::Basilisk
1231                | quadruped_low::Species::Hydra => Some("common.items.npc_armor.generic_high"),
1232                quadruped_low::Species::Dagon => Some("common.items.npc_armor.quadruped_low.dagon"),
1233                _ => None,
1234            },
1235            Body::QuadrupedMedium(body) => match body.species {
1236                quadruped_medium::Species::Bonerattler => Some("common.items.npc_armor.generic"),
1237                quadruped_medium::Species::Tarasque => Some("common.items.npc_armor.generic_high"),
1238                quadruped_medium::Species::ClaySteed => {
1239                    Some("common.items.npc_armor.quadruped_medium.claysteed")
1240                },
1241                _ => None,
1242            },
1243            Body::Theropod(body) => match body.species {
1244                theropod::Species::Archaeos | theropod::Species::Ntouka => {
1245                    Some("common.items.npc_armor.generic")
1246                },
1247                theropod::Species::Dodarock => Some("common.items.npc_armor.generic_high"),
1248                _ => None,
1249            },
1250            // TODO: Check over
1251            Body::Arthropod(body) => match body.species {
1252                arthropod::Species::Blackwidow
1253                | arthropod::Species::Cavespider
1254                | arthropod::Species::Emberfly
1255                | arthropod::Species::Moltencrawler
1256                | arthropod::Species::Mosscrawler
1257                | arthropod::Species::Sandcrawler
1258                | arthropod::Species::Tarantula => None,
1259                _ => Some("common.items.npc_armor.generic"),
1260            },
1261            Body::QuadrupedSmall(body) => match body.species {
1262                quadruped_small::Species::Turtle
1263                | quadruped_small::Species::Holladon
1264                | quadruped_small::Species::TreantSapling
1265                | quadruped_small::Species::MossySnail => Some("common.items.npc_armor.generic"),
1266                _ => None,
1267            },
1268            Body::Crustacean(body) => match body.species {
1269                crustacean::Species::Karkatha => Some("common.items.npc_armor.crustacean.karkatha"),
1270                _ => None,
1271            },
1272            _ => None,
1273        };
1274
1275        if let Some(chest) = chest {
1276            self.chest(Some(Item::new_from_asset_expect(chest)))
1277        } else {
1278            self
1279        }
1280    }
1281
1282    #[must_use = "Method consumes builder and returns updated builder."]
1283    pub fn with_preset(mut self, preset: Preset) -> Self {
1284        let rng = &mut rand::thread_rng();
1285        match preset {
1286            Preset::HuskSummon => {
1287                self = self.with_asset_expect("common.loadout.dungeon.cultist.husk", rng, None);
1288            },
1289            Preset::BorealSummon => {
1290                self =
1291                    self.with_asset_expect("common.loadout.world.boreal.boreal_warrior", rng, None);
1292            },
1293            Preset::IronDwarfSummon => {
1294                self = self.with_asset_expect(
1295                    "common.loadout.dungeon.dwarven_quarry.iron_dwarf",
1296                    rng,
1297                    None,
1298                );
1299            },
1300            Preset::ShamanicSpiritSummon => {
1301                self = self.with_asset_expect(
1302                    "common.loadout.dungeon.terracotta.shamanic_spirit",
1303                    rng,
1304                    None,
1305                );
1306            },
1307            Preset::JiangshiSummon => {
1308                self =
1309                    self.with_asset_expect("common.loadout.dungeon.terracotta.jiangshi", rng, None);
1310            },
1311            Preset::BloodservantSummon => {
1312                self = self.with_asset_expect(
1313                    "common.loadout.dungeon.vampire.bloodservant",
1314                    rng,
1315                    None,
1316                );
1317            },
1318        }
1319
1320        self
1321    }
1322
1323    #[must_use = "Method consumes builder and returns updated builder."]
1324    pub fn with_creator(
1325        mut self,
1326        creator: fn(
1327            LoadoutBuilder,
1328            Option<&SiteInformation>,
1329            time: Option<&(TimeOfDay, Calendar)>,
1330        ) -> LoadoutBuilder,
1331        economy: Option<&SiteInformation>,
1332        time: Option<&(TimeOfDay, Calendar)>,
1333    ) -> LoadoutBuilder {
1334        self = creator(self, economy, time);
1335
1336        self
1337    }
1338
1339    #[must_use = "Method consumes builder and returns updated builder."]
1340    fn with_loadout_spec<R: Rng>(
1341        mut self,
1342        spec: LoadoutSpec,
1343        rng: &mut R,
1344        time: Option<&(TimeOfDay, Calendar)>,
1345    ) -> Result<Self, SpecError> {
1346        // Include any inheritance
1347        let spec = spec.eval(rng)?;
1348
1349        // Utility function to unwrap our itemspec
1350        let mut to_item = |maybe_item: Option<ItemSpec>| {
1351            if let Some(item) = maybe_item {
1352                item.try_to_item(rng, time)
1353            } else {
1354                Ok(None)
1355            }
1356        };
1357
1358        let to_pair = |maybe_hands: Option<Hands>, rng: &mut R| {
1359            if let Some(hands) = maybe_hands {
1360                hands.try_to_pair(rng, time)
1361            } else {
1362                Ok((None, None))
1363            }
1364        };
1365
1366        // Place every item
1367        if let Some(item) = to_item(spec.head)? {
1368            self = self.head(Some(item));
1369        }
1370        if let Some(item) = to_item(spec.neck)? {
1371            self = self.neck(Some(item));
1372        }
1373        if let Some(item) = to_item(spec.shoulders)? {
1374            self = self.shoulder(Some(item));
1375        }
1376        if let Some(item) = to_item(spec.chest)? {
1377            self = self.chest(Some(item));
1378        }
1379        if let Some(item) = to_item(spec.gloves)? {
1380            self = self.hands(Some(item));
1381        }
1382        if let Some(item) = to_item(spec.ring1)? {
1383            self = self.ring1(Some(item));
1384        }
1385        if let Some(item) = to_item(spec.ring2)? {
1386            self = self.ring2(Some(item));
1387        }
1388        if let Some(item) = to_item(spec.back)? {
1389            self = self.back(Some(item));
1390        }
1391        if let Some(item) = to_item(spec.belt)? {
1392            self = self.belt(Some(item));
1393        }
1394        if let Some(item) = to_item(spec.legs)? {
1395            self = self.pants(Some(item));
1396        }
1397        if let Some(item) = to_item(spec.feet)? {
1398            self = self.feet(Some(item));
1399        }
1400        if let Some(item) = to_item(spec.tabard)? {
1401            self = self.tabard(Some(item));
1402        }
1403        if let Some(item) = to_item(spec.bag1)? {
1404            self = self.bag(ArmorSlot::Bag1, Some(item));
1405        }
1406        if let Some(item) = to_item(spec.bag2)? {
1407            self = self.bag(ArmorSlot::Bag2, Some(item));
1408        }
1409        if let Some(item) = to_item(spec.bag3)? {
1410            self = self.bag(ArmorSlot::Bag3, Some(item));
1411        }
1412        if let Some(item) = to_item(spec.bag4)? {
1413            self = self.bag(ArmorSlot::Bag4, Some(item));
1414        }
1415        if let Some(item) = to_item(spec.lantern)? {
1416            self = self.lantern(Some(item));
1417        }
1418        if let Some(item) = to_item(spec.glider)? {
1419            self = self.glider(Some(item));
1420        }
1421        let (active_mainhand, active_offhand) = to_pair(spec.active_hands, rng)?;
1422        if let Some(item) = active_mainhand {
1423            self = self.active_mainhand(Some(item));
1424        }
1425        if let Some(item) = active_offhand {
1426            self = self.active_offhand(Some(item));
1427        }
1428        let (inactive_mainhand, inactive_offhand) = to_pair(spec.inactive_hands, rng)?;
1429        if let Some(item) = inactive_mainhand {
1430            self = self.inactive_mainhand(Some(item));
1431        }
1432        if let Some(item) = inactive_offhand {
1433            self = self.inactive_offhand(Some(item));
1434        }
1435
1436        Ok(self)
1437    }
1438
1439    #[must_use = "Method consumes builder and returns updated builder."]
1440    pub fn with_asset(
1441        self,
1442        asset_specifier: &str,
1443        rng: &mut impl Rng,
1444        time: Option<&(TimeOfDay, Calendar)>,
1445    ) -> Result<Self, SpecError> {
1446        let spec =
1447            LoadoutSpec::load_cloned(asset_specifier).map_err(SpecError::LoadoutAssetError)?;
1448        self.with_loadout_spec(spec, rng, time)
1449    }
1450
1451    /// # Usage
1452    /// Creates new `LoadoutBuilder` with all field replaced from
1453    /// `asset_specifier` which corresponds to loadout config
1454    ///
1455    /// # Panics
1456    /// 1) Will panic if there is no asset with such `asset_specifier`
1457    /// 2) Will panic if path to item specified in loadout file doesn't exist
1458    #[must_use = "Method consumes builder and returns updated builder."]
1459    pub fn with_asset_expect(
1460        self,
1461        asset_specifier: &str,
1462        rng: &mut impl Rng,
1463        time: Option<&(TimeOfDay, Calendar)>,
1464    ) -> Self {
1465        self.with_asset(asset_specifier, rng, time)
1466            .expect("failed loading loadout config")
1467    }
1468
1469    /// Set default armor items for the loadout. This may vary with game
1470    /// updates, but should be safe defaults for a new character.
1471    #[must_use = "Method consumes builder and returns updated builder."]
1472    pub fn defaults(self) -> Self {
1473        let rng = &mut rand::thread_rng();
1474        self.with_asset_expect("common.loadout.default", rng, None)
1475    }
1476
1477    #[must_use = "Method consumes builder and returns updated builder."]
1478    fn with_equipment(mut self, equip_slot: EquipSlot, item: Option<Item>) -> Self {
1479        // Panic if item doesn't correspond to slot
1480        assert!(
1481            item.as_ref()
1482                .is_none_or(|item| equip_slot.can_hold(&item.kind()))
1483        );
1484
1485        // TODO: What if `with_equipment` is used twice for the same slot. Or defaults
1486        // include an item in this slot.
1487        // Used when creating a loadout, so time not needed as it is used to check when
1488        // stuff gets unequipped. A new loadout has never unequipped an item.
1489        let time = Time(0.0);
1490
1491        self.0.swap(equip_slot, item, time);
1492        self
1493    }
1494
1495    #[must_use = "Method consumes builder and returns updated builder."]
1496    fn with_armor(self, armor_slot: ArmorSlot, item: Option<Item>) -> Self {
1497        self.with_equipment(EquipSlot::Armor(armor_slot), item)
1498    }
1499
1500    #[must_use = "Method consumes builder and returns updated builder."]
1501    pub fn active_mainhand(self, item: Option<Item>) -> Self {
1502        self.with_equipment(EquipSlot::ActiveMainhand, item)
1503    }
1504
1505    #[must_use = "Method consumes builder and returns updated builder."]
1506    pub fn active_offhand(self, item: Option<Item>) -> Self {
1507        self.with_equipment(EquipSlot::ActiveOffhand, item)
1508    }
1509
1510    #[must_use = "Method consumes builder and returns updated builder."]
1511    pub fn inactive_mainhand(self, item: Option<Item>) -> Self {
1512        self.with_equipment(EquipSlot::InactiveMainhand, item)
1513    }
1514
1515    #[must_use = "Method consumes builder and returns updated builder."]
1516    pub fn inactive_offhand(self, item: Option<Item>) -> Self {
1517        self.with_equipment(EquipSlot::InactiveOffhand, item)
1518    }
1519
1520    #[must_use = "Method consumes builder and returns updated builder."]
1521    pub fn shoulder(self, item: Option<Item>) -> Self {
1522        self.with_armor(ArmorSlot::Shoulders, item)
1523    }
1524
1525    #[must_use = "Method consumes builder and returns updated builder."]
1526    pub fn chest(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Chest, item) }
1527
1528    #[must_use = "Method consumes builder and returns updated builder."]
1529    pub fn belt(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Belt, item) }
1530
1531    #[must_use = "Method consumes builder and returns updated builder."]
1532    pub fn hands(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Hands, item) }
1533
1534    #[must_use = "Method consumes builder and returns updated builder."]
1535    pub fn pants(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Legs, item) }
1536
1537    #[must_use = "Method consumes builder and returns updated builder."]
1538    pub fn feet(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Feet, item) }
1539
1540    #[must_use = "Method consumes builder and returns updated builder."]
1541    pub fn back(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Back, item) }
1542
1543    #[must_use = "Method consumes builder and returns updated builder."]
1544    pub fn ring1(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Ring1, item) }
1545
1546    #[must_use = "Method consumes builder and returns updated builder."]
1547    pub fn ring2(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Ring2, item) }
1548
1549    #[must_use = "Method consumes builder and returns updated builder."]
1550    pub fn neck(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Neck, item) }
1551
1552    #[must_use = "Method consumes builder and returns updated builder."]
1553    pub fn lantern(self, item: Option<Item>) -> Self {
1554        self.with_equipment(EquipSlot::Lantern, item)
1555    }
1556
1557    #[must_use = "Method consumes builder and returns updated builder."]
1558    pub fn glider(self, item: Option<Item>) -> Self { self.with_equipment(EquipSlot::Glider, item) }
1559
1560    #[must_use = "Method consumes builder and returns updated builder."]
1561    pub fn head(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Head, item) }
1562
1563    #[must_use = "Method consumes builder and returns updated builder."]
1564    pub fn tabard(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Tabard, item) }
1565
1566    #[must_use = "Method consumes builder and returns updated builder."]
1567    pub fn bag(self, which: ArmorSlot, item: Option<Item>) -> Self { self.with_armor(which, item) }
1568
1569    #[must_use]
1570    pub fn build(self) -> Loadout { self.0 }
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575    use super::*;
1576    use crate::comp::{self, Body};
1577    use rand::thread_rng;
1578    use strum::IntoEnumIterator;
1579
1580    // Testing different species
1581    //
1582    // Things that will be caught - invalid assets paths for
1583    // creating default main hand tool or equipment without config
1584    #[test]
1585    fn test_loadout_species() {
1586        macro_rules! test_species {
1587            // base case
1588            ($species:tt : $body:tt) => {
1589                let mut rng = thread_rng();
1590                for s in comp::$species::ALL_SPECIES.iter() {
1591                    let body = comp::$species::Body::random_with(&mut rng, s);
1592                    let female_body = comp::$species::Body {
1593                        body_type: comp::$species::BodyType::Female,
1594                        ..body
1595                    };
1596                    let male_body = comp::$species::Body {
1597                        body_type: comp::$species::BodyType::Male,
1598                        ..body
1599                    };
1600                    std::mem::drop(LoadoutBuilder::from_default(
1601                        &Body::$body(female_body),
1602                    ));
1603                    std::mem::drop(LoadoutBuilder::from_default(
1604                        &Body::$body(male_body),
1605                    ));
1606                }
1607            };
1608            // recursive call
1609            ($base:tt : $body:tt, $($species:tt : $nextbody:tt),+ $(,)?) => {
1610                test_species!($base: $body);
1611                test_species!($($species: $nextbody),+);
1612            }
1613        }
1614
1615        // See `[AllBodies](crate::comp::body::AllBodies)`
1616        test_species!(
1617            humanoid: Humanoid,
1618            quadruped_small: QuadrupedSmall,
1619            quadruped_medium: QuadrupedMedium,
1620            quadruped_low: QuadrupedLow,
1621            quadruped_small: QuadrupedSmall,
1622            bird_medium: BirdMedium,
1623            bird_large: BirdLarge,
1624            fish_small: FishSmall,
1625            fish_medium: FishMedium,
1626            biped_small: BipedSmall,
1627            biped_large: BipedLarge,
1628            theropod: Theropod,
1629            dragon: Dragon,
1630            golem: Golem,
1631            arthropod: Arthropod,
1632        );
1633    }
1634
1635    // Testing all loadout presets
1636    //
1637    // Things that will be catched - invalid assets paths
1638    #[test]
1639    fn test_loadout_presets() {
1640        for preset in Preset::iter() {
1641            drop(LoadoutBuilder::empty().with_preset(preset));
1642        }
1643    }
1644
1645    // It just loads every loadout asset and tries to validate them
1646    //
1647    // TODO: optimize by caching checks
1648    // Because of nature of inheritance of loadout specs,
1649    // we will check some loadout assets at least two times.
1650    // One for asset itself and second if it serves as a base for other asset.
1651    #[test]
1652    fn validate_all_loadout_assets() {
1653        let loadouts = assets::load_rec_dir::<LoadoutSpec>("common.loadout")
1654            .expect("failed to load loadout directory");
1655        for loadout_id in loadouts.read().ids() {
1656            let loadout =
1657                LoadoutSpec::load_cloned(loadout_id).expect("failed to load loadout asset");
1658            loadout
1659                .validate(vec![loadout_id.to_string()])
1660                .unwrap_or_else(|e| panic!("{loadout_id} is broken: {e:?}"));
1661        }
1662    }
1663
1664    // Basically test that our validation tests don't have false-positives
1665    #[test]
1666    fn test_valid_assets() {
1667        let loadouts = assets::load_rec_dir::<LoadoutSpec>("test.loadout.ok")
1668            .expect("failed to load loadout directory");
1669
1670        for loadout_id in loadouts.read().ids() {
1671            let loadout =
1672                LoadoutSpec::load_cloned(loadout_id).expect("failed to load loadout asset");
1673            loadout
1674                .validate(vec![loadout_id.to_string()])
1675                .unwrap_or_else(|e| panic!("{loadout_id} is broken: {e:?}"));
1676        }
1677    }
1678}