veloren_common/comp/inventory/
loadout_builder.rs

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