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 match_some,
14 resources::{Time, TimeOfDay},
15 trade::SiteInformation,
16};
17use rand::{self, Rng, distributions::WeightedError, seq::SliceRandom};
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(WeightedError),
29 BaseChoiceError(WeightedError),
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 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 #[cfg(test)]
105 fn validate(&self) -> Result<(), ValidationError> {
106 let mut rng = rand::thread_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 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 InHands((Option<ItemSpec>, Option<ItemSpec>)),
138 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 #[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 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 Combine(Vec<Base>),
196 Choice(Vec<(Weight, Base)>),
197}
198
199impl Base {
200 fn to_spec(&self, rng: &mut impl Rng) -> Result<LoadoutSpec, SpecError> {
205 match self {
206 Base::Asset(asset_specifier) => {
207 LoadoutSpec::load_cloned(asset_specifier).map_err(SpecError::LoadoutAssetError)
208 },
209 Base::Combine(bases) => {
210 let bases = bases.iter().map(|b| b.to_spec(rng)?.eval(rng));
211 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#[derive(Debug, Deserialize, Clone, Default)]
238#[serde(deny_unknown_fields)]
239pub struct LoadoutSpec {
240 pub inherit: Option<Base>,
242 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 pub active_hands: Option<Hands>,
263 pub inactive_hands: Option<Hands>,
264}
265
266impl LoadoutSpec {
267 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 fn eval(self, rng: &mut impl Rng) -> Result<Self, SpecError> {
347 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 #[cfg(test)]
370 pub fn validate(&self, history: Vec<String>) -> Result<(), ValidationError> {
371 fn validate_base(base: &Base, mut history: Vec<String>) -> Result<(), ValidationError> {
377 match base {
378 Base::Asset(asset) => {
379 let based = LoadoutSpec::load_cloned(asset)
381 .map_err(ValidationError::LoadoutAssetError)?;
382
383 history.push(asset.to_owned());
385
386 based.validate(history)
388 },
389 Base::Combine(bases) => {
390 for base in bases {
391 validate_base(base, history.clone())?;
392 }
393 Ok(())
394 },
395 Base::Choice(choices) => {
396 for (_weight, base) in choices {
398 validate_base(base, history.clone())?;
399 }
400 Ok(())
401 },
402 }
403 }
404
405 if let Some((last, tail)) = history.split_last() {
414 for asset in tail {
415 if last == asset {
416 return Err(ValidationError::Loop(history));
417 }
418 }
419 }
420
421 if let Some(base) = &self.inherit {
422 validate_base(base, history)?
423 }
424
425 self.validate_entries()
426 }
427
428 #[cfg(test)]
436 fn validate_entries(&self) -> Result<(), ValidationError> {
437 if let Some(item) = &self.head {
439 item.validate()?;
440 }
441 if let Some(item) = &self.neck {
442 item.validate()?;
443 }
444 if let Some(item) = &self.shoulders {
445 item.validate()?;
446 }
447 if let Some(item) = &self.chest {
448 item.validate()?;
449 }
450 if let Some(item) = &self.gloves {
451 item.validate()?;
452 }
453 if let Some(item) = &self.ring1 {
454 item.validate()?;
455 }
456 if let Some(item) = &self.ring2 {
457 item.validate()?;
458 }
459 if let Some(item) = &self.back {
460 item.validate()?;
461 }
462 if let Some(item) = &self.belt {
463 item.validate()?;
464 }
465 if let Some(item) = &self.legs {
466 item.validate()?;
467 }
468 if let Some(item) = &self.feet {
469 item.validate()?;
470 }
471 if let Some(item) = &self.tabard {
472 item.validate()?;
473 }
474 if let Some(item) = &self.bag1 {
476 item.validate()?;
477 }
478 if let Some(item) = &self.bag2 {
479 item.validate()?;
480 }
481 if let Some(item) = &self.bag3 {
482 item.validate()?;
483 }
484 if let Some(item) = &self.bag4 {
485 item.validate()?;
486 }
487 if let Some(item) = &self.lantern {
488 item.validate()?;
489 }
490 if let Some(item) = &self.glider {
491 item.validate()?;
492 }
493 if let Some(hands) = &self.active_hands {
495 hands.validate()?;
496 }
497 if let Some(hands) = &self.inactive_hands {
498 hands.validate()?;
499 }
500
501 Ok(())
502 }
503}
504
505impl assets::Asset for LoadoutSpec {
506 type Loader = assets::RonLoader;
507
508 const EXTENSION: &'static str = "ron";
509}
510
511#[must_use]
512pub fn make_potion_bag(quantity: u32) -> Item {
513 let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.tiny_leather_pouch");
514 if let Some(i) = bag.slots_mut().iter_mut().next() {
515 let mut potions = Item::new_from_asset_expect("common.items.consumable.potion_big");
516 if let Err(e) = potions.set_amount(quantity) {
517 warn!("Failed to set potion quantity: {:?}", e);
518 }
519 *i = Some(potions);
520 }
521 bag
522}
523
524#[must_use]
525pub fn make_food_bag(quantity: u32) -> Item {
526 let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.tiny_leather_pouch");
527 if let Some(i) = bag.slots_mut().iter_mut().next() {
528 let mut food = Item::new_from_asset_expect("common.items.food.apple_stick");
529 if let Err(e) = food.set_amount(quantity) {
530 warn!("Failed to set food quantity: {:?}", e);
531 }
532 *i = Some(food);
533 }
534 bag
535}
536
537#[must_use]
538pub fn default_chest(body: &Body) -> Option<&'static str> {
539 match body {
540 Body::BipedLarge(body) => match_some!(body.species,
541 biped_large::Species::Mindflayer => "common.items.npc_armor.biped_large.mindflayer",
542 biped_large::Species::Minotaur => "common.items.npc_armor.biped_large.minotaur",
543 biped_large::Species::Tidalwarrior => "common.items.npc_armor.biped_large.tidal_warrior",
544 biped_large::Species::Yeti => "common.items.npc_armor.biped_large.yeti",
545 biped_large::Species::Harvester => "common.items.npc_armor.biped_large.harvester",
546 biped_large::Species::Ogre
547 | biped_large::Species::Blueoni
548 | biped_large::Species::Redoni
549 | biped_large::Species::Cavetroll
550 | biped_large::Species::Mountaintroll
551 | biped_large::Species::Swamptroll
552 | biped_large::Species::Wendigo => "common.items.npc_armor.biped_large.generic",
553 biped_large::Species::Cyclops => "common.items.npc_armor.biped_large.cyclops",
554 biped_large::Species::Dullahan => "common.items.npc_armor.biped_large.dullahan",
555 biped_large::Species::Tursus => "common.items.npc_armor.biped_large.tursus",
556 biped_large::Species::Cultistwarlord => "common.items.npc_armor.biped_large.warlord",
557 biped_large::Species::Cultistwarlock => "common.items.npc_armor.biped_large.warlock",
558 biped_large::Species::Gigasfrost => "common.items.npc_armor.biped_large.gigas_frost",
559 biped_large::Species::Gigasfire => "common.items.npc_armor.biped_large.gigas_fire",
560 biped_large::Species::HaniwaGeneral => "common.items.npc_armor.biped_large.haniwageneral",
561 biped_large::Species::TerracottaBesieger
562 | biped_large::Species::TerracottaDemolisher
563 | biped_large::Species::TerracottaPunisher
564 | biped_large::Species::TerracottaPursuer
565 | biped_large::Species::Cursekeeper => "common.items.npc_armor.biped_large.terracotta",
566 biped_large::Species::Forgemaster => "common.items.npc_armor.biped_large.forgemaster",
567 ),
568 Body::BirdLarge(body) => match_some!(body.species,
569 bird_large::Species::FlameWyvern
570 | bird_large::Species::FrostWyvern
571 | bird_large::Species::CloudWyvern
572 | bird_large::Species::SeaWyvern
573 | bird_large::Species::WealdWyvern => "common.items.npc_armor.bird_large.wyvern",
574 bird_large::Species::Phoenix => "common.items.npc_armor.bird_large.phoenix",
575 ),
576 Body::BirdMedium(body) => match_some!(body.species,
577 bird_medium::Species::BloodmoonBat => "common.items.npc_armor.bird_medium.bloodmoon_bat",
578 ),
579 Body::Golem(body) => match_some!(body.species,
580 golem::Species::ClayGolem => "common.items.npc_armor.golem.claygolem",
581 golem::Species::Gravewarden => "common.items.npc_armor.golem.gravewarden",
582 golem::Species::WoodGolem => "common.items.npc_armor.golem.woodgolem",
583 golem::Species::AncientEffigy => "common.items.npc_armor.golem.ancienteffigy",
584 golem::Species::Mogwai => "common.items.npc_armor.golem.mogwai",
585 golem::Species::IronGolem => "common.items.npc_armor.golem.irongolem",
586 ),
587 Body::QuadrupedLow(body) => match_some!(body.species,
588 quadruped_low::Species::Sandshark
589 | quadruped_low::Species::Alligator
590 | quadruped_low::Species::Crocodile
591 | quadruped_low::Species::SeaCrocodile
592 | quadruped_low::Species::Icedrake
593 | quadruped_low::Species::Lavadrake
594 | quadruped_low::Species::Mossdrake => "common.items.npc_armor.generic",
595 quadruped_low::Species::Reefsnapper
596 | quadruped_low::Species::Rocksnapper
597 | quadruped_low::Species::Rootsnapper
598 | quadruped_low::Species::Tortoise
599 | quadruped_low::Species::Basilisk
600 | quadruped_low::Species::Hydra => "common.items.npc_armor.generic_high",
601 quadruped_low::Species::Dagon => "common.items.npc_armor.quadruped_low.dagon",
602 ),
603 Body::QuadrupedMedium(body) => match_some!(body.species,
604 quadruped_medium::Species::Bonerattler => "common.items.npc_armor.generic",
605 quadruped_medium::Species::Tarasque => "common.items.npc_armor.generic_high",
606 quadruped_medium::Species::ClaySteed => "common.items.npc_armor.quadruped_medium.claysteed",
607 ),
608 Body::Theropod(body) => match_some!(body.species,
609 theropod::Species::Archaeos | theropod::Species::Ntouka => "common.items.npc_armor.generic",
610 theropod::Species::Dodarock => "common.items.npc_armor.generic_high",
611 ),
612 Body::Arthropod(body) => match body.species {
614 arthropod::Species::Blackwidow
615 | arthropod::Species::Cavespider
616 | arthropod::Species::Emberfly
617 | arthropod::Species::Moltencrawler
618 | arthropod::Species::Mosscrawler
619 | arthropod::Species::Sandcrawler
620 | arthropod::Species::Tarantula => None,
621 _ => Some("common.items.npc_armor.generic"),
622 },
623 Body::QuadrupedSmall(body) => match_some!(body.species,
624 quadruped_small::Species::Turtle
625 | quadruped_small::Species::Holladon
626 | quadruped_small::Species::TreantSapling
627 | quadruped_small::Species::MossySnail => "common.items.npc_armor.generic",
628 ),
629 Body::Crustacean(body) => match_some!(body.species,
630 crustacean::Species::Karkatha => "common.items.npc_armor.crustacean.karkatha",
631 ),
632 _ => None,
633 }
634}
635
636#[must_use]
637#[expect(clippy::too_many_lines)]
641pub fn default_main_tool(body: &Body) -> Option<&'static str> {
642 match body {
643 Body::Golem(golem) => match_some!(golem.species,
644 golem::Species::StoneGolem => "common.items.npc_weapons.unique.stone_golems_fist",
645 golem::Species::ClayGolem => "common.items.npc_weapons.unique.clay_golem_fist",
646 golem::Species::Gravewarden => "common.items.npc_weapons.unique.gravewarden_fist",
647 golem::Species::WoodGolem => "common.items.npc_weapons.unique.wood_golem_fist",
648 golem::Species::CoralGolem => "common.items.npc_weapons.unique.coral_golem_fist",
649 golem::Species::AncientEffigy => "common.items.npc_weapons.unique.ancient_effigy_eyes",
650 golem::Species::Mogwai => "common.items.npc_weapons.unique.mogwai",
651 golem::Species::IronGolem => "common.items.npc_weapons.unique.iron_golem_fist",
652 ),
653 Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
654 quadruped_medium::Species::Wolf => {
655 Some("common.items.npc_weapons.unique.quadruped_medium.wolf")
656 },
657 quadruped_medium::Species::Alpaca | quadruped_medium::Species::Llama => {
659 Some("common.items.npc_weapons.unique.quadruped_medium.alpaca")
660 },
661 quadruped_medium::Species::Antelope | quadruped_medium::Species::Deer => {
662 Some("common.items.npc_weapons.unique.quadruped_medium.antelope")
663 },
664 quadruped_medium::Species::Donkey | quadruped_medium::Species::Zebra => {
665 Some("common.items.npc_weapons.unique.quadruped_medium.donkey")
666 },
667 quadruped_medium::Species::Horse | quadruped_medium::Species::Kelpie => {
669 Some("common.items.npc_weapons.unique.quadruped_medium.horse")
670 },
671 quadruped_medium::Species::ClaySteed => {
672 Some("common.items.npc_weapons.unique.claysteed")
673 },
674 quadruped_medium::Species::Saber
675 | quadruped_medium::Species::Bonerattler
676 | quadruped_medium::Species::Lion
677 | quadruped_medium::Species::Snowleopard => {
678 Some("common.items.npc_weapons.unique.quadmedjump")
679 },
680 quadruped_medium::Species::Darkhound => {
681 Some("common.items.npc_weapons.unique.darkhound")
682 },
683 quadruped_medium::Species::Moose | quadruped_medium::Species::Tuskram => {
685 Some("common.items.npc_weapons.unique.quadruped_medium.moose")
686 },
687 quadruped_medium::Species::Mouflon => {
688 Some("common.items.npc_weapons.unique.quadruped_medium.mouflon")
689 },
690 quadruped_medium::Species::Akhlut
691 | quadruped_medium::Species::Dreadhorn
692 | quadruped_medium::Species::Mammoth
693 | quadruped_medium::Species::Ngoubou => {
694 Some("common.items.npc_weapons.unique.quadmedcharge")
695 },
696 quadruped_medium::Species::Grolgar => {
697 Some("common.items.npc_weapons.unique.quadruped_medium.grolgar")
698 },
699 quadruped_medium::Species::Roshwalr => Some("common.items.npc_weapons.unique.roshwalr"),
700 quadruped_medium::Species::Cattle => {
701 Some("common.items.npc_weapons.unique.quadmedbasicgentle")
702 },
703 quadruped_medium::Species::Highland | quadruped_medium::Species::Yak => {
704 Some("common.items.npc_weapons.unique.quadruped_medium.highland")
705 },
706 quadruped_medium::Species::Frostfang => {
707 Some("common.items.npc_weapons.unique.frostfang")
708 },
709 _ => Some("common.items.npc_weapons.unique.quadmedbasic"),
710 },
711 Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
712 quadruped_low::Species::Maneater => {
713 Some("common.items.npc_weapons.unique.quadruped_low.maneater")
714 },
715 quadruped_low::Species::Asp => {
716 Some("common.items.npc_weapons.unique.quadruped_low.asp")
717 },
718 quadruped_low::Species::Dagon => Some("common.items.npc_weapons.unique.dagon"),
719 quadruped_low::Species::Snaretongue => {
720 Some("common.items.npc_weapons.unique.snaretongue")
721 },
722 quadruped_low::Species::Crocodile
723 | quadruped_low::Species::SeaCrocodile
724 | quadruped_low::Species::Alligator
725 | quadruped_low::Species::Salamander
726 | quadruped_low::Species::Elbst => Some("common.items.npc_weapons.unique.quadlowtail"),
727 quadruped_low::Species::Monitor | quadruped_low::Species::Pangolin => {
728 Some("common.items.npc_weapons.unique.quadlowquick")
729 },
730 quadruped_low::Species::Lavadrake => {
731 Some("common.items.npc_weapons.unique.quadruped_low.lavadrake")
732 },
733 quadruped_low::Species::Deadwood => {
734 Some("common.items.npc_weapons.unique.quadruped_low.deadwood")
735 },
736 quadruped_low::Species::Basilisk => {
737 Some("common.items.npc_weapons.unique.quadruped_low.basilisk")
738 },
739 quadruped_low::Species::Icedrake => {
740 Some("common.items.npc_weapons.unique.quadruped_low.icedrake")
741 },
742 quadruped_low::Species::Hakulaq => {
743 Some("common.items.npc_weapons.unique.quadruped_low.hakulaq")
744 },
745 quadruped_low::Species::Tortoise => {
746 Some("common.items.npc_weapons.unique.quadruped_low.tortoise")
747 },
748 quadruped_low::Species::Driggle => Some("common.items.npc_weapons.unique.driggle"),
749 quadruped_low::Species::Rocksnapper => {
750 Some("common.items.npc_weapons.unique.rocksnapper")
751 },
752 quadruped_low::Species::Hydra => {
753 Some("common.items.npc_weapons.unique.quadruped_low.hydra")
754 },
755 _ => Some("common.items.npc_weapons.unique.quadlowbasic"),
756 },
757 Body::QuadrupedSmall(quadruped_small) => match quadruped_small.species {
758 quadruped_small::Species::TreantSapling => {
759 Some("common.items.npc_weapons.unique.treantsapling")
760 },
761 quadruped_small::Species::MossySnail => {
762 Some("common.items.npc_weapons.unique.mossysnail")
763 },
764 quadruped_small::Species::Boar | quadruped_small::Species::Truffler => {
765 Some("common.items.npc_weapons.unique.quadruped_small.boar")
766 },
767 quadruped_small::Species::Hyena => {
768 Some("common.items.npc_weapons.unique.quadruped_small.hyena")
769 },
770 quadruped_small::Species::Beaver
771 | quadruped_small::Species::Dog
772 | quadruped_small::Species::Cat
773 | quadruped_small::Species::Goat
774 | quadruped_small::Species::Holladon
775 | quadruped_small::Species::Sheep
776 | quadruped_small::Species::Seal => {
777 Some("common.items.npc_weapons.unique.quadsmallbasic")
778 },
779 _ => Some("common.items.npc_weapons.unique.quadruped_small.rodent"),
780 },
781 Body::Theropod(theropod) => match theropod.species {
782 theropod::Species::Sandraptor
783 | theropod::Species::Snowraptor
784 | theropod::Species::Woodraptor
785 | theropod::Species::Axebeak
786 | theropod::Species::Sunlizard => Some("common.items.npc_weapons.unique.theropodbird"),
787 theropod::Species::Yale => Some("common.items.npc_weapons.unique.theropod.yale"),
788 theropod::Species::Dodarock => Some("common.items.npc_weapons.unique.theropodsmall"),
789 _ => Some("common.items.npc_weapons.unique.theropodbasic"),
790 },
791 Body::Arthropod(arthropod) => match arthropod.species {
792 arthropod::Species::Hornbeetle | arthropod::Species::Stagbeetle => {
793 Some("common.items.npc_weapons.unique.arthropods.hornbeetle")
794 },
795 arthropod::Species::Emberfly => Some("common.items.npc_weapons.unique.emberfly"),
796 arthropod::Species::Cavespider => {
797 Some("common.items.npc_weapons.unique.arthropods.cavespider")
798 },
799 arthropod::Species::Sandcrawler | arthropod::Species::Mosscrawler => {
800 Some("common.items.npc_weapons.unique.arthropods.mosscrawler")
801 },
802 arthropod::Species::Moltencrawler => {
803 Some("common.items.npc_weapons.unique.arthropods.moltencrawler")
804 },
805 arthropod::Species::Weevil => Some("common.items.npc_weapons.unique.arthropods.weevil"),
806 arthropod::Species::Blackwidow => {
807 Some("common.items.npc_weapons.unique.arthropods.blackwidow")
808 },
809 arthropod::Species::Tarantula => {
810 Some("common.items.npc_weapons.unique.arthropods.tarantula")
811 },
812 arthropod::Species::Antlion => {
813 Some("common.items.npc_weapons.unique.arthropods.antlion")
814 },
815 arthropod::Species::Dagonite => {
816 Some("common.items.npc_weapons.unique.arthropods.dagonite")
817 },
818 arthropod::Species::Leafbeetle => {
819 Some("common.items.npc_weapons.unique.arthropods.leafbeetle")
820 },
821 },
822 Body::BipedLarge(biped_large) => match (biped_large.species, biped_large.body_type) {
823 (biped_large::Species::Occultsaurok, _) => {
824 Some("common.items.npc_weapons.staff.saurok_staff")
825 },
826 (biped_large::Species::Mightysaurok, _) => {
827 Some("common.items.npc_weapons.sword.saurok_sword")
828 },
829 (biped_large::Species::Slysaurok, _) => Some("common.items.npc_weapons.bow.saurok_bow"),
830 (biped_large::Species::Ogre, biped_large::BodyType::Male) => {
831 Some("common.items.npc_weapons.hammer.ogre_hammer")
832 },
833 (biped_large::Species::Ogre, biped_large::BodyType::Female) => {
834 Some("common.items.npc_weapons.staff.ogre_staff")
835 },
836 (
837 biped_large::Species::Mountaintroll
838 | biped_large::Species::Swamptroll
839 | biped_large::Species::Cavetroll,
840 _,
841 ) => Some("common.items.npc_weapons.hammer.troll_hammer"),
842 (biped_large::Species::Wendigo, _) => {
843 Some("common.items.npc_weapons.unique.wendigo_magic")
844 },
845 (biped_large::Species::Werewolf, _) => {
846 Some("common.items.npc_weapons.unique.beast_claws")
847 },
848 (biped_large::Species::Tursus, _) => {
849 Some("common.items.npc_weapons.unique.tursus_claws")
850 },
851 (biped_large::Species::Cyclops, _) => {
852 Some("common.items.npc_weapons.hammer.cyclops_hammer")
853 },
854 (biped_large::Species::Dullahan, _) => {
855 Some("common.items.npc_weapons.sword.dullahan_sword")
856 },
857 (biped_large::Species::Mindflayer, _) => {
858 Some("common.items.npc_weapons.staff.mindflayer_staff")
859 },
860 (biped_large::Species::Minotaur, _) => {
861 Some("common.items.npc_weapons.axe.minotaur_axe")
862 },
863 (biped_large::Species::Tidalwarrior, _) => {
864 Some("common.items.npc_weapons.unique.tidal_spear")
865 },
866 (biped_large::Species::Yeti, _) => Some("common.items.npc_weapons.hammer.yeti_hammer"),
867 (biped_large::Species::Harvester, _) => {
868 Some("common.items.npc_weapons.hammer.harvester_scythe")
869 },
870 (biped_large::Species::Blueoni, _) => Some("common.items.npc_weapons.axe.oni_blue_axe"),
871 (biped_large::Species::Redoni, _) => {
872 Some("common.items.npc_weapons.hammer.oni_red_hammer")
873 },
874 (biped_large::Species::Cultistwarlord, _) => {
875 Some("common.items.npc_weapons.sword.bipedlarge-cultist")
876 },
877 (biped_large::Species::Cultistwarlock, _) => {
878 Some("common.items.npc_weapons.staff.bipedlarge-cultist")
879 },
880 (biped_large::Species::Huskbrute, _) => {
881 Some("common.items.npc_weapons.unique.husk_brute")
882 },
883 (biped_large::Species::Strigoi, _) => {
884 Some("common.items.npc_weapons.unique.strigoi_claws")
885 },
886 (biped_large::Species::Executioner, _) => {
887 Some("common.items.npc_weapons.axe.executioner_axe")
888 },
889 (biped_large::Species::Gigasfrost, _) => {
890 Some("common.items.npc_weapons.axe.gigas_frost_axe")
891 },
892 (biped_large::Species::Gigasfire, _) => {
893 Some("common.items.npc_weapons.sword.gigas_fire_sword")
894 },
895 (biped_large::Species::AdletElder, _) => {
896 Some("common.items.npc_weapons.sword.adlet_elder_sword")
897 },
898 (biped_large::Species::SeaBishop, _) => {
899 Some("common.items.npc_weapons.unique.sea_bishop_sceptre")
900 },
901 (biped_large::Species::HaniwaGeneral, _) => {
902 Some("common.items.npc_weapons.sword.haniwa_general_sword")
903 },
904 (biped_large::Species::TerracottaBesieger, _) => {
905 Some("common.items.npc_weapons.bow.terracotta_besieger_bow")
906 },
907 (biped_large::Species::TerracottaDemolisher, _) => {
908 Some("common.items.npc_weapons.unique.terracotta_demolisher_fist")
909 },
910 (biped_large::Species::TerracottaPunisher, _) => {
911 Some("common.items.npc_weapons.hammer.terracotta_punisher_club")
912 },
913 (biped_large::Species::TerracottaPursuer, _) => {
914 Some("common.items.npc_weapons.sword.terracotta_pursuer_sword")
915 },
916 (biped_large::Species::Cursekeeper, _) => {
917 Some("common.items.npc_weapons.unique.cursekeeper_sceptre")
918 },
919 (biped_large::Species::Forgemaster, _) => {
920 Some("common.items.npc_weapons.hammer.forgemaster_hammer")
921 },
922 },
923 Body::Object(body) => match_some!(body,
924 object::Body::Crossbow => "common.items.npc_weapons.unique.turret",
925 object::Body::Flamethrower | object::Body::Lavathrower => {
926 "common.items.npc_weapons.unique.flamethrower"
927 },
928 object::Body::BarrelOrgan => "common.items.npc_weapons.unique.organ",
929 object::Body::TerracottaStatue => "common.items.npc_weapons.unique.terracotta_statue",
930 object::Body::HaniwaSentry => "common.items.npc_weapons.unique.haniwa_sentry",
931 object::Body::SeaLantern => "common.items.npc_weapons.unique.tidal_totem",
932 object::Body::Tornado => "common.items.npc_weapons.unique.tornado",
933 object::Body::FieryTornado => "common.items.npc_weapons.unique.fiery_tornado",
934 object::Body::GnarlingTotemRed => "common.items.npc_weapons.biped_small.gnarling.redtotem",
935 object::Body::GnarlingTotemGreen => "common.items.npc_weapons.biped_small.gnarling.greentotem",
936 object::Body::GnarlingTotemWhite => "common.items.npc_weapons.biped_small.gnarling.whitetotem",
937 ),
938 Body::BipedSmall(biped_small) => match (biped_small.species, biped_small.body_type) {
939 (biped_small::Species::Gnome, _) => {
940 Some("common.items.npc_weapons.biped_small.adlet.tracker")
941 },
942 (biped_small::Species::Bushly, _) => Some("common.items.npc_weapons.unique.bushly"),
943 (biped_small::Species::Cactid, _) => Some("common.items.npc_weapons.unique.cactid"),
944 (biped_small::Species::Irrwurz, _) => Some("common.items.npc_weapons.unique.irrwurz"),
945 (biped_small::Species::Husk, _) => Some("common.items.npc_weapons.unique.husk"),
946 (biped_small::Species::Flamekeeper, _) => {
947 Some("common.items.npc_weapons.unique.flamekeeper_staff")
948 },
949 (biped_small::Species::IronDwarf, _) => {
950 Some("common.items.npc_weapons.unique.iron_dwarf")
951 },
952 (biped_small::Species::ShamanicSpirit, _) => {
953 Some("common.items.npc_weapons.unique.shamanic_spirit")
954 },
955 (biped_small::Species::Jiangshi, _) => Some("common.items.npc_weapons.unique.jiangshi"),
956 (biped_small::Species::BloodmoonHeiress, _) => {
957 Some("common.items.npc_weapons.biped_small.vampire.bloodmoon_heiress_sword")
958 },
959 (biped_small::Species::Bloodservant, _) => {
960 Some("common.items.npc_weapons.biped_small.vampire.bloodservant_axe")
961 },
962 (biped_small::Species::Harlequin, _) => {
963 Some("common.items.npc_weapons.biped_small.vampire.harlequin_dagger")
964 },
965 (biped_small::Species::GoblinThug, _) => {
966 Some("common.items.npc_weapons.unique.goblin_thug_club")
967 },
968 (biped_small::Species::GoblinChucker, _) => {
969 Some("common.items.npc_weapons.unique.goblin_chucker")
970 },
971 (biped_small::Species::GoblinRuffian, _) => {
972 Some("common.items.npc_weapons.unique.goblin_ruffian_knife")
973 },
974 (biped_small::Species::GreenLegoom, _) => {
975 Some("common.items.npc_weapons.unique.green_legoom_rake")
976 },
977 (biped_small::Species::OchreLegoom, _) => {
978 Some("common.items.npc_weapons.unique.ochre_legoom_spade")
979 },
980 (biped_small::Species::PurpleLegoom, _) => {
981 Some("common.items.npc_weapons.unique.purple_legoom_pitchfork")
982 },
983 (biped_small::Species::RedLegoom, _) => {
984 Some("common.items.npc_weapons.unique.red_legoom_hoe")
985 },
986 _ => Some("common.items.npc_weapons.biped_small.adlet.hunter"),
987 },
988 Body::BirdLarge(bird_large) => match (bird_large.species, bird_large.body_type) {
989 (bird_large::Species::Cockatrice, _) => {
990 Some("common.items.npc_weapons.unique.birdlargebreathe")
991 },
992 (bird_large::Species::Phoenix, _) => {
993 Some("common.items.npc_weapons.unique.birdlargefire")
994 },
995 (bird_large::Species::Roc, _) => Some("common.items.npc_weapons.unique.birdlargebasic"),
996 (bird_large::Species::FlameWyvern, _) => {
997 Some("common.items.npc_weapons.unique.flamewyvern")
998 },
999 (bird_large::Species::FrostWyvern, _) => {
1000 Some("common.items.npc_weapons.unique.frostwyvern")
1001 },
1002 (bird_large::Species::CloudWyvern, _) => {
1003 Some("common.items.npc_weapons.unique.cloudwyvern")
1004 },
1005 (bird_large::Species::SeaWyvern, _) => {
1006 Some("common.items.npc_weapons.unique.seawyvern")
1007 },
1008 (bird_large::Species::WealdWyvern, _) => {
1009 Some("common.items.npc_weapons.unique.wealdwyvern")
1010 },
1011 },
1012 Body::BirdMedium(bird_medium) => match bird_medium.species {
1013 bird_medium::Species::Cockatiel
1014 | bird_medium::Species::Bat
1015 | bird_medium::Species::Parrot
1016 | bird_medium::Species::Crow
1017 | bird_medium::Species::Parakeet => {
1018 Some("common.items.npc_weapons.unique.simpleflyingbasic")
1019 },
1020 bird_medium::Species::VampireBat => Some("common.items.npc_weapons.unique.vampire_bat"),
1021 bird_medium::Species::BloodmoonBat => {
1022 Some("common.items.npc_weapons.unique.bloodmoon_bat")
1023 },
1024 _ => Some("common.items.npc_weapons.unique.birdmediumbasic"),
1025 },
1026 Body::Crustacean(crustacean) => match crustacean.species {
1027 crustacean::Species::Crab | crustacean::Species::SoldierCrab => {
1028 Some("common.items.npc_weapons.unique.crab_pincer")
1029 },
1030 crustacean::Species::Karkatha => {
1031 Some("common.items.npc_weapons.unique.karkatha_pincer")
1032 },
1033 },
1034 _ => None,
1035 }
1036}
1037
1038#[derive(Clone)]
1054pub struct LoadoutBuilder(Loadout);
1055
1056#[derive(Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Debug, EnumIter)]
1057pub enum Preset {
1058 HuskSummon,
1059 BorealSummon,
1060 AshenSummon,
1061 IronDwarfSummon,
1062 ShamanicSpiritSummon,
1063 JiangshiSummon,
1064 BloodservantSummon,
1065}
1066
1067impl LoadoutBuilder {
1068 #[must_use]
1069 pub fn empty() -> Self { Self(Loadout::new_empty()) }
1070
1071 #[must_use]
1072 pub fn from_asset_expect(
1075 asset_specifier: &str,
1076 rng: &mut impl Rng,
1077 time: Option<&(TimeOfDay, Calendar)>,
1078 ) -> Self {
1079 Self::from_asset(asset_specifier, rng, time).expect("failed to load loadut config")
1080 }
1081
1082 pub fn from_asset(
1084 asset_specifier: &str,
1085 rng: &mut impl Rng,
1086 time: Option<&(TimeOfDay, Calendar)>,
1087 ) -> Result<Self, SpecError> {
1088 let loadout = Self::empty();
1089 loadout.with_asset(asset_specifier, rng, time)
1090 }
1091
1092 #[must_use]
1093 pub fn from_default(body: &Body) -> Self {
1098 let loadout = Self::empty();
1099 loadout
1100 .with_default_maintool(body)
1101 .with_default_equipment(body)
1102 }
1103
1104 pub fn from_loadout_spec(
1106 loadout_spec: LoadoutSpec,
1107 rng: &mut impl Rng,
1108 time: Option<&(TimeOfDay, Calendar)>,
1109 ) -> Result<Self, SpecError> {
1110 let loadout = Self::empty();
1111 loadout.with_loadout_spec(loadout_spec, rng, time)
1112 }
1113
1114 #[must_use]
1115 pub fn from_loadout_spec_expect(
1119 loadout_spec: LoadoutSpec,
1120 rng: &mut impl Rng,
1121 time: Option<&(TimeOfDay, Calendar)>,
1122 ) -> Self {
1123 Self::from_loadout_spec(loadout_spec, rng, time).expect("failed to load loadout spec")
1124 }
1125
1126 #[must_use = "Method consumes builder and returns updated builder."]
1127 pub fn with_default_maintool(self, body: &Body) -> Self {
1129 self.active_mainhand(default_main_tool(body).map(Item::new_from_asset_expect))
1130 }
1131
1132 #[must_use = "Method consumes builder and returns updated builder."]
1133 pub fn with_default_equipment(self, body: &Body) -> Self {
1135 let chest = default_chest(body);
1136
1137 if let Some(chest) = chest {
1138 self.chest(Some(Item::new_from_asset_expect(chest)))
1139 } else {
1140 self
1141 }
1142 }
1143
1144 #[must_use = "Method consumes builder and returns updated builder."]
1145 pub fn with_preset(mut self, preset: Preset) -> Self {
1146 let rng = &mut rand::thread_rng();
1147 match preset {
1148 Preset::HuskSummon => {
1149 self = self.with_asset_expect("common.loadout.dungeon.cultist.husk", rng, None);
1150 },
1151 Preset::BorealSummon => {
1152 self =
1153 self.with_asset_expect("common.loadout.world.boreal.boreal_warrior", rng, None);
1154 },
1155 Preset::AshenSummon => {
1156 self =
1157 self.with_asset_expect("common.loadout.world.ashen.ashen_warrior", rng, None);
1158 },
1159 Preset::IronDwarfSummon => {
1160 self = self.with_asset_expect(
1161 "common.loadout.dungeon.dwarven_quarry.iron_dwarf",
1162 rng,
1163 None,
1164 );
1165 },
1166 Preset::ShamanicSpiritSummon => {
1167 self = self.with_asset_expect(
1168 "common.loadout.dungeon.terracotta.shamanic_spirit",
1169 rng,
1170 None,
1171 );
1172 },
1173 Preset::JiangshiSummon => {
1174 self =
1175 self.with_asset_expect("common.loadout.dungeon.terracotta.jiangshi", rng, None);
1176 },
1177 Preset::BloodservantSummon => {
1178 self = self.with_asset_expect(
1179 "common.loadout.dungeon.vampire.bloodservant",
1180 rng,
1181 None,
1182 );
1183 },
1184 }
1185
1186 self
1187 }
1188
1189 #[must_use = "Method consumes builder and returns updated builder."]
1190 pub fn with_creator(
1191 mut self,
1192 creator: fn(
1193 LoadoutBuilder,
1194 Option<&SiteInformation>,
1195 time: Option<&(TimeOfDay, Calendar)>,
1196 ) -> LoadoutBuilder,
1197 economy: Option<&SiteInformation>,
1198 time: Option<&(TimeOfDay, Calendar)>,
1199 ) -> LoadoutBuilder {
1200 self = creator(self, economy, time);
1201
1202 self
1203 }
1204
1205 #[must_use = "Method consumes builder and returns updated builder."]
1206 fn with_loadout_spec<R: Rng>(
1207 mut self,
1208 spec: LoadoutSpec,
1209 rng: &mut R,
1210 time: Option<&(TimeOfDay, Calendar)>,
1211 ) -> Result<Self, SpecError> {
1212 let spec = spec.eval(rng)?;
1214
1215 let mut to_item = |maybe_item: Option<ItemSpec>| {
1217 if let Some(item) = maybe_item {
1218 item.try_to_item(rng, time)
1219 } else {
1220 Ok(None)
1221 }
1222 };
1223
1224 let to_pair = |maybe_hands: Option<Hands>, rng: &mut R| {
1225 if let Some(hands) = maybe_hands {
1226 hands.try_to_pair(rng, time)
1227 } else {
1228 Ok((None, None))
1229 }
1230 };
1231
1232 if let Some(item) = to_item(spec.head)? {
1234 self = self.head(Some(item));
1235 }
1236 if let Some(item) = to_item(spec.neck)? {
1237 self = self.neck(Some(item));
1238 }
1239 if let Some(item) = to_item(spec.shoulders)? {
1240 self = self.shoulder(Some(item));
1241 }
1242 if let Some(item) = to_item(spec.chest)? {
1243 self = self.chest(Some(item));
1244 }
1245 if let Some(item) = to_item(spec.gloves)? {
1246 self = self.hands(Some(item));
1247 }
1248 if let Some(item) = to_item(spec.ring1)? {
1249 self = self.ring1(Some(item));
1250 }
1251 if let Some(item) = to_item(spec.ring2)? {
1252 self = self.ring2(Some(item));
1253 }
1254 if let Some(item) = to_item(spec.back)? {
1255 self = self.back(Some(item));
1256 }
1257 if let Some(item) = to_item(spec.belt)? {
1258 self = self.belt(Some(item));
1259 }
1260 if let Some(item) = to_item(spec.legs)? {
1261 self = self.pants(Some(item));
1262 }
1263 if let Some(item) = to_item(spec.feet)? {
1264 self = self.feet(Some(item));
1265 }
1266 if let Some(item) = to_item(spec.tabard)? {
1267 self = self.tabard(Some(item));
1268 }
1269 if let Some(item) = to_item(spec.bag1)? {
1270 self = self.bag(ArmorSlot::Bag1, Some(item));
1271 }
1272 if let Some(item) = to_item(spec.bag2)? {
1273 self = self.bag(ArmorSlot::Bag2, Some(item));
1274 }
1275 if let Some(item) = to_item(spec.bag3)? {
1276 self = self.bag(ArmorSlot::Bag3, Some(item));
1277 }
1278 if let Some(item) = to_item(spec.bag4)? {
1279 self = self.bag(ArmorSlot::Bag4, Some(item));
1280 }
1281 if let Some(item) = to_item(spec.lantern)? {
1282 self = self.lantern(Some(item));
1283 }
1284 if let Some(item) = to_item(spec.glider)? {
1285 self = self.glider(Some(item));
1286 }
1287 let (active_mainhand, active_offhand) = to_pair(spec.active_hands, rng)?;
1288 if let Some(item) = active_mainhand {
1289 self = self.active_mainhand(Some(item));
1290 }
1291 if let Some(item) = active_offhand {
1292 self = self.active_offhand(Some(item));
1293 }
1294 let (inactive_mainhand, inactive_offhand) = to_pair(spec.inactive_hands, rng)?;
1295 if let Some(item) = inactive_mainhand {
1296 self = self.inactive_mainhand(Some(item));
1297 }
1298 if let Some(item) = inactive_offhand {
1299 self = self.inactive_offhand(Some(item));
1300 }
1301
1302 Ok(self)
1303 }
1304
1305 #[must_use = "Method consumes builder and returns updated builder."]
1306 pub fn with_asset(
1307 self,
1308 asset_specifier: &str,
1309 rng: &mut impl Rng,
1310 time: Option<&(TimeOfDay, Calendar)>,
1311 ) -> Result<Self, SpecError> {
1312 let spec =
1313 LoadoutSpec::load_cloned(asset_specifier).map_err(SpecError::LoadoutAssetError)?;
1314 self.with_loadout_spec(spec, rng, time)
1315 }
1316
1317 #[must_use = "Method consumes builder and returns updated builder."]
1325 pub fn with_asset_expect(
1326 self,
1327 asset_specifier: &str,
1328 rng: &mut impl Rng,
1329 time: Option<&(TimeOfDay, Calendar)>,
1330 ) -> Self {
1331 self.with_asset(asset_specifier, rng, time)
1332 .expect("failed loading loadout config")
1333 }
1334
1335 #[must_use = "Method consumes builder and returns updated builder."]
1338 pub fn defaults(self) -> Self {
1339 let rng = &mut rand::thread_rng();
1340 self.with_asset_expect("common.loadout.default", rng, None)
1341 }
1342
1343 #[must_use = "Method consumes builder and returns updated builder."]
1344 fn with_equipment(mut self, equip_slot: EquipSlot, item: Option<Item>) -> Self {
1345 assert!(
1347 item.as_ref()
1348 .is_none_or(|item| equip_slot.can_hold(&item.kind()))
1349 );
1350
1351 let time = Time(0.0);
1356
1357 self.0.swap(equip_slot, item, time);
1358 self
1359 }
1360
1361 #[must_use = "Method consumes builder and returns updated builder."]
1362 fn with_armor(self, armor_slot: ArmorSlot, item: Option<Item>) -> Self {
1363 self.with_equipment(EquipSlot::Armor(armor_slot), item)
1364 }
1365
1366 #[must_use = "Method consumes builder and returns updated builder."]
1367 pub fn active_mainhand(self, item: Option<Item>) -> Self {
1368 self.with_equipment(EquipSlot::ActiveMainhand, item)
1369 }
1370
1371 #[must_use = "Method consumes builder and returns updated builder."]
1372 pub fn active_offhand(self, item: Option<Item>) -> Self {
1373 self.with_equipment(EquipSlot::ActiveOffhand, item)
1374 }
1375
1376 #[must_use = "Method consumes builder and returns updated builder."]
1377 pub fn inactive_mainhand(self, item: Option<Item>) -> Self {
1378 self.with_equipment(EquipSlot::InactiveMainhand, item)
1379 }
1380
1381 #[must_use = "Method consumes builder and returns updated builder."]
1382 pub fn inactive_offhand(self, item: Option<Item>) -> Self {
1383 self.with_equipment(EquipSlot::InactiveOffhand, item)
1384 }
1385
1386 #[must_use = "Method consumes builder and returns updated builder."]
1387 pub fn shoulder(self, item: Option<Item>) -> Self {
1388 self.with_armor(ArmorSlot::Shoulders, item)
1389 }
1390
1391 #[must_use = "Method consumes builder and returns updated builder."]
1392 pub fn chest(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Chest, item) }
1393
1394 #[must_use = "Method consumes builder and returns updated builder."]
1395 pub fn belt(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Belt, item) }
1396
1397 #[must_use = "Method consumes builder and returns updated builder."]
1398 pub fn hands(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Hands, item) }
1399
1400 #[must_use = "Method consumes builder and returns updated builder."]
1401 pub fn pants(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Legs, item) }
1402
1403 #[must_use = "Method consumes builder and returns updated builder."]
1404 pub fn feet(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Feet, item) }
1405
1406 #[must_use = "Method consumes builder and returns updated builder."]
1407 pub fn back(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Back, item) }
1408
1409 #[must_use = "Method consumes builder and returns updated builder."]
1410 pub fn ring1(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Ring1, item) }
1411
1412 #[must_use = "Method consumes builder and returns updated builder."]
1413 pub fn ring2(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Ring2, item) }
1414
1415 #[must_use = "Method consumes builder and returns updated builder."]
1416 pub fn neck(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Neck, item) }
1417
1418 #[must_use = "Method consumes builder and returns updated builder."]
1419 pub fn lantern(self, item: Option<Item>) -> Self {
1420 self.with_equipment(EquipSlot::Lantern, item)
1421 }
1422
1423 #[must_use = "Method consumes builder and returns updated builder."]
1424 pub fn glider(self, item: Option<Item>) -> Self { self.with_equipment(EquipSlot::Glider, item) }
1425
1426 #[must_use = "Method consumes builder and returns updated builder."]
1427 pub fn head(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Head, item) }
1428
1429 #[must_use = "Method consumes builder and returns updated builder."]
1430 pub fn tabard(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Tabard, item) }
1431
1432 #[must_use = "Method consumes builder and returns updated builder."]
1433 pub fn bag(self, which: ArmorSlot, item: Option<Item>) -> Self { self.with_armor(which, item) }
1434
1435 #[must_use]
1436 pub fn build(self) -> Loadout { self.0 }
1437}
1438
1439#[cfg(test)]
1440mod tests {
1441 use super::*;
1442 use crate::comp::Body;
1443 use strum::IntoEnumIterator;
1444
1445 #[test]
1450 fn test_loadout_species() {
1451 for body in Body::iter() {
1452 std::mem::drop(LoadoutBuilder::from_default(&body))
1453 }
1454 }
1455
1456 #[test]
1460 fn test_loadout_presets() {
1461 for preset in Preset::iter() {
1462 drop(LoadoutBuilder::empty().with_preset(preset));
1463 }
1464 }
1465
1466 #[test]
1473 fn validate_all_loadout_assets() {
1474 let loadouts = assets::load_rec_dir::<LoadoutSpec>("common.loadout")
1475 .expect("failed to load loadout directory");
1476 for loadout_id in loadouts.read().ids() {
1477 let loadout =
1478 LoadoutSpec::load_cloned(loadout_id).expect("failed to load loadout asset");
1479 loadout
1480 .validate(vec![loadout_id.to_string()])
1481 .unwrap_or_else(|e| panic!("{loadout_id} is broken: {e:?}"));
1482 }
1483 }
1484
1485 #[test]
1487 fn test_valid_assets() {
1488 let loadouts = assets::load_rec_dir::<LoadoutSpec>("test.loadout.ok")
1489 .expect("failed to load loadout directory");
1490
1491 for loadout_id in loadouts.read().ids() {
1492 let loadout =
1493 LoadoutSpec::load_cloned(loadout_id).expect("failed to load loadout asset");
1494 loadout
1495 .validate(vec![loadout_id.to_string()])
1496 .unwrap_or_else(|e| panic!("{loadout_id} is broken: {e:?}"));
1497 }
1498 }
1499}