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)]
42pub enum 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)]
134pub enum 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)]
190pub enum 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    pub inherit: Option<Base>,
241    // Armor
242    pub head: Option<ItemSpec>,
243    pub neck: Option<ItemSpec>,
244    pub shoulders: Option<ItemSpec>,
245    pub chest: Option<ItemSpec>,
246    pub gloves: Option<ItemSpec>,
247    pub ring1: Option<ItemSpec>,
248    pub ring2: Option<ItemSpec>,
249    pub back: Option<ItemSpec>,
250    pub belt: Option<ItemSpec>,
251    pub legs: Option<ItemSpec>,
252    pub feet: Option<ItemSpec>,
253    pub tabard: Option<ItemSpec>,
254    pub bag1: Option<ItemSpec>,
255    pub bag2: Option<ItemSpec>,
256    pub bag3: Option<ItemSpec>,
257    pub bag4: Option<ItemSpec>,
258    pub lantern: Option<ItemSpec>,
259    pub glider: Option<ItemSpec>,
260    // Weapons
261    pub active_hands: Option<Hands>,
262    pub 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]
537pub fn default_chest(body: &Body) -> Option<&'static str> {
538    match body {
539        Body::BipedLarge(body) => match body.species {
540            biped_large::Species::Mindflayer => {
541                Some("common.items.npc_armor.biped_large.mindflayer")
542            },
543            biped_large::Species::Minotaur => Some("common.items.npc_armor.biped_large.minotaur"),
544            biped_large::Species::Tidalwarrior => {
545                Some("common.items.npc_armor.biped_large.tidal_warrior")
546            },
547            biped_large::Species::Yeti => Some("common.items.npc_armor.biped_large.yeti"),
548            biped_large::Species::Harvester => Some("common.items.npc_armor.biped_large.harvester"),
549            biped_large::Species::Ogre
550            | biped_large::Species::Blueoni
551            | biped_large::Species::Redoni
552            | biped_large::Species::Cavetroll
553            | biped_large::Species::Mountaintroll
554            | biped_large::Species::Swamptroll
555            | biped_large::Species::Wendigo => Some("common.items.npc_armor.biped_large.generic"),
556            biped_large::Species::Cyclops => Some("common.items.npc_armor.biped_large.cyclops"),
557            biped_large::Species::Dullahan => Some("common.items.npc_armor.biped_large.dullahan"),
558            biped_large::Species::Tursus => Some("common.items.npc_armor.biped_large.tursus"),
559            biped_large::Species::Cultistwarlord => {
560                Some("common.items.npc_armor.biped_large.warlord")
561            },
562            biped_large::Species::Cultistwarlock => {
563                Some("common.items.npc_armor.biped_large.warlock")
564            },
565            biped_large::Species::Gigasfrost => {
566                Some("common.items.npc_armor.biped_large.gigas_frost")
567            },
568            biped_large::Species::HaniwaGeneral => {
569                Some("common.items.npc_armor.biped_large.haniwageneral")
570            },
571            biped_large::Species::TerracottaBesieger
572            | biped_large::Species::TerracottaDemolisher
573            | biped_large::Species::TerracottaPunisher
574            | biped_large::Species::TerracottaPursuer
575            | biped_large::Species::Cursekeeper => {
576                Some("common.items.npc_armor.biped_large.terracotta")
577            },
578            biped_large::Species::Forgemaster => {
579                Some("common.items.npc_armor.biped_large.forgemaster")
580            },
581            _ => None,
582        },
583        Body::BirdLarge(body) => match body.species {
584            bird_large::Species::FlameWyvern
585            | bird_large::Species::FrostWyvern
586            | bird_large::Species::CloudWyvern
587            | bird_large::Species::SeaWyvern
588            | bird_large::Species::WealdWyvern => Some("common.items.npc_armor.bird_large.wyvern"),
589            bird_large::Species::Phoenix => Some("common.items.npc_armor.bird_large.phoenix"),
590            _ => None,
591        },
592        Body::BirdMedium(body) => match body.species {
593            bird_medium::Species::BloodmoonBat => {
594                Some("common.items.npc_armor.bird_medium.bloodmoon_bat")
595            },
596            _ => None,
597        },
598        Body::Golem(body) => match body.species {
599            golem::Species::ClayGolem => Some("common.items.npc_armor.golem.claygolem"),
600            golem::Species::Gravewarden => Some("common.items.npc_armor.golem.gravewarden"),
601            golem::Species::WoodGolem => Some("common.items.npc_armor.golem.woodgolem"),
602            golem::Species::AncientEffigy => Some("common.items.npc_armor.golem.ancienteffigy"),
603            golem::Species::Mogwai => Some("common.items.npc_armor.golem.mogwai"),
604            golem::Species::IronGolem => Some("common.items.npc_armor.golem.irongolem"),
605            _ => None,
606        },
607        Body::QuadrupedLow(body) => match body.species {
608            quadruped_low::Species::Sandshark
609            | quadruped_low::Species::Alligator
610            | quadruped_low::Species::Crocodile
611            | quadruped_low::Species::SeaCrocodile
612            | quadruped_low::Species::Icedrake
613            | quadruped_low::Species::Lavadrake
614            | quadruped_low::Species::Mossdrake => Some("common.items.npc_armor.generic"),
615            quadruped_low::Species::Reefsnapper
616            | quadruped_low::Species::Rocksnapper
617            | quadruped_low::Species::Rootsnapper
618            | quadruped_low::Species::Tortoise
619            | quadruped_low::Species::Basilisk
620            | quadruped_low::Species::Hydra => Some("common.items.npc_armor.generic_high"),
621            quadruped_low::Species::Dagon => Some("common.items.npc_armor.quadruped_low.dagon"),
622            _ => None,
623        },
624        Body::QuadrupedMedium(body) => match body.species {
625            quadruped_medium::Species::Bonerattler => Some("common.items.npc_armor.generic"),
626            quadruped_medium::Species::Tarasque => Some("common.items.npc_armor.generic_high"),
627            quadruped_medium::Species::ClaySteed => {
628                Some("common.items.npc_armor.quadruped_medium.claysteed")
629            },
630            _ => None,
631        },
632        Body::Theropod(body) => match body.species {
633            theropod::Species::Archaeos | theropod::Species::Ntouka => {
634                Some("common.items.npc_armor.generic")
635            },
636            theropod::Species::Dodarock => Some("common.items.npc_armor.generic_high"),
637            _ => None,
638        },
639        // TODO: Check over
640        Body::Arthropod(body) => match body.species {
641            arthropod::Species::Blackwidow
642            | arthropod::Species::Cavespider
643            | arthropod::Species::Emberfly
644            | arthropod::Species::Moltencrawler
645            | arthropod::Species::Mosscrawler
646            | arthropod::Species::Sandcrawler
647            | arthropod::Species::Tarantula => None,
648            _ => Some("common.items.npc_armor.generic"),
649        },
650        Body::QuadrupedSmall(body) => match body.species {
651            quadruped_small::Species::Turtle
652            | quadruped_small::Species::Holladon
653            | quadruped_small::Species::TreantSapling
654            | quadruped_small::Species::MossySnail => Some("common.items.npc_armor.generic"),
655            _ => None,
656        },
657        Body::Crustacean(body) => match body.species {
658            crustacean::Species::Karkatha => Some("common.items.npc_armor.crustacean.karkatha"),
659            _ => None,
660        },
661        _ => None,
662    }
663}
664
665#[must_use]
666// We have many species so this function is long
667// Also we are using default tools for un-specified species so
668// it's fine to have wildcards
669#[expect(clippy::too_many_lines, clippy::match_wildcard_for_single_variants)]
670pub fn default_main_tool(body: &Body) -> Option<&'static str> {
671    match body {
672        Body::Golem(golem) => match golem.species {
673            golem::Species::StoneGolem => Some("common.items.npc_weapons.unique.stone_golems_fist"),
674            golem::Species::ClayGolem => Some("common.items.npc_weapons.unique.clay_golem_fist"),
675            golem::Species::Gravewarden => Some("common.items.npc_weapons.unique.gravewarden_fist"),
676            golem::Species::WoodGolem => Some("common.items.npc_weapons.unique.wood_golem_fist"),
677            golem::Species::CoralGolem => Some("common.items.npc_weapons.unique.coral_golem_fist"),
678            golem::Species::AncientEffigy => {
679                Some("common.items.npc_weapons.unique.ancient_effigy_eyes")
680            },
681            golem::Species::Mogwai => Some("common.items.npc_weapons.unique.mogwai"),
682            golem::Species::IronGolem => Some("common.items.npc_weapons.unique.iron_golem_fist"),
683            _ => None,
684        },
685        Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
686            quadruped_medium::Species::Wolf => {
687                Some("common.items.npc_weapons.unique.quadruped_medium.wolf")
688            },
689            // Below uniques still follow quadmedhoof just with stat alterations
690            quadruped_medium::Species::Alpaca | quadruped_medium::Species::Llama => {
691                Some("common.items.npc_weapons.unique.quadruped_medium.alpaca")
692            },
693            quadruped_medium::Species::Antelope | quadruped_medium::Species::Deer => {
694                Some("common.items.npc_weapons.unique.quadruped_medium.antelope")
695            },
696            quadruped_medium::Species::Donkey | quadruped_medium::Species::Zebra => {
697                Some("common.items.npc_weapons.unique.quadruped_medium.donkey")
698            },
699            // Provide Kelpie with unique water-centered abilities
700            quadruped_medium::Species::Horse | quadruped_medium::Species::Kelpie => {
701                Some("common.items.npc_weapons.unique.quadruped_medium.horse")
702            },
703            quadruped_medium::Species::ClaySteed => {
704                Some("common.items.npc_weapons.unique.claysteed")
705            },
706            quadruped_medium::Species::Saber
707            | quadruped_medium::Species::Bonerattler
708            | quadruped_medium::Species::Lion
709            | quadruped_medium::Species::Snowleopard => {
710                Some("common.items.npc_weapons.unique.quadmedjump")
711            },
712            quadruped_medium::Species::Darkhound => {
713                Some("common.items.npc_weapons.unique.darkhound")
714            },
715            // Below uniques still follow quadmedcharge just with stat alterations
716            quadruped_medium::Species::Moose | quadruped_medium::Species::Tuskram => {
717                Some("common.items.npc_weapons.unique.quadruped_medium.moose")
718            },
719            quadruped_medium::Species::Mouflon => {
720                Some("common.items.npc_weapons.unique.quadruped_medium.mouflon")
721            },
722            quadruped_medium::Species::Akhlut
723            | quadruped_medium::Species::Dreadhorn
724            | quadruped_medium::Species::Mammoth
725            | quadruped_medium::Species::Ngoubou => {
726                Some("common.items.npc_weapons.unique.quadmedcharge")
727            },
728            quadruped_medium::Species::Grolgar => {
729                Some("common.items.npc_weapons.unique.quadruped_medium.grolgar")
730            },
731            quadruped_medium::Species::Roshwalr => Some("common.items.npc_weapons.unique.roshwalr"),
732            quadruped_medium::Species::Cattle => {
733                Some("common.items.npc_weapons.unique.quadmedbasicgentle")
734            },
735            quadruped_medium::Species::Highland | quadruped_medium::Species::Yak => {
736                Some("common.items.npc_weapons.unique.quadruped_medium.highland")
737            },
738            quadruped_medium::Species::Frostfang => {
739                Some("common.items.npc_weapons.unique.frostfang")
740            },
741            _ => Some("common.items.npc_weapons.unique.quadmedbasic"),
742        },
743        Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
744            quadruped_low::Species::Maneater => {
745                Some("common.items.npc_weapons.unique.quadruped_low.maneater")
746            },
747            quadruped_low::Species::Asp => {
748                Some("common.items.npc_weapons.unique.quadruped_low.asp")
749            },
750            quadruped_low::Species::Dagon => Some("common.items.npc_weapons.unique.dagon"),
751            quadruped_low::Species::Snaretongue => {
752                Some("common.items.npc_weapons.unique.snaretongue")
753            },
754            quadruped_low::Species::Crocodile
755            | quadruped_low::Species::SeaCrocodile
756            | quadruped_low::Species::Alligator
757            | quadruped_low::Species::Salamander
758            | quadruped_low::Species::Elbst => Some("common.items.npc_weapons.unique.quadlowtail"),
759            quadruped_low::Species::Monitor | quadruped_low::Species::Pangolin => {
760                Some("common.items.npc_weapons.unique.quadlowquick")
761            },
762            quadruped_low::Species::Lavadrake => {
763                Some("common.items.npc_weapons.unique.quadruped_low.lavadrake")
764            },
765            quadruped_low::Species::Deadwood => {
766                Some("common.items.npc_weapons.unique.quadruped_low.deadwood")
767            },
768            quadruped_low::Species::Basilisk => {
769                Some("common.items.npc_weapons.unique.quadruped_low.basilisk")
770            },
771            quadruped_low::Species::Icedrake => {
772                Some("common.items.npc_weapons.unique.quadruped_low.icedrake")
773            },
774            quadruped_low::Species::Hakulaq => {
775                Some("common.items.npc_weapons.unique.quadruped_low.hakulaq")
776            },
777            quadruped_low::Species::Tortoise => {
778                Some("common.items.npc_weapons.unique.quadruped_low.tortoise")
779            },
780            quadruped_low::Species::Driggle => Some("common.items.npc_weapons.unique.driggle"),
781            quadruped_low::Species::Rocksnapper => {
782                Some("common.items.npc_weapons.unique.rocksnapper")
783            },
784            quadruped_low::Species::Hydra => {
785                Some("common.items.npc_weapons.unique.quadruped_low.hydra")
786            },
787            _ => Some("common.items.npc_weapons.unique.quadlowbasic"),
788        },
789        Body::QuadrupedSmall(quadruped_small) => match quadruped_small.species {
790            quadruped_small::Species::TreantSapling => {
791                Some("common.items.npc_weapons.unique.treantsapling")
792            },
793            quadruped_small::Species::MossySnail => {
794                Some("common.items.npc_weapons.unique.mossysnail")
795            },
796            quadruped_small::Species::Boar | quadruped_small::Species::Truffler => {
797                Some("common.items.npc_weapons.unique.quadruped_small.boar")
798            },
799            quadruped_small::Species::Hyena => {
800                Some("common.items.npc_weapons.unique.quadruped_small.hyena")
801            },
802            quadruped_small::Species::Beaver
803            | quadruped_small::Species::Dog
804            | quadruped_small::Species::Cat
805            | quadruped_small::Species::Goat
806            | quadruped_small::Species::Holladon
807            | quadruped_small::Species::Sheep
808            | quadruped_small::Species::Seal => {
809                Some("common.items.npc_weapons.unique.quadsmallbasic")
810            },
811            _ => Some("common.items.npc_weapons.unique.quadruped_small.rodent"),
812        },
813        Body::Theropod(theropod) => match theropod.species {
814            theropod::Species::Sandraptor
815            | theropod::Species::Snowraptor
816            | theropod::Species::Woodraptor
817            | theropod::Species::Axebeak
818            | theropod::Species::Sunlizard => Some("common.items.npc_weapons.unique.theropodbird"),
819            theropod::Species::Yale => Some("common.items.npc_weapons.unique.theropod.yale"),
820            theropod::Species::Dodarock => Some("common.items.npc_weapons.unique.theropodsmall"),
821            _ => Some("common.items.npc_weapons.unique.theropodbasic"),
822        },
823        Body::Arthropod(arthropod) => match arthropod.species {
824            arthropod::Species::Hornbeetle | arthropod::Species::Stagbeetle => {
825                Some("common.items.npc_weapons.unique.arthropods.hornbeetle")
826            },
827            arthropod::Species::Emberfly => Some("common.items.npc_weapons.unique.emberfly"),
828            arthropod::Species::Cavespider => {
829                Some("common.items.npc_weapons.unique.arthropods.cavespider")
830            },
831            arthropod::Species::Sandcrawler | arthropod::Species::Mosscrawler => {
832                Some("common.items.npc_weapons.unique.arthropods.mosscrawler")
833            },
834            arthropod::Species::Moltencrawler => {
835                Some("common.items.npc_weapons.unique.arthropods.moltencrawler")
836            },
837            arthropod::Species::Weevil => Some("common.items.npc_weapons.unique.arthropods.weevil"),
838            arthropod::Species::Blackwidow => {
839                Some("common.items.npc_weapons.unique.arthropods.blackwidow")
840            },
841            arthropod::Species::Tarantula => {
842                Some("common.items.npc_weapons.unique.arthropods.tarantula")
843            },
844            arthropod::Species::Antlion => {
845                Some("common.items.npc_weapons.unique.arthropods.antlion")
846            },
847            arthropod::Species::Dagonite => {
848                Some("common.items.npc_weapons.unique.arthropods.dagonite")
849            },
850            arthropod::Species::Leafbeetle => {
851                Some("common.items.npc_weapons.unique.arthropods.leafbeetle")
852            },
853        },
854        Body::BipedLarge(biped_large) => match (biped_large.species, biped_large.body_type) {
855            (biped_large::Species::Occultsaurok, _) => {
856                Some("common.items.npc_weapons.staff.saurok_staff")
857            },
858            (biped_large::Species::Mightysaurok, _) => {
859                Some("common.items.npc_weapons.sword.saurok_sword")
860            },
861            (biped_large::Species::Slysaurok, _) => Some("common.items.npc_weapons.bow.saurok_bow"),
862            (biped_large::Species::Ogre, biped_large::BodyType::Male) => {
863                Some("common.items.npc_weapons.hammer.ogre_hammer")
864            },
865            (biped_large::Species::Ogre, biped_large::BodyType::Female) => {
866                Some("common.items.npc_weapons.staff.ogre_staff")
867            },
868            (
869                biped_large::Species::Mountaintroll
870                | biped_large::Species::Swamptroll
871                | biped_large::Species::Cavetroll,
872                _,
873            ) => Some("common.items.npc_weapons.hammer.troll_hammer"),
874            (biped_large::Species::Wendigo, _) => {
875                Some("common.items.npc_weapons.unique.wendigo_magic")
876            },
877            (biped_large::Species::Werewolf, _) => {
878                Some("common.items.npc_weapons.unique.beast_claws")
879            },
880            (biped_large::Species::Tursus, _) => {
881                Some("common.items.npc_weapons.unique.tursus_claws")
882            },
883            (biped_large::Species::Cyclops, _) => {
884                Some("common.items.npc_weapons.hammer.cyclops_hammer")
885            },
886            (biped_large::Species::Dullahan, _) => {
887                Some("common.items.npc_weapons.sword.dullahan_sword")
888            },
889            (biped_large::Species::Mindflayer, _) => {
890                Some("common.items.npc_weapons.staff.mindflayer_staff")
891            },
892            (biped_large::Species::Minotaur, _) => {
893                Some("common.items.npc_weapons.axe.minotaur_axe")
894            },
895            (biped_large::Species::Tidalwarrior, _) => {
896                Some("common.items.npc_weapons.unique.tidal_claws")
897            },
898            (biped_large::Species::Yeti, _) => Some("common.items.npc_weapons.hammer.yeti_hammer"),
899            (biped_large::Species::Harvester, _) => {
900                Some("common.items.npc_weapons.hammer.harvester_scythe")
901            },
902            (biped_large::Species::Blueoni, _) => Some("common.items.npc_weapons.axe.oni_blue_axe"),
903            (biped_large::Species::Redoni, _) => {
904                Some("common.items.npc_weapons.hammer.oni_red_hammer")
905            },
906            (biped_large::Species::Cultistwarlord, _) => {
907                Some("common.items.npc_weapons.sword.bipedlarge-cultist")
908            },
909            (biped_large::Species::Cultistwarlock, _) => {
910                Some("common.items.npc_weapons.staff.bipedlarge-cultist")
911            },
912            (biped_large::Species::Huskbrute, _) => {
913                Some("common.items.npc_weapons.unique.husk_brute")
914            },
915            (biped_large::Species::Strigoi, _) => {
916                Some("common.items.npc_weapons.unique.strigoi_claws")
917            },
918            (biped_large::Species::Executioner, _) => {
919                Some("common.items.npc_weapons.axe.executioner_axe")
920            },
921            (biped_large::Species::Gigasfrost, _) => {
922                Some("common.items.npc_weapons.axe.gigas_frost_axe")
923            },
924            (biped_large::Species::AdletElder, _) => {
925                Some("common.items.npc_weapons.sword.adlet_elder_sword")
926            },
927            (biped_large::Species::SeaBishop, _) => {
928                Some("common.items.npc_weapons.unique.sea_bishop_sceptre")
929            },
930            (biped_large::Species::HaniwaGeneral, _) => {
931                Some("common.items.npc_weapons.sword.haniwa_general_sword")
932            },
933            (biped_large::Species::TerracottaBesieger, _) => {
934                Some("common.items.npc_weapons.bow.terracotta_besieger_bow")
935            },
936            (biped_large::Species::TerracottaDemolisher, _) => {
937                Some("common.items.npc_weapons.unique.terracotta_demolisher_fist")
938            },
939            (biped_large::Species::TerracottaPunisher, _) => {
940                Some("common.items.npc_weapons.hammer.terracotta_punisher_club")
941            },
942            (biped_large::Species::TerracottaPursuer, _) => {
943                Some("common.items.npc_weapons.sword.terracotta_pursuer_sword")
944            },
945            (biped_large::Species::Cursekeeper, _) => {
946                Some("common.items.npc_weapons.unique.cursekeeper_sceptre")
947            },
948            (biped_large::Species::Forgemaster, _) => {
949                Some("common.items.npc_weapons.hammer.forgemaster_hammer")
950            },
951        },
952        Body::Object(body) => match body {
953            object::Body::Crossbow => Some("common.items.npc_weapons.unique.turret"),
954            object::Body::Flamethrower | object::Body::Lavathrower => {
955                Some("common.items.npc_weapons.unique.flamethrower")
956            },
957            object::Body::BarrelOrgan => Some("common.items.npc_weapons.unique.organ"),
958            object::Body::TerracottaStatue => {
959                Some("common.items.npc_weapons.unique.terracotta_statue")
960            },
961            object::Body::HaniwaSentry => Some("common.items.npc_weapons.unique.haniwa_sentry"),
962            object::Body::SeaLantern => Some("common.items.npc_weapons.unique.tidal_totem"),
963            object::Body::Tornado => Some("common.items.npc_weapons.unique.tornado"),
964            object::Body::FieryTornado => Some("common.items.npc_weapons.unique.fiery_tornado"),
965            object::Body::GnarlingTotemRed => {
966                Some("common.items.npc_weapons.biped_small.gnarling.redtotem")
967            },
968            object::Body::GnarlingTotemGreen => {
969                Some("common.items.npc_weapons.biped_small.gnarling.greentotem")
970            },
971            object::Body::GnarlingTotemWhite => {
972                Some("common.items.npc_weapons.biped_small.gnarling.whitetotem")
973            },
974            _ => None,
975        },
976        Body::BipedSmall(biped_small) => match (biped_small.species, biped_small.body_type) {
977            (biped_small::Species::Gnome, _) => {
978                Some("common.items.npc_weapons.biped_small.adlet.tracker")
979            },
980            (biped_small::Species::Bushly, _) => Some("common.items.npc_weapons.unique.bushly"),
981            (biped_small::Species::Cactid, _) => Some("common.items.npc_weapons.unique.cactid"),
982            (biped_small::Species::Irrwurz, _) => Some("common.items.npc_weapons.unique.irrwurz"),
983            (biped_small::Species::Husk, _) => Some("common.items.npc_weapons.unique.husk"),
984            (biped_small::Species::Flamekeeper, _) => {
985                Some("common.items.npc_weapons.unique.flamekeeper_staff")
986            },
987            (biped_small::Species::IronDwarf, _) => {
988                Some("common.items.npc_weapons.unique.iron_dwarf")
989            },
990            (biped_small::Species::ShamanicSpirit, _) => {
991                Some("common.items.npc_weapons.unique.shamanic_spirit")
992            },
993            (biped_small::Species::Jiangshi, _) => Some("common.items.npc_weapons.unique.jiangshi"),
994            (biped_small::Species::BloodmoonHeiress, _) => {
995                Some("common.items.npc_weapons.biped_small.vampire.bloodmoon_heiress_sword")
996            },
997            (biped_small::Species::Bloodservant, _) => {
998                Some("common.items.npc_weapons.biped_small.vampire.bloodservant_axe")
999            },
1000            (biped_small::Species::Harlequin, _) => {
1001                Some("common.items.npc_weapons.biped_small.vampire.harlequin_dagger")
1002            },
1003            (biped_small::Species::GoblinThug, _) => {
1004                Some("common.items.npc_weapons.unique.goblin_thug_club")
1005            },
1006            (biped_small::Species::GoblinChucker, _) => {
1007                Some("common.items.npc_weapons.unique.goblin_chucker")
1008            },
1009            (biped_small::Species::GoblinRuffian, _) => {
1010                Some("common.items.npc_weapons.unique.goblin_ruffian_knife")
1011            },
1012            (biped_small::Species::GreenLegoom, _) => {
1013                Some("common.items.npc_weapons.unique.green_legoom_rake")
1014            },
1015            (biped_small::Species::OchreLegoom, _) => {
1016                Some("common.items.npc_weapons.unique.ochre_legoom_spade")
1017            },
1018            (biped_small::Species::PurpleLegoom, _) => {
1019                Some("common.items.npc_weapons.unique.purple_legoom_pitchfork")
1020            },
1021            (biped_small::Species::RedLegoom, _) => {
1022                Some("common.items.npc_weapons.unique.red_legoom_hoe")
1023            },
1024            _ => Some("common.items.npc_weapons.biped_small.adlet.hunter"),
1025        },
1026        Body::BirdLarge(bird_large) => match (bird_large.species, bird_large.body_type) {
1027            (bird_large::Species::Cockatrice, _) => {
1028                Some("common.items.npc_weapons.unique.birdlargebreathe")
1029            },
1030            (bird_large::Species::Phoenix, _) => {
1031                Some("common.items.npc_weapons.unique.birdlargefire")
1032            },
1033            (bird_large::Species::Roc, _) => Some("common.items.npc_weapons.unique.birdlargebasic"),
1034            (bird_large::Species::FlameWyvern, _) => {
1035                Some("common.items.npc_weapons.unique.flamewyvern")
1036            },
1037            (bird_large::Species::FrostWyvern, _) => {
1038                Some("common.items.npc_weapons.unique.frostwyvern")
1039            },
1040            (bird_large::Species::CloudWyvern, _) => {
1041                Some("common.items.npc_weapons.unique.cloudwyvern")
1042            },
1043            (bird_large::Species::SeaWyvern, _) => {
1044                Some("common.items.npc_weapons.unique.seawyvern")
1045            },
1046            (bird_large::Species::WealdWyvern, _) => {
1047                Some("common.items.npc_weapons.unique.wealdwyvern")
1048            },
1049        },
1050        Body::BirdMedium(bird_medium) => match bird_medium.species {
1051            bird_medium::Species::Cockatiel
1052            | bird_medium::Species::Bat
1053            | bird_medium::Species::Parrot
1054            | bird_medium::Species::Crow
1055            | bird_medium::Species::Parakeet => {
1056                Some("common.items.npc_weapons.unique.simpleflyingbasic")
1057            },
1058            bird_medium::Species::VampireBat => Some("common.items.npc_weapons.unique.vampire_bat"),
1059            bird_medium::Species::BloodmoonBat => {
1060                Some("common.items.npc_weapons.unique.bloodmoon_bat")
1061            },
1062            _ => Some("common.items.npc_weapons.unique.birdmediumbasic"),
1063        },
1064        Body::Crustacean(crustacean) => match crustacean.species {
1065            crustacean::Species::Crab | crustacean::Species::SoldierCrab => {
1066                Some("common.items.npc_weapons.unique.crab_pincer")
1067            },
1068            crustacean::Species::Karkatha => {
1069                Some("common.items.npc_weapons.unique.karkatha_pincer")
1070            },
1071        },
1072        _ => None,
1073    }
1074}
1075
1076/// Builder for character Loadouts, containing weapon and armour items belonging
1077/// to a character, along with some helper methods for loading `Item`-s and
1078/// `ItemConfig`
1079///
1080/// ```
1081/// use veloren_common::{LoadoutBuilder, comp::Item};
1082///
1083/// // Build a loadout with character starter defaults
1084/// // and a specific sword with default sword abilities
1085/// let sword = Item::new_from_asset_expect("common.items.weapons.sword.starter");
1086/// let loadout = LoadoutBuilder::empty()
1087///     .defaults()
1088///     .active_mainhand(Some(sword))
1089///     .build();
1090/// ```
1091#[derive(Clone)]
1092pub struct LoadoutBuilder(Loadout);
1093
1094#[derive(Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Debug, EnumIter)]
1095pub enum Preset {
1096    HuskSummon,
1097    BorealSummon,
1098    IronDwarfSummon,
1099    ShamanicSpiritSummon,
1100    JiangshiSummon,
1101    BloodservantSummon,
1102}
1103
1104impl LoadoutBuilder {
1105    #[must_use]
1106    pub fn empty() -> Self { Self(Loadout::new_empty()) }
1107
1108    #[must_use]
1109    /// Construct new `LoadoutBuilder` from `asset_specifier`
1110    /// Will panic if asset is broken
1111    pub fn from_asset_expect(
1112        asset_specifier: &str,
1113        rng: &mut impl Rng,
1114        time: Option<&(TimeOfDay, Calendar)>,
1115    ) -> Self {
1116        Self::from_asset(asset_specifier, rng, time).expect("failed to load loadut config")
1117    }
1118
1119    /// Construct new `LoadoutBuilder` from `asset_specifier`
1120    pub fn from_asset(
1121        asset_specifier: &str,
1122        rng: &mut impl Rng,
1123        time: Option<&(TimeOfDay, Calendar)>,
1124    ) -> Result<Self, SpecError> {
1125        let loadout = Self::empty();
1126        loadout.with_asset(asset_specifier, rng, time)
1127    }
1128
1129    #[must_use]
1130    /// Construct new default `LoadoutBuilder` for corresponding `body`
1131    ///
1132    /// NOTE: make sure that you check what is default for this body
1133    /// Use it if you don't care much about it, for example in "/spawn" command
1134    pub fn from_default(body: &Body) -> Self {
1135        let loadout = Self::empty();
1136        loadout
1137            .with_default_maintool(body)
1138            .with_default_equipment(body)
1139    }
1140
1141    /// Construct new `LoadoutBuilder` from `asset_specifier`
1142    pub fn from_loadout_spec(
1143        loadout_spec: LoadoutSpec,
1144        rng: &mut impl Rng,
1145        time: Option<&(TimeOfDay, Calendar)>,
1146    ) -> Result<Self, SpecError> {
1147        let loadout = Self::empty();
1148        loadout.with_loadout_spec(loadout_spec, rng, time)
1149    }
1150
1151    #[must_use]
1152    /// Construct new `LoadoutBuilder` from `asset_specifier`
1153    ///
1154    /// Will panic if asset is broken
1155    pub fn from_loadout_spec_expect(
1156        loadout_spec: LoadoutSpec,
1157        rng: &mut impl Rng,
1158        time: Option<&(TimeOfDay, Calendar)>,
1159    ) -> Self {
1160        Self::from_loadout_spec(loadout_spec, rng, time).expect("failed to load loadout spec")
1161    }
1162
1163    #[must_use = "Method consumes builder and returns updated builder."]
1164    /// Set default active mainhand weapon based on `body`
1165    pub fn with_default_maintool(self, body: &Body) -> Self {
1166        self.active_mainhand(default_main_tool(body).map(Item::new_from_asset_expect))
1167    }
1168
1169    #[must_use = "Method consumes builder and returns updated builder."]
1170    /// Set default equipement based on `body`
1171    pub fn with_default_equipment(self, body: &Body) -> Self {
1172        let chest = default_chest(body);
1173
1174        if let Some(chest) = chest {
1175            self.chest(Some(Item::new_from_asset_expect(chest)))
1176        } else {
1177            self
1178        }
1179    }
1180
1181    #[must_use = "Method consumes builder and returns updated builder."]
1182    pub fn with_preset(mut self, preset: Preset) -> Self {
1183        let rng = &mut rand::thread_rng();
1184        match preset {
1185            Preset::HuskSummon => {
1186                self = self.with_asset_expect("common.loadout.dungeon.cultist.husk", rng, None);
1187            },
1188            Preset::BorealSummon => {
1189                self =
1190                    self.with_asset_expect("common.loadout.world.boreal.boreal_warrior", rng, None);
1191            },
1192            Preset::IronDwarfSummon => {
1193                self = self.with_asset_expect(
1194                    "common.loadout.dungeon.dwarven_quarry.iron_dwarf",
1195                    rng,
1196                    None,
1197                );
1198            },
1199            Preset::ShamanicSpiritSummon => {
1200                self = self.with_asset_expect(
1201                    "common.loadout.dungeon.terracotta.shamanic_spirit",
1202                    rng,
1203                    None,
1204                );
1205            },
1206            Preset::JiangshiSummon => {
1207                self =
1208                    self.with_asset_expect("common.loadout.dungeon.terracotta.jiangshi", rng, None);
1209            },
1210            Preset::BloodservantSummon => {
1211                self = self.with_asset_expect(
1212                    "common.loadout.dungeon.vampire.bloodservant",
1213                    rng,
1214                    None,
1215                );
1216            },
1217        }
1218
1219        self
1220    }
1221
1222    #[must_use = "Method consumes builder and returns updated builder."]
1223    pub fn with_creator(
1224        mut self,
1225        creator: fn(
1226            LoadoutBuilder,
1227            Option<&SiteInformation>,
1228            time: Option<&(TimeOfDay, Calendar)>,
1229        ) -> LoadoutBuilder,
1230        economy: Option<&SiteInformation>,
1231        time: Option<&(TimeOfDay, Calendar)>,
1232    ) -> LoadoutBuilder {
1233        self = creator(self, economy, time);
1234
1235        self
1236    }
1237
1238    #[must_use = "Method consumes builder and returns updated builder."]
1239    fn with_loadout_spec<R: Rng>(
1240        mut self,
1241        spec: LoadoutSpec,
1242        rng: &mut R,
1243        time: Option<&(TimeOfDay, Calendar)>,
1244    ) -> Result<Self, SpecError> {
1245        // Include any inheritance
1246        let spec = spec.eval(rng)?;
1247
1248        // Utility function to unwrap our itemspec
1249        let mut to_item = |maybe_item: Option<ItemSpec>| {
1250            if let Some(item) = maybe_item {
1251                item.try_to_item(rng, time)
1252            } else {
1253                Ok(None)
1254            }
1255        };
1256
1257        let to_pair = |maybe_hands: Option<Hands>, rng: &mut R| {
1258            if let Some(hands) = maybe_hands {
1259                hands.try_to_pair(rng, time)
1260            } else {
1261                Ok((None, None))
1262            }
1263        };
1264
1265        // Place every item
1266        if let Some(item) = to_item(spec.head)? {
1267            self = self.head(Some(item));
1268        }
1269        if let Some(item) = to_item(spec.neck)? {
1270            self = self.neck(Some(item));
1271        }
1272        if let Some(item) = to_item(spec.shoulders)? {
1273            self = self.shoulder(Some(item));
1274        }
1275        if let Some(item) = to_item(spec.chest)? {
1276            self = self.chest(Some(item));
1277        }
1278        if let Some(item) = to_item(spec.gloves)? {
1279            self = self.hands(Some(item));
1280        }
1281        if let Some(item) = to_item(spec.ring1)? {
1282            self = self.ring1(Some(item));
1283        }
1284        if let Some(item) = to_item(spec.ring2)? {
1285            self = self.ring2(Some(item));
1286        }
1287        if let Some(item) = to_item(spec.back)? {
1288            self = self.back(Some(item));
1289        }
1290        if let Some(item) = to_item(spec.belt)? {
1291            self = self.belt(Some(item));
1292        }
1293        if let Some(item) = to_item(spec.legs)? {
1294            self = self.pants(Some(item));
1295        }
1296        if let Some(item) = to_item(spec.feet)? {
1297            self = self.feet(Some(item));
1298        }
1299        if let Some(item) = to_item(spec.tabard)? {
1300            self = self.tabard(Some(item));
1301        }
1302        if let Some(item) = to_item(spec.bag1)? {
1303            self = self.bag(ArmorSlot::Bag1, Some(item));
1304        }
1305        if let Some(item) = to_item(spec.bag2)? {
1306            self = self.bag(ArmorSlot::Bag2, Some(item));
1307        }
1308        if let Some(item) = to_item(spec.bag3)? {
1309            self = self.bag(ArmorSlot::Bag3, Some(item));
1310        }
1311        if let Some(item) = to_item(spec.bag4)? {
1312            self = self.bag(ArmorSlot::Bag4, Some(item));
1313        }
1314        if let Some(item) = to_item(spec.lantern)? {
1315            self = self.lantern(Some(item));
1316        }
1317        if let Some(item) = to_item(spec.glider)? {
1318            self = self.glider(Some(item));
1319        }
1320        let (active_mainhand, active_offhand) = to_pair(spec.active_hands, rng)?;
1321        if let Some(item) = active_mainhand {
1322            self = self.active_mainhand(Some(item));
1323        }
1324        if let Some(item) = active_offhand {
1325            self = self.active_offhand(Some(item));
1326        }
1327        let (inactive_mainhand, inactive_offhand) = to_pair(spec.inactive_hands, rng)?;
1328        if let Some(item) = inactive_mainhand {
1329            self = self.inactive_mainhand(Some(item));
1330        }
1331        if let Some(item) = inactive_offhand {
1332            self = self.inactive_offhand(Some(item));
1333        }
1334
1335        Ok(self)
1336    }
1337
1338    #[must_use = "Method consumes builder and returns updated builder."]
1339    pub fn with_asset(
1340        self,
1341        asset_specifier: &str,
1342        rng: &mut impl Rng,
1343        time: Option<&(TimeOfDay, Calendar)>,
1344    ) -> Result<Self, SpecError> {
1345        let spec =
1346            LoadoutSpec::load_cloned(asset_specifier).map_err(SpecError::LoadoutAssetError)?;
1347        self.with_loadout_spec(spec, rng, time)
1348    }
1349
1350    /// # Usage
1351    /// Creates new `LoadoutBuilder` with all field replaced from
1352    /// `asset_specifier` which corresponds to loadout config
1353    ///
1354    /// # Panics
1355    /// 1) Will panic if there is no asset with such `asset_specifier`
1356    /// 2) Will panic if path to item specified in loadout file doesn't exist
1357    #[must_use = "Method consumes builder and returns updated builder."]
1358    pub fn with_asset_expect(
1359        self,
1360        asset_specifier: &str,
1361        rng: &mut impl Rng,
1362        time: Option<&(TimeOfDay, Calendar)>,
1363    ) -> Self {
1364        self.with_asset(asset_specifier, rng, time)
1365            .expect("failed loading loadout config")
1366    }
1367
1368    /// Set default armor items for the loadout. This may vary with game
1369    /// updates, but should be safe defaults for a new character.
1370    #[must_use = "Method consumes builder and returns updated builder."]
1371    pub fn defaults(self) -> Self {
1372        let rng = &mut rand::thread_rng();
1373        self.with_asset_expect("common.loadout.default", rng, None)
1374    }
1375
1376    #[must_use = "Method consumes builder and returns updated builder."]
1377    fn with_equipment(mut self, equip_slot: EquipSlot, item: Option<Item>) -> Self {
1378        // Panic if item doesn't correspond to slot
1379        assert!(
1380            item.as_ref()
1381                .is_none_or(|item| equip_slot.can_hold(&item.kind()))
1382        );
1383
1384        // TODO: What if `with_equipment` is used twice for the same slot. Or defaults
1385        // include an item in this slot.
1386        // Used when creating a loadout, so time not needed as it is used to check when
1387        // stuff gets unequipped. A new loadout has never unequipped an item.
1388        let time = Time(0.0);
1389
1390        self.0.swap(equip_slot, item, time);
1391        self
1392    }
1393
1394    #[must_use = "Method consumes builder and returns updated builder."]
1395    fn with_armor(self, armor_slot: ArmorSlot, item: Option<Item>) -> Self {
1396        self.with_equipment(EquipSlot::Armor(armor_slot), item)
1397    }
1398
1399    #[must_use = "Method consumes builder and returns updated builder."]
1400    pub fn active_mainhand(self, item: Option<Item>) -> Self {
1401        self.with_equipment(EquipSlot::ActiveMainhand, item)
1402    }
1403
1404    #[must_use = "Method consumes builder and returns updated builder."]
1405    pub fn active_offhand(self, item: Option<Item>) -> Self {
1406        self.with_equipment(EquipSlot::ActiveOffhand, item)
1407    }
1408
1409    #[must_use = "Method consumes builder and returns updated builder."]
1410    pub fn inactive_mainhand(self, item: Option<Item>) -> Self {
1411        self.with_equipment(EquipSlot::InactiveMainhand, item)
1412    }
1413
1414    #[must_use = "Method consumes builder and returns updated builder."]
1415    pub fn inactive_offhand(self, item: Option<Item>) -> Self {
1416        self.with_equipment(EquipSlot::InactiveOffhand, item)
1417    }
1418
1419    #[must_use = "Method consumes builder and returns updated builder."]
1420    pub fn shoulder(self, item: Option<Item>) -> Self {
1421        self.with_armor(ArmorSlot::Shoulders, item)
1422    }
1423
1424    #[must_use = "Method consumes builder and returns updated builder."]
1425    pub fn chest(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Chest, item) }
1426
1427    #[must_use = "Method consumes builder and returns updated builder."]
1428    pub fn belt(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Belt, item) }
1429
1430    #[must_use = "Method consumes builder and returns updated builder."]
1431    pub fn hands(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Hands, item) }
1432
1433    #[must_use = "Method consumes builder and returns updated builder."]
1434    pub fn pants(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Legs, item) }
1435
1436    #[must_use = "Method consumes builder and returns updated builder."]
1437    pub fn feet(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Feet, item) }
1438
1439    #[must_use = "Method consumes builder and returns updated builder."]
1440    pub fn back(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Back, item) }
1441
1442    #[must_use = "Method consumes builder and returns updated builder."]
1443    pub fn ring1(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Ring1, item) }
1444
1445    #[must_use = "Method consumes builder and returns updated builder."]
1446    pub fn ring2(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Ring2, item) }
1447
1448    #[must_use = "Method consumes builder and returns updated builder."]
1449    pub fn neck(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Neck, item) }
1450
1451    #[must_use = "Method consumes builder and returns updated builder."]
1452    pub fn lantern(self, item: Option<Item>) -> Self {
1453        self.with_equipment(EquipSlot::Lantern, item)
1454    }
1455
1456    #[must_use = "Method consumes builder and returns updated builder."]
1457    pub fn glider(self, item: Option<Item>) -> Self { self.with_equipment(EquipSlot::Glider, item) }
1458
1459    #[must_use = "Method consumes builder and returns updated builder."]
1460    pub fn head(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Head, item) }
1461
1462    #[must_use = "Method consumes builder and returns updated builder."]
1463    pub fn tabard(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Tabard, item) }
1464
1465    #[must_use = "Method consumes builder and returns updated builder."]
1466    pub fn bag(self, which: ArmorSlot, item: Option<Item>) -> Self { self.with_armor(which, item) }
1467
1468    #[must_use]
1469    pub fn build(self) -> Loadout { self.0 }
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475    use crate::comp::{self, Body};
1476    use rand::thread_rng;
1477    use strum::IntoEnumIterator;
1478
1479    // Testing different species
1480    //
1481    // Things that will be caught - invalid assets paths for
1482    // creating default main hand tool or equipment without config
1483    #[test]
1484    fn test_loadout_species() {
1485        macro_rules! test_species {
1486            // base case
1487            ($species:tt : $body:tt) => {
1488                let mut rng = thread_rng();
1489                for s in comp::$species::ALL_SPECIES.iter() {
1490                    let body = comp::$species::Body::random_with(&mut rng, s);
1491                    let female_body = comp::$species::Body {
1492                        body_type: comp::$species::BodyType::Female,
1493                        ..body
1494                    };
1495                    let male_body = comp::$species::Body {
1496                        body_type: comp::$species::BodyType::Male,
1497                        ..body
1498                    };
1499                    std::mem::drop(LoadoutBuilder::from_default(
1500                        &Body::$body(female_body),
1501                    ));
1502                    std::mem::drop(LoadoutBuilder::from_default(
1503                        &Body::$body(male_body),
1504                    ));
1505                }
1506            };
1507            // recursive call
1508            ($base:tt : $body:tt, $($species:tt : $nextbody:tt),+ $(,)?) => {
1509                test_species!($base: $body);
1510                test_species!($($species: $nextbody),+);
1511            }
1512        }
1513
1514        // See `[AllBodies](crate::comp::body::AllBodies)`
1515        test_species!(
1516            humanoid: Humanoid,
1517            quadruped_small: QuadrupedSmall,
1518            quadruped_medium: QuadrupedMedium,
1519            quadruped_low: QuadrupedLow,
1520            quadruped_small: QuadrupedSmall,
1521            bird_medium: BirdMedium,
1522            bird_large: BirdLarge,
1523            fish_small: FishSmall,
1524            fish_medium: FishMedium,
1525            biped_small: BipedSmall,
1526            biped_large: BipedLarge,
1527            theropod: Theropod,
1528            dragon: Dragon,
1529            golem: Golem,
1530            arthropod: Arthropod,
1531        );
1532    }
1533
1534    // Testing all loadout presets
1535    //
1536    // Things that will be catched - invalid assets paths
1537    #[test]
1538    fn test_loadout_presets() {
1539        for preset in Preset::iter() {
1540            drop(LoadoutBuilder::empty().with_preset(preset));
1541        }
1542    }
1543
1544    // It just loads every loadout asset and tries to validate them
1545    //
1546    // TODO: optimize by caching checks
1547    // Because of nature of inheritance of loadout specs,
1548    // we will check some loadout assets at least two times.
1549    // One for asset itself and second if it serves as a base for other asset.
1550    #[test]
1551    fn validate_all_loadout_assets() {
1552        let loadouts = assets::load_rec_dir::<LoadoutSpec>("common.loadout")
1553            .expect("failed to load loadout directory");
1554        for loadout_id in loadouts.read().ids() {
1555            let loadout =
1556                LoadoutSpec::load_cloned(loadout_id).expect("failed to load loadout asset");
1557            loadout
1558                .validate(vec![loadout_id.to_string()])
1559                .unwrap_or_else(|e| panic!("{loadout_id} is broken: {e:?}"));
1560        }
1561    }
1562
1563    // Basically test that our validation tests don't have false-positives
1564    #[test]
1565    fn test_valid_assets() {
1566        let loadouts = assets::load_rec_dir::<LoadoutSpec>("test.loadout.ok")
1567            .expect("failed to load loadout directory");
1568
1569        for loadout_id in loadouts.read().ids() {
1570            let loadout =
1571                LoadoutSpec::load_cloned(loadout_id).expect("failed to load loadout asset");
1572            loadout
1573                .validate(vec![loadout_id.to_string()])
1574                .unwrap_or_else(|e| panic!("{loadout_id} is broken: {e:?}"));
1575        }
1576    }
1577}