1use crate::{
2 assets::{self, AssetExt},
3 calendar::{Calendar, CalendarEvent},
4 comp::{
5 Body, arthropod, biped_large, biped_small, bird_large, bird_medium, crustacean, golem,
6 inventory::{
7 loadout::Loadout,
8 slot::{ArmorSlot, EquipSlot},
9 },
10 item::{self, Item},
11 object, quadruped_low, quadruped_medium, quadruped_small, theropod,
12 },
13 resources::{Time, TimeOfDay},
14 trade::SiteInformation,
15};
16use rand::{self, Rng, distributions::WeightedError, seq::SliceRandom};
17use serde::{Deserialize, Serialize};
18use strum::EnumIter;
19use tracing::warn;
20
21type Weight = u8;
22
23#[derive(Debug)]
24pub enum SpecError {
25 LoadoutAssetError(assets::Error),
26 ItemAssetError(assets::Error),
27 ItemChoiceError(WeightedError),
28 BaseChoiceError(WeightedError),
29 ModularWeaponCreationError(item::modular::ModularWeaponCreationError),
30}
31
32#[derive(Debug)]
33#[cfg(test)]
34pub enum ValidationError {
35 ItemAssetError(assets::Error),
36 LoadoutAssetError(assets::Error),
37 Loop(Vec<String>),
38 ModularWeaponCreationError(item::modular::ModularWeaponCreationError),
39}
40
41#[derive(Debug, Deserialize, Clone)]
42pub enum ItemSpec {
43 Item(String),
44 ModularWeapon {
47 tool: item::tool::ToolKind,
48 material: item::Material,
49 hands: Option<item::tool::Hands>,
50 },
51 Choice(Vec<(Weight, Option<ItemSpec>)>),
52 Seasonal(Vec<(Option<CalendarEvent>, ItemSpec)>),
53}
54
55impl ItemSpec {
56 fn try_to_item(
57 &self,
58 rng: &mut impl Rng,
59 time: Option<&(TimeOfDay, Calendar)>,
60 ) -> Result<Option<Item>, SpecError> {
61 match self {
62 ItemSpec::Item(item_asset) => {
63 let item = Item::new_from_asset(item_asset).map_err(SpecError::ItemAssetError)?;
64 Ok(Some(item))
65 },
66 ItemSpec::Choice(items) => {
67 let (_, item_spec) = items
68 .choose_weighted(rng, |(weight, _)| *weight)
69 .map_err(SpecError::ItemChoiceError)?;
70
71 let item = if let Some(item_spec) = item_spec {
72 item_spec.try_to_item(rng, time)?
73 } else {
74 None
75 };
76 Ok(item)
77 },
78 ItemSpec::ModularWeapon {
79 tool,
80 material,
81 hands,
82 } => item::modular::random_weapon(*tool, *material, *hands, rng)
83 .map(Some)
84 .map_err(SpecError::ModularWeaponCreationError),
85 ItemSpec::Seasonal(specs) => specs
86 .iter()
87 .find_map(|(season, spec)| match (season, time) {
88 (Some(season), Some((_time, calendar))) => {
89 if calendar.is_event(*season) {
90 Some(spec.try_to_item(rng, time))
91 } else {
92 None
93 }
94 },
95 (Some(_season), None) => None,
96 (None, _) => Some(spec.try_to_item(rng, time)),
97 })
98 .unwrap_or(Ok(None)),
99 }
100 }
101
102 #[cfg(test)]
104 fn validate(&self) -> Result<(), ValidationError> {
105 let mut rng = rand::thread_rng();
106 match self {
107 ItemSpec::Item(item_asset) => Item::new_from_asset(item_asset)
108 .map(drop)
109 .map_err(ValidationError::ItemAssetError),
110 ItemSpec::Choice(choices) => {
111 for (_weight, choice) in choices {
113 if let Some(item) = choice {
114 item.validate()?;
115 }
116 }
117 Ok(())
118 },
119 ItemSpec::ModularWeapon {
120 tool,
121 material,
122 hands,
123 } => item::modular::random_weapon(*tool, *material, *hands, &mut rng)
124 .map(drop)
125 .map_err(ValidationError::ModularWeaponCreationError),
126 ItemSpec::Seasonal(specs) => {
127 specs.iter().try_for_each(|(_season, spec)| spec.validate())
128 },
129 }
130 }
131}
132
133#[derive(Debug, Deserialize, Clone)]
134pub enum Hands {
135 InHands((Option<ItemSpec>, Option<ItemSpec>)),
137 Choice(Vec<(Weight, Hands)>),
139}
140
141impl Hands {
142 fn try_to_pair(
143 &self,
144 rng: &mut impl Rng,
145 time: Option<&(TimeOfDay, Calendar)>,
146 ) -> Result<(Option<Item>, Option<Item>), SpecError> {
147 match self {
148 Hands::InHands((mainhand, offhand)) => {
149 let mut from_spec = |i: &ItemSpec| i.try_to_item(rng, time);
150
151 let mainhand = mainhand.as_ref().map(&mut from_spec).transpose()?.flatten();
152 let offhand = offhand.as_ref().map(&mut from_spec).transpose()?.flatten();
153 Ok((mainhand, offhand))
154 },
155 Hands::Choice(pairs) => {
156 let (_, pair_spec) = pairs
157 .choose_weighted(rng, |(weight, _)| *weight)
158 .map_err(SpecError::ItemChoiceError)?;
159
160 pair_spec.try_to_pair(rng, time)
161 },
162 }
163 }
164
165 #[cfg(test)]
167 fn validate(&self) -> Result<(), ValidationError> {
168 match self {
169 Self::InHands((left, right)) => {
170 if let Some(hand) = left {
171 hand.validate()?;
172 }
173 if let Some(hand) = right {
174 hand.validate()?;
175 }
176 Ok(())
177 },
178 Self::Choice(choices) => {
179 for (_weight, choice) in choices {
181 choice.validate()?;
182 }
183 Ok(())
184 },
185 }
186 }
187}
188
189#[derive(Debug, Deserialize, Clone)]
190pub enum Base {
191 Asset(String),
192 Combine(Vec<Base>),
195 Choice(Vec<(Weight, Base)>),
196}
197
198impl Base {
199 fn to_spec(&self, rng: &mut impl Rng) -> Result<LoadoutSpec, SpecError> {
204 match self {
205 Base::Asset(asset_specifier) => {
206 LoadoutSpec::load_cloned(asset_specifier).map_err(SpecError::LoadoutAssetError)
207 },
208 Base::Combine(bases) => {
209 let bases = bases.iter().map(|b| b.to_spec(rng)?.eval(rng));
210 let mut current = LoadoutSpec::default();
212 for base in bases {
213 current = current.merge(base?);
214 }
215
216 Ok(current)
217 },
218 Base::Choice(choice) => {
219 let (_, base) = choice
220 .choose_weighted(rng, |(weight, _)| *weight)
221 .map_err(SpecError::BaseChoiceError)?;
222
223 base.to_spec(rng)
224 },
225 }
226 }
227}
228
229#[derive(Debug, Deserialize, Clone, Default)]
237#[serde(deny_unknown_fields)]
238pub struct LoadoutSpec {
239 pub inherit: Option<Base>,
241 pub head: Option<ItemSpec>,
243 pub neck: Option<ItemSpec>,
244 pub shoulders: Option<ItemSpec>,
245 pub chest: Option<ItemSpec>,
246 pub gloves: Option<ItemSpec>,
247 pub ring1: Option<ItemSpec>,
248 pub ring2: Option<ItemSpec>,
249 pub back: Option<ItemSpec>,
250 pub belt: Option<ItemSpec>,
251 pub legs: Option<ItemSpec>,
252 pub feet: Option<ItemSpec>,
253 pub tabard: Option<ItemSpec>,
254 pub bag1: Option<ItemSpec>,
255 pub bag2: Option<ItemSpec>,
256 pub bag3: Option<ItemSpec>,
257 pub bag4: Option<ItemSpec>,
258 pub lantern: Option<ItemSpec>,
259 pub glider: Option<ItemSpec>,
260 pub active_hands: Option<Hands>,
262 pub inactive_hands: Option<Hands>,
263}
264
265impl LoadoutSpec {
266 fn merge(self, base: Self) -> Self {
295 Self {
296 inherit: base.inherit,
297 head: self.head.or(base.head),
298 neck: self.neck.or(base.neck),
299 shoulders: self.shoulders.or(base.shoulders),
300 chest: self.chest.or(base.chest),
301 gloves: self.gloves.or(base.gloves),
302 ring1: self.ring1.or(base.ring1),
303 ring2: self.ring2.or(base.ring2),
304 back: self.back.or(base.back),
305 belt: self.belt.or(base.belt),
306 legs: self.legs.or(base.legs),
307 feet: self.feet.or(base.feet),
308 tabard: self.tabard.or(base.tabard),
309 bag1: self.bag1.or(base.bag1),
310 bag2: self.bag2.or(base.bag2),
311 bag3: self.bag3.or(base.bag3),
312 bag4: self.bag4.or(base.bag4),
313 lantern: self.lantern.or(base.lantern),
314 glider: self.glider.or(base.glider),
315 active_hands: self.active_hands.or(base.active_hands),
316 inactive_hands: self.inactive_hands.or(base.inactive_hands),
317 }
318 }
319
320 fn eval(self, rng: &mut impl Rng) -> Result<Self, SpecError> {
346 if let Some(ref base) = self.inherit {
348 let base = base.to_spec(rng)?.eval(rng);
349 Ok(self.merge(base?))
350 } else {
351 Ok(self)
352 }
353 }
354
355 #[cfg(test)]
369 pub fn validate(&self, history: Vec<String>) -> Result<(), ValidationError> {
370 fn validate_base(base: &Base, mut history: Vec<String>) -> Result<(), ValidationError> {
376 match base {
377 Base::Asset(asset) => {
378 let based = LoadoutSpec::load_cloned(asset)
380 .map_err(ValidationError::LoadoutAssetError)?;
381
382 history.push(asset.to_owned());
384
385 based.validate(history)
387 },
388 Base::Combine(bases) => {
389 for base in bases {
390 validate_base(base, history.clone())?;
391 }
392 Ok(())
393 },
394 Base::Choice(choices) => {
395 for (_weight, base) in choices {
397 validate_base(base, history.clone())?;
398 }
399 Ok(())
400 },
401 }
402 }
403
404 if let Some((last, tail)) = history.split_last() {
413 for asset in tail {
414 if last == asset {
415 return Err(ValidationError::Loop(history));
416 }
417 }
418 }
419
420 if let Some(base) = &self.inherit {
421 validate_base(base, history)?
422 }
423
424 self.validate_entries()
425 }
426
427 #[cfg(test)]
435 fn validate_entries(&self) -> Result<(), ValidationError> {
436 if let Some(item) = &self.head {
438 item.validate()?;
439 }
440 if let Some(item) = &self.neck {
441 item.validate()?;
442 }
443 if let Some(item) = &self.shoulders {
444 item.validate()?;
445 }
446 if let Some(item) = &self.chest {
447 item.validate()?;
448 }
449 if let Some(item) = &self.gloves {
450 item.validate()?;
451 }
452 if let Some(item) = &self.ring1 {
453 item.validate()?;
454 }
455 if let Some(item) = &self.ring2 {
456 item.validate()?;
457 }
458 if let Some(item) = &self.back {
459 item.validate()?;
460 }
461 if let Some(item) = &self.belt {
462 item.validate()?;
463 }
464 if let Some(item) = &self.legs {
465 item.validate()?;
466 }
467 if let Some(item) = &self.feet {
468 item.validate()?;
469 }
470 if let Some(item) = &self.tabard {
471 item.validate()?;
472 }
473 if let Some(item) = &self.bag1 {
475 item.validate()?;
476 }
477 if let Some(item) = &self.bag2 {
478 item.validate()?;
479 }
480 if let Some(item) = &self.bag3 {
481 item.validate()?;
482 }
483 if let Some(item) = &self.bag4 {
484 item.validate()?;
485 }
486 if let Some(item) = &self.lantern {
487 item.validate()?;
488 }
489 if let Some(item) = &self.glider {
490 item.validate()?;
491 }
492 if let Some(hands) = &self.active_hands {
494 hands.validate()?;
495 }
496 if let Some(hands) = &self.inactive_hands {
497 hands.validate()?;
498 }
499
500 Ok(())
501 }
502}
503
504impl assets::Asset for LoadoutSpec {
505 type Loader = assets::RonLoader;
506
507 const EXTENSION: &'static str = "ron";
508}
509
510#[must_use]
511pub fn make_potion_bag(quantity: u32) -> Item {
512 let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.tiny_leather_pouch");
513 if let Some(i) = bag.slots_mut().iter_mut().next() {
514 let mut potions = Item::new_from_asset_expect("common.items.consumable.potion_big");
515 if let Err(e) = potions.set_amount(quantity) {
516 warn!("Failed to set potion quantity: {:?}", e);
517 }
518 *i = Some(potions);
519 }
520 bag
521}
522
523#[must_use]
524pub fn make_food_bag(quantity: u32) -> Item {
525 let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.tiny_leather_pouch");
526 if let Some(i) = bag.slots_mut().iter_mut().next() {
527 let mut food = Item::new_from_asset_expect("common.items.food.apple_stick");
528 if let Err(e) = food.set_amount(quantity) {
529 warn!("Failed to set food quantity: {:?}", e);
530 }
531 *i = Some(food);
532 }
533 bag
534}
535
536#[must_use]
537pub fn default_chest(body: &Body) -> Option<&'static str> {
538 match body {
539 Body::BipedLarge(body) => match body.species {
540 biped_large::Species::Mindflayer => {
541 Some("common.items.npc_armor.biped_large.mindflayer")
542 },
543 biped_large::Species::Minotaur => Some("common.items.npc_armor.biped_large.minotaur"),
544 biped_large::Species::Tidalwarrior => {
545 Some("common.items.npc_armor.biped_large.tidal_warrior")
546 },
547 biped_large::Species::Yeti => Some("common.items.npc_armor.biped_large.yeti"),
548 biped_large::Species::Harvester => Some("common.items.npc_armor.biped_large.harvester"),
549 biped_large::Species::Ogre
550 | biped_large::Species::Blueoni
551 | biped_large::Species::Redoni
552 | biped_large::Species::Cavetroll
553 | biped_large::Species::Mountaintroll
554 | biped_large::Species::Swamptroll
555 | biped_large::Species::Wendigo => Some("common.items.npc_armor.biped_large.generic"),
556 biped_large::Species::Cyclops => Some("common.items.npc_armor.biped_large.cyclops"),
557 biped_large::Species::Dullahan => Some("common.items.npc_armor.biped_large.dullahan"),
558 biped_large::Species::Tursus => Some("common.items.npc_armor.biped_large.tursus"),
559 biped_large::Species::Cultistwarlord => {
560 Some("common.items.npc_armor.biped_large.warlord")
561 },
562 biped_large::Species::Cultistwarlock => {
563 Some("common.items.npc_armor.biped_large.warlock")
564 },
565 biped_large::Species::Gigasfrost => {
566 Some("common.items.npc_armor.biped_large.gigas_frost")
567 },
568 biped_large::Species::HaniwaGeneral => {
569 Some("common.items.npc_armor.biped_large.haniwageneral")
570 },
571 biped_large::Species::TerracottaBesieger
572 | biped_large::Species::TerracottaDemolisher
573 | biped_large::Species::TerracottaPunisher
574 | biped_large::Species::TerracottaPursuer
575 | biped_large::Species::Cursekeeper => {
576 Some("common.items.npc_armor.biped_large.terracotta")
577 },
578 biped_large::Species::Forgemaster => {
579 Some("common.items.npc_armor.biped_large.forgemaster")
580 },
581 _ => None,
582 },
583 Body::BirdLarge(body) => match body.species {
584 bird_large::Species::FlameWyvern
585 | bird_large::Species::FrostWyvern
586 | bird_large::Species::CloudWyvern
587 | bird_large::Species::SeaWyvern
588 | bird_large::Species::WealdWyvern => Some("common.items.npc_armor.bird_large.wyvern"),
589 bird_large::Species::Phoenix => Some("common.items.npc_armor.bird_large.phoenix"),
590 _ => None,
591 },
592 Body::BirdMedium(body) => match body.species {
593 bird_medium::Species::BloodmoonBat => {
594 Some("common.items.npc_armor.bird_medium.bloodmoon_bat")
595 },
596 _ => None,
597 },
598 Body::Golem(body) => match body.species {
599 golem::Species::ClayGolem => Some("common.items.npc_armor.golem.claygolem"),
600 golem::Species::Gravewarden => Some("common.items.npc_armor.golem.gravewarden"),
601 golem::Species::WoodGolem => Some("common.items.npc_armor.golem.woodgolem"),
602 golem::Species::AncientEffigy => Some("common.items.npc_armor.golem.ancienteffigy"),
603 golem::Species::Mogwai => Some("common.items.npc_armor.golem.mogwai"),
604 golem::Species::IronGolem => Some("common.items.npc_armor.golem.irongolem"),
605 _ => None,
606 },
607 Body::QuadrupedLow(body) => match body.species {
608 quadruped_low::Species::Sandshark
609 | quadruped_low::Species::Alligator
610 | quadruped_low::Species::Crocodile
611 | quadruped_low::Species::SeaCrocodile
612 | quadruped_low::Species::Icedrake
613 | quadruped_low::Species::Lavadrake
614 | quadruped_low::Species::Mossdrake => Some("common.items.npc_armor.generic"),
615 quadruped_low::Species::Reefsnapper
616 | quadruped_low::Species::Rocksnapper
617 | quadruped_low::Species::Rootsnapper
618 | quadruped_low::Species::Tortoise
619 | quadruped_low::Species::Basilisk
620 | quadruped_low::Species::Hydra => Some("common.items.npc_armor.generic_high"),
621 quadruped_low::Species::Dagon => Some("common.items.npc_armor.quadruped_low.dagon"),
622 _ => None,
623 },
624 Body::QuadrupedMedium(body) => match body.species {
625 quadruped_medium::Species::Bonerattler => Some("common.items.npc_armor.generic"),
626 quadruped_medium::Species::Tarasque => Some("common.items.npc_armor.generic_high"),
627 quadruped_medium::Species::ClaySteed => {
628 Some("common.items.npc_armor.quadruped_medium.claysteed")
629 },
630 _ => None,
631 },
632 Body::Theropod(body) => match body.species {
633 theropod::Species::Archaeos | theropod::Species::Ntouka => {
634 Some("common.items.npc_armor.generic")
635 },
636 theropod::Species::Dodarock => Some("common.items.npc_armor.generic_high"),
637 _ => None,
638 },
639 Body::Arthropod(body) => match body.species {
641 arthropod::Species::Blackwidow
642 | arthropod::Species::Cavespider
643 | arthropod::Species::Emberfly
644 | arthropod::Species::Moltencrawler
645 | arthropod::Species::Mosscrawler
646 | arthropod::Species::Sandcrawler
647 | arthropod::Species::Tarantula => None,
648 _ => Some("common.items.npc_armor.generic"),
649 },
650 Body::QuadrupedSmall(body) => match body.species {
651 quadruped_small::Species::Turtle
652 | quadruped_small::Species::Holladon
653 | quadruped_small::Species::TreantSapling
654 | quadruped_small::Species::MossySnail => Some("common.items.npc_armor.generic"),
655 _ => None,
656 },
657 Body::Crustacean(body) => match body.species {
658 crustacean::Species::Karkatha => Some("common.items.npc_armor.crustacean.karkatha"),
659 _ => None,
660 },
661 _ => None,
662 }
663}
664
665#[must_use]
666#[expect(clippy::too_many_lines, clippy::match_wildcard_for_single_variants)]
670pub fn default_main_tool(body: &Body) -> Option<&'static str> {
671 match body {
672 Body::Golem(golem) => match golem.species {
673 golem::Species::StoneGolem => Some("common.items.npc_weapons.unique.stone_golems_fist"),
674 golem::Species::ClayGolem => Some("common.items.npc_weapons.unique.clay_golem_fist"),
675 golem::Species::Gravewarden => Some("common.items.npc_weapons.unique.gravewarden_fist"),
676 golem::Species::WoodGolem => Some("common.items.npc_weapons.unique.wood_golem_fist"),
677 golem::Species::CoralGolem => Some("common.items.npc_weapons.unique.coral_golem_fist"),
678 golem::Species::AncientEffigy => {
679 Some("common.items.npc_weapons.unique.ancient_effigy_eyes")
680 },
681 golem::Species::Mogwai => Some("common.items.npc_weapons.unique.mogwai"),
682 golem::Species::IronGolem => Some("common.items.npc_weapons.unique.iron_golem_fist"),
683 _ => None,
684 },
685 Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
686 quadruped_medium::Species::Wolf => {
687 Some("common.items.npc_weapons.unique.quadruped_medium.wolf")
688 },
689 quadruped_medium::Species::Alpaca | quadruped_medium::Species::Llama => {
691 Some("common.items.npc_weapons.unique.quadruped_medium.alpaca")
692 },
693 quadruped_medium::Species::Antelope | quadruped_medium::Species::Deer => {
694 Some("common.items.npc_weapons.unique.quadruped_medium.antelope")
695 },
696 quadruped_medium::Species::Donkey | quadruped_medium::Species::Zebra => {
697 Some("common.items.npc_weapons.unique.quadruped_medium.donkey")
698 },
699 quadruped_medium::Species::Horse | quadruped_medium::Species::Kelpie => {
701 Some("common.items.npc_weapons.unique.quadruped_medium.horse")
702 },
703 quadruped_medium::Species::ClaySteed => {
704 Some("common.items.npc_weapons.unique.claysteed")
705 },
706 quadruped_medium::Species::Saber
707 | quadruped_medium::Species::Bonerattler
708 | quadruped_medium::Species::Lion
709 | quadruped_medium::Species::Snowleopard => {
710 Some("common.items.npc_weapons.unique.quadmedjump")
711 },
712 quadruped_medium::Species::Darkhound => {
713 Some("common.items.npc_weapons.unique.darkhound")
714 },
715 quadruped_medium::Species::Moose | quadruped_medium::Species::Tuskram => {
717 Some("common.items.npc_weapons.unique.quadruped_medium.moose")
718 },
719 quadruped_medium::Species::Mouflon => {
720 Some("common.items.npc_weapons.unique.quadruped_medium.mouflon")
721 },
722 quadruped_medium::Species::Akhlut
723 | quadruped_medium::Species::Dreadhorn
724 | quadruped_medium::Species::Mammoth
725 | quadruped_medium::Species::Ngoubou => {
726 Some("common.items.npc_weapons.unique.quadmedcharge")
727 },
728 quadruped_medium::Species::Grolgar => {
729 Some("common.items.npc_weapons.unique.quadruped_medium.grolgar")
730 },
731 quadruped_medium::Species::Roshwalr => Some("common.items.npc_weapons.unique.roshwalr"),
732 quadruped_medium::Species::Cattle => {
733 Some("common.items.npc_weapons.unique.quadmedbasicgentle")
734 },
735 quadruped_medium::Species::Highland | quadruped_medium::Species::Yak => {
736 Some("common.items.npc_weapons.unique.quadruped_medium.highland")
737 },
738 quadruped_medium::Species::Frostfang => {
739 Some("common.items.npc_weapons.unique.frostfang")
740 },
741 _ => Some("common.items.npc_weapons.unique.quadmedbasic"),
742 },
743 Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
744 quadruped_low::Species::Maneater => {
745 Some("common.items.npc_weapons.unique.quadruped_low.maneater")
746 },
747 quadruped_low::Species::Asp => {
748 Some("common.items.npc_weapons.unique.quadruped_low.asp")
749 },
750 quadruped_low::Species::Dagon => Some("common.items.npc_weapons.unique.dagon"),
751 quadruped_low::Species::Snaretongue => {
752 Some("common.items.npc_weapons.unique.snaretongue")
753 },
754 quadruped_low::Species::Crocodile
755 | quadruped_low::Species::SeaCrocodile
756 | quadruped_low::Species::Alligator
757 | quadruped_low::Species::Salamander
758 | quadruped_low::Species::Elbst => Some("common.items.npc_weapons.unique.quadlowtail"),
759 quadruped_low::Species::Monitor | quadruped_low::Species::Pangolin => {
760 Some("common.items.npc_weapons.unique.quadlowquick")
761 },
762 quadruped_low::Species::Lavadrake => {
763 Some("common.items.npc_weapons.unique.quadruped_low.lavadrake")
764 },
765 quadruped_low::Species::Deadwood => {
766 Some("common.items.npc_weapons.unique.quadruped_low.deadwood")
767 },
768 quadruped_low::Species::Basilisk => {
769 Some("common.items.npc_weapons.unique.quadruped_low.basilisk")
770 },
771 quadruped_low::Species::Icedrake => {
772 Some("common.items.npc_weapons.unique.quadruped_low.icedrake")
773 },
774 quadruped_low::Species::Hakulaq => {
775 Some("common.items.npc_weapons.unique.quadruped_low.hakulaq")
776 },
777 quadruped_low::Species::Tortoise => {
778 Some("common.items.npc_weapons.unique.quadruped_low.tortoise")
779 },
780 quadruped_low::Species::Driggle => Some("common.items.npc_weapons.unique.driggle"),
781 quadruped_low::Species::Rocksnapper => {
782 Some("common.items.npc_weapons.unique.rocksnapper")
783 },
784 quadruped_low::Species::Hydra => {
785 Some("common.items.npc_weapons.unique.quadruped_low.hydra")
786 },
787 _ => Some("common.items.npc_weapons.unique.quadlowbasic"),
788 },
789 Body::QuadrupedSmall(quadruped_small) => match quadruped_small.species {
790 quadruped_small::Species::TreantSapling => {
791 Some("common.items.npc_weapons.unique.treantsapling")
792 },
793 quadruped_small::Species::MossySnail => {
794 Some("common.items.npc_weapons.unique.mossysnail")
795 },
796 quadruped_small::Species::Boar | quadruped_small::Species::Truffler => {
797 Some("common.items.npc_weapons.unique.quadruped_small.boar")
798 },
799 quadruped_small::Species::Hyena => {
800 Some("common.items.npc_weapons.unique.quadruped_small.hyena")
801 },
802 quadruped_small::Species::Beaver
803 | quadruped_small::Species::Dog
804 | quadruped_small::Species::Cat
805 | quadruped_small::Species::Goat
806 | quadruped_small::Species::Holladon
807 | quadruped_small::Species::Sheep
808 | quadruped_small::Species::Seal => {
809 Some("common.items.npc_weapons.unique.quadsmallbasic")
810 },
811 _ => Some("common.items.npc_weapons.unique.quadruped_small.rodent"),
812 },
813 Body::Theropod(theropod) => match theropod.species {
814 theropod::Species::Sandraptor
815 | theropod::Species::Snowraptor
816 | theropod::Species::Woodraptor
817 | theropod::Species::Axebeak
818 | theropod::Species::Sunlizard => Some("common.items.npc_weapons.unique.theropodbird"),
819 theropod::Species::Yale => Some("common.items.npc_weapons.unique.theropod.yale"),
820 theropod::Species::Dodarock => Some("common.items.npc_weapons.unique.theropodsmall"),
821 _ => Some("common.items.npc_weapons.unique.theropodbasic"),
822 },
823 Body::Arthropod(arthropod) => match arthropod.species {
824 arthropod::Species::Hornbeetle | arthropod::Species::Stagbeetle => {
825 Some("common.items.npc_weapons.unique.arthropods.hornbeetle")
826 },
827 arthropod::Species::Emberfly => Some("common.items.npc_weapons.unique.emberfly"),
828 arthropod::Species::Cavespider => {
829 Some("common.items.npc_weapons.unique.arthropods.cavespider")
830 },
831 arthropod::Species::Sandcrawler | arthropod::Species::Mosscrawler => {
832 Some("common.items.npc_weapons.unique.arthropods.mosscrawler")
833 },
834 arthropod::Species::Moltencrawler => {
835 Some("common.items.npc_weapons.unique.arthropods.moltencrawler")
836 },
837 arthropod::Species::Weevil => Some("common.items.npc_weapons.unique.arthropods.weevil"),
838 arthropod::Species::Blackwidow => {
839 Some("common.items.npc_weapons.unique.arthropods.blackwidow")
840 },
841 arthropod::Species::Tarantula => {
842 Some("common.items.npc_weapons.unique.arthropods.tarantula")
843 },
844 arthropod::Species::Antlion => {
845 Some("common.items.npc_weapons.unique.arthropods.antlion")
846 },
847 arthropod::Species::Dagonite => {
848 Some("common.items.npc_weapons.unique.arthropods.dagonite")
849 },
850 arthropod::Species::Leafbeetle => {
851 Some("common.items.npc_weapons.unique.arthropods.leafbeetle")
852 },
853 },
854 Body::BipedLarge(biped_large) => match (biped_large.species, biped_large.body_type) {
855 (biped_large::Species::Occultsaurok, _) => {
856 Some("common.items.npc_weapons.staff.saurok_staff")
857 },
858 (biped_large::Species::Mightysaurok, _) => {
859 Some("common.items.npc_weapons.sword.saurok_sword")
860 },
861 (biped_large::Species::Slysaurok, _) => Some("common.items.npc_weapons.bow.saurok_bow"),
862 (biped_large::Species::Ogre, biped_large::BodyType::Male) => {
863 Some("common.items.npc_weapons.hammer.ogre_hammer")
864 },
865 (biped_large::Species::Ogre, biped_large::BodyType::Female) => {
866 Some("common.items.npc_weapons.staff.ogre_staff")
867 },
868 (
869 biped_large::Species::Mountaintroll
870 | biped_large::Species::Swamptroll
871 | biped_large::Species::Cavetroll,
872 _,
873 ) => Some("common.items.npc_weapons.hammer.troll_hammer"),
874 (biped_large::Species::Wendigo, _) => {
875 Some("common.items.npc_weapons.unique.wendigo_magic")
876 },
877 (biped_large::Species::Werewolf, _) => {
878 Some("common.items.npc_weapons.unique.beast_claws")
879 },
880 (biped_large::Species::Tursus, _) => {
881 Some("common.items.npc_weapons.unique.tursus_claws")
882 },
883 (biped_large::Species::Cyclops, _) => {
884 Some("common.items.npc_weapons.hammer.cyclops_hammer")
885 },
886 (biped_large::Species::Dullahan, _) => {
887 Some("common.items.npc_weapons.sword.dullahan_sword")
888 },
889 (biped_large::Species::Mindflayer, _) => {
890 Some("common.items.npc_weapons.staff.mindflayer_staff")
891 },
892 (biped_large::Species::Minotaur, _) => {
893 Some("common.items.npc_weapons.axe.minotaur_axe")
894 },
895 (biped_large::Species::Tidalwarrior, _) => {
896 Some("common.items.npc_weapons.unique.tidal_claws")
897 },
898 (biped_large::Species::Yeti, _) => Some("common.items.npc_weapons.hammer.yeti_hammer"),
899 (biped_large::Species::Harvester, _) => {
900 Some("common.items.npc_weapons.hammer.harvester_scythe")
901 },
902 (biped_large::Species::Blueoni, _) => Some("common.items.npc_weapons.axe.oni_blue_axe"),
903 (biped_large::Species::Redoni, _) => {
904 Some("common.items.npc_weapons.hammer.oni_red_hammer")
905 },
906 (biped_large::Species::Cultistwarlord, _) => {
907 Some("common.items.npc_weapons.sword.bipedlarge-cultist")
908 },
909 (biped_large::Species::Cultistwarlock, _) => {
910 Some("common.items.npc_weapons.staff.bipedlarge-cultist")
911 },
912 (biped_large::Species::Huskbrute, _) => {
913 Some("common.items.npc_weapons.unique.husk_brute")
914 },
915 (biped_large::Species::Strigoi, _) => {
916 Some("common.items.npc_weapons.unique.strigoi_claws")
917 },
918 (biped_large::Species::Executioner, _) => {
919 Some("common.items.npc_weapons.axe.executioner_axe")
920 },
921 (biped_large::Species::Gigasfrost, _) => {
922 Some("common.items.npc_weapons.axe.gigas_frost_axe")
923 },
924 (biped_large::Species::AdletElder, _) => {
925 Some("common.items.npc_weapons.sword.adlet_elder_sword")
926 },
927 (biped_large::Species::SeaBishop, _) => {
928 Some("common.items.npc_weapons.unique.sea_bishop_sceptre")
929 },
930 (biped_large::Species::HaniwaGeneral, _) => {
931 Some("common.items.npc_weapons.sword.haniwa_general_sword")
932 },
933 (biped_large::Species::TerracottaBesieger, _) => {
934 Some("common.items.npc_weapons.bow.terracotta_besieger_bow")
935 },
936 (biped_large::Species::TerracottaDemolisher, _) => {
937 Some("common.items.npc_weapons.unique.terracotta_demolisher_fist")
938 },
939 (biped_large::Species::TerracottaPunisher, _) => {
940 Some("common.items.npc_weapons.hammer.terracotta_punisher_club")
941 },
942 (biped_large::Species::TerracottaPursuer, _) => {
943 Some("common.items.npc_weapons.sword.terracotta_pursuer_sword")
944 },
945 (biped_large::Species::Cursekeeper, _) => {
946 Some("common.items.npc_weapons.unique.cursekeeper_sceptre")
947 },
948 (biped_large::Species::Forgemaster, _) => {
949 Some("common.items.npc_weapons.hammer.forgemaster_hammer")
950 },
951 },
952 Body::Object(body) => match body {
953 object::Body::Crossbow => Some("common.items.npc_weapons.unique.turret"),
954 object::Body::Flamethrower | object::Body::Lavathrower => {
955 Some("common.items.npc_weapons.unique.flamethrower")
956 },
957 object::Body::BarrelOrgan => Some("common.items.npc_weapons.unique.organ"),
958 object::Body::TerracottaStatue => {
959 Some("common.items.npc_weapons.unique.terracotta_statue")
960 },
961 object::Body::HaniwaSentry => Some("common.items.npc_weapons.unique.haniwa_sentry"),
962 object::Body::SeaLantern => Some("common.items.npc_weapons.unique.tidal_totem"),
963 object::Body::Tornado => Some("common.items.npc_weapons.unique.tornado"),
964 object::Body::FieryTornado => Some("common.items.npc_weapons.unique.fiery_tornado"),
965 object::Body::GnarlingTotemRed => {
966 Some("common.items.npc_weapons.biped_small.gnarling.redtotem")
967 },
968 object::Body::GnarlingTotemGreen => {
969 Some("common.items.npc_weapons.biped_small.gnarling.greentotem")
970 },
971 object::Body::GnarlingTotemWhite => {
972 Some("common.items.npc_weapons.biped_small.gnarling.whitetotem")
973 },
974 _ => None,
975 },
976 Body::BipedSmall(biped_small) => match (biped_small.species, biped_small.body_type) {
977 (biped_small::Species::Gnome, _) => {
978 Some("common.items.npc_weapons.biped_small.adlet.tracker")
979 },
980 (biped_small::Species::Bushly, _) => Some("common.items.npc_weapons.unique.bushly"),
981 (biped_small::Species::Cactid, _) => Some("common.items.npc_weapons.unique.cactid"),
982 (biped_small::Species::Irrwurz, _) => Some("common.items.npc_weapons.unique.irrwurz"),
983 (biped_small::Species::Husk, _) => Some("common.items.npc_weapons.unique.husk"),
984 (biped_small::Species::Flamekeeper, _) => {
985 Some("common.items.npc_weapons.unique.flamekeeper_staff")
986 },
987 (biped_small::Species::IronDwarf, _) => {
988 Some("common.items.npc_weapons.unique.iron_dwarf")
989 },
990 (biped_small::Species::ShamanicSpirit, _) => {
991 Some("common.items.npc_weapons.unique.shamanic_spirit")
992 },
993 (biped_small::Species::Jiangshi, _) => Some("common.items.npc_weapons.unique.jiangshi"),
994 (biped_small::Species::BloodmoonHeiress, _) => {
995 Some("common.items.npc_weapons.biped_small.vampire.bloodmoon_heiress_sword")
996 },
997 (biped_small::Species::Bloodservant, _) => {
998 Some("common.items.npc_weapons.biped_small.vampire.bloodservant_axe")
999 },
1000 (biped_small::Species::Harlequin, _) => {
1001 Some("common.items.npc_weapons.biped_small.vampire.harlequin_dagger")
1002 },
1003 (biped_small::Species::GoblinThug, _) => {
1004 Some("common.items.npc_weapons.unique.goblin_thug_club")
1005 },
1006 (biped_small::Species::GoblinChucker, _) => {
1007 Some("common.items.npc_weapons.unique.goblin_chucker")
1008 },
1009 (biped_small::Species::GoblinRuffian, _) => {
1010 Some("common.items.npc_weapons.unique.goblin_ruffian_knife")
1011 },
1012 (biped_small::Species::GreenLegoom, _) => {
1013 Some("common.items.npc_weapons.unique.green_legoom_rake")
1014 },
1015 (biped_small::Species::OchreLegoom, _) => {
1016 Some("common.items.npc_weapons.unique.ochre_legoom_spade")
1017 },
1018 (biped_small::Species::PurpleLegoom, _) => {
1019 Some("common.items.npc_weapons.unique.purple_legoom_pitchfork")
1020 },
1021 (biped_small::Species::RedLegoom, _) => {
1022 Some("common.items.npc_weapons.unique.red_legoom_hoe")
1023 },
1024 _ => Some("common.items.npc_weapons.biped_small.adlet.hunter"),
1025 },
1026 Body::BirdLarge(bird_large) => match (bird_large.species, bird_large.body_type) {
1027 (bird_large::Species::Cockatrice, _) => {
1028 Some("common.items.npc_weapons.unique.birdlargebreathe")
1029 },
1030 (bird_large::Species::Phoenix, _) => {
1031 Some("common.items.npc_weapons.unique.birdlargefire")
1032 },
1033 (bird_large::Species::Roc, _) => Some("common.items.npc_weapons.unique.birdlargebasic"),
1034 (bird_large::Species::FlameWyvern, _) => {
1035 Some("common.items.npc_weapons.unique.flamewyvern")
1036 },
1037 (bird_large::Species::FrostWyvern, _) => {
1038 Some("common.items.npc_weapons.unique.frostwyvern")
1039 },
1040 (bird_large::Species::CloudWyvern, _) => {
1041 Some("common.items.npc_weapons.unique.cloudwyvern")
1042 },
1043 (bird_large::Species::SeaWyvern, _) => {
1044 Some("common.items.npc_weapons.unique.seawyvern")
1045 },
1046 (bird_large::Species::WealdWyvern, _) => {
1047 Some("common.items.npc_weapons.unique.wealdwyvern")
1048 },
1049 },
1050 Body::BirdMedium(bird_medium) => match bird_medium.species {
1051 bird_medium::Species::Cockatiel
1052 | bird_medium::Species::Bat
1053 | bird_medium::Species::Parrot
1054 | bird_medium::Species::Crow
1055 | bird_medium::Species::Parakeet => {
1056 Some("common.items.npc_weapons.unique.simpleflyingbasic")
1057 },
1058 bird_medium::Species::VampireBat => Some("common.items.npc_weapons.unique.vampire_bat"),
1059 bird_medium::Species::BloodmoonBat => {
1060 Some("common.items.npc_weapons.unique.bloodmoon_bat")
1061 },
1062 _ => Some("common.items.npc_weapons.unique.birdmediumbasic"),
1063 },
1064 Body::Crustacean(crustacean) => match crustacean.species {
1065 crustacean::Species::Crab | crustacean::Species::SoldierCrab => {
1066 Some("common.items.npc_weapons.unique.crab_pincer")
1067 },
1068 crustacean::Species::Karkatha => {
1069 Some("common.items.npc_weapons.unique.karkatha_pincer")
1070 },
1071 },
1072 _ => None,
1073 }
1074}
1075
1076#[derive(Clone)]
1092pub struct LoadoutBuilder(Loadout);
1093
1094#[derive(Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Debug, EnumIter)]
1095pub enum Preset {
1096 HuskSummon,
1097 BorealSummon,
1098 IronDwarfSummon,
1099 ShamanicSpiritSummon,
1100 JiangshiSummon,
1101 BloodservantSummon,
1102}
1103
1104impl LoadoutBuilder {
1105 #[must_use]
1106 pub fn empty() -> Self { Self(Loadout::new_empty()) }
1107
1108 #[must_use]
1109 pub fn from_asset_expect(
1112 asset_specifier: &str,
1113 rng: &mut impl Rng,
1114 time: Option<&(TimeOfDay, Calendar)>,
1115 ) -> Self {
1116 Self::from_asset(asset_specifier, rng, time).expect("failed to load loadut config")
1117 }
1118
1119 pub fn from_asset(
1121 asset_specifier: &str,
1122 rng: &mut impl Rng,
1123 time: Option<&(TimeOfDay, Calendar)>,
1124 ) -> Result<Self, SpecError> {
1125 let loadout = Self::empty();
1126 loadout.with_asset(asset_specifier, rng, time)
1127 }
1128
1129 #[must_use]
1130 pub fn from_default(body: &Body) -> Self {
1135 let loadout = Self::empty();
1136 loadout
1137 .with_default_maintool(body)
1138 .with_default_equipment(body)
1139 }
1140
1141 pub fn from_loadout_spec(
1143 loadout_spec: LoadoutSpec,
1144 rng: &mut impl Rng,
1145 time: Option<&(TimeOfDay, Calendar)>,
1146 ) -> Result<Self, SpecError> {
1147 let loadout = Self::empty();
1148 loadout.with_loadout_spec(loadout_spec, rng, time)
1149 }
1150
1151 #[must_use]
1152 pub fn from_loadout_spec_expect(
1156 loadout_spec: LoadoutSpec,
1157 rng: &mut impl Rng,
1158 time: Option<&(TimeOfDay, Calendar)>,
1159 ) -> Self {
1160 Self::from_loadout_spec(loadout_spec, rng, time).expect("failed to load loadout spec")
1161 }
1162
1163 #[must_use = "Method consumes builder and returns updated builder."]
1164 pub fn with_default_maintool(self, body: &Body) -> Self {
1166 self.active_mainhand(default_main_tool(body).map(Item::new_from_asset_expect))
1167 }
1168
1169 #[must_use = "Method consumes builder and returns updated builder."]
1170 pub fn with_default_equipment(self, body: &Body) -> Self {
1172 let chest = default_chest(body);
1173
1174 if let Some(chest) = chest {
1175 self.chest(Some(Item::new_from_asset_expect(chest)))
1176 } else {
1177 self
1178 }
1179 }
1180
1181 #[must_use = "Method consumes builder and returns updated builder."]
1182 pub fn with_preset(mut self, preset: Preset) -> Self {
1183 let rng = &mut rand::thread_rng();
1184 match preset {
1185 Preset::HuskSummon => {
1186 self = self.with_asset_expect("common.loadout.dungeon.cultist.husk", rng, None);
1187 },
1188 Preset::BorealSummon => {
1189 self =
1190 self.with_asset_expect("common.loadout.world.boreal.boreal_warrior", rng, None);
1191 },
1192 Preset::IronDwarfSummon => {
1193 self = self.with_asset_expect(
1194 "common.loadout.dungeon.dwarven_quarry.iron_dwarf",
1195 rng,
1196 None,
1197 );
1198 },
1199 Preset::ShamanicSpiritSummon => {
1200 self = self.with_asset_expect(
1201 "common.loadout.dungeon.terracotta.shamanic_spirit",
1202 rng,
1203 None,
1204 );
1205 },
1206 Preset::JiangshiSummon => {
1207 self =
1208 self.with_asset_expect("common.loadout.dungeon.terracotta.jiangshi", rng, None);
1209 },
1210 Preset::BloodservantSummon => {
1211 self = self.with_asset_expect(
1212 "common.loadout.dungeon.vampire.bloodservant",
1213 rng,
1214 None,
1215 );
1216 },
1217 }
1218
1219 self
1220 }
1221
1222 #[must_use = "Method consumes builder and returns updated builder."]
1223 pub fn with_creator(
1224 mut self,
1225 creator: fn(
1226 LoadoutBuilder,
1227 Option<&SiteInformation>,
1228 time: Option<&(TimeOfDay, Calendar)>,
1229 ) -> LoadoutBuilder,
1230 economy: Option<&SiteInformation>,
1231 time: Option<&(TimeOfDay, Calendar)>,
1232 ) -> LoadoutBuilder {
1233 self = creator(self, economy, time);
1234
1235 self
1236 }
1237
1238 #[must_use = "Method consumes builder and returns updated builder."]
1239 fn with_loadout_spec<R: Rng>(
1240 mut self,
1241 spec: LoadoutSpec,
1242 rng: &mut R,
1243 time: Option<&(TimeOfDay, Calendar)>,
1244 ) -> Result<Self, SpecError> {
1245 let spec = spec.eval(rng)?;
1247
1248 let mut to_item = |maybe_item: Option<ItemSpec>| {
1250 if let Some(item) = maybe_item {
1251 item.try_to_item(rng, time)
1252 } else {
1253 Ok(None)
1254 }
1255 };
1256
1257 let to_pair = |maybe_hands: Option<Hands>, rng: &mut R| {
1258 if let Some(hands) = maybe_hands {
1259 hands.try_to_pair(rng, time)
1260 } else {
1261 Ok((None, None))
1262 }
1263 };
1264
1265 if let Some(item) = to_item(spec.head)? {
1267 self = self.head(Some(item));
1268 }
1269 if let Some(item) = to_item(spec.neck)? {
1270 self = self.neck(Some(item));
1271 }
1272 if let Some(item) = to_item(spec.shoulders)? {
1273 self = self.shoulder(Some(item));
1274 }
1275 if let Some(item) = to_item(spec.chest)? {
1276 self = self.chest(Some(item));
1277 }
1278 if let Some(item) = to_item(spec.gloves)? {
1279 self = self.hands(Some(item));
1280 }
1281 if let Some(item) = to_item(spec.ring1)? {
1282 self = self.ring1(Some(item));
1283 }
1284 if let Some(item) = to_item(spec.ring2)? {
1285 self = self.ring2(Some(item));
1286 }
1287 if let Some(item) = to_item(spec.back)? {
1288 self = self.back(Some(item));
1289 }
1290 if let Some(item) = to_item(spec.belt)? {
1291 self = self.belt(Some(item));
1292 }
1293 if let Some(item) = to_item(spec.legs)? {
1294 self = self.pants(Some(item));
1295 }
1296 if let Some(item) = to_item(spec.feet)? {
1297 self = self.feet(Some(item));
1298 }
1299 if let Some(item) = to_item(spec.tabard)? {
1300 self = self.tabard(Some(item));
1301 }
1302 if let Some(item) = to_item(spec.bag1)? {
1303 self = self.bag(ArmorSlot::Bag1, Some(item));
1304 }
1305 if let Some(item) = to_item(spec.bag2)? {
1306 self = self.bag(ArmorSlot::Bag2, Some(item));
1307 }
1308 if let Some(item) = to_item(spec.bag3)? {
1309 self = self.bag(ArmorSlot::Bag3, Some(item));
1310 }
1311 if let Some(item) = to_item(spec.bag4)? {
1312 self = self.bag(ArmorSlot::Bag4, Some(item));
1313 }
1314 if let Some(item) = to_item(spec.lantern)? {
1315 self = self.lantern(Some(item));
1316 }
1317 if let Some(item) = to_item(spec.glider)? {
1318 self = self.glider(Some(item));
1319 }
1320 let (active_mainhand, active_offhand) = to_pair(spec.active_hands, rng)?;
1321 if let Some(item) = active_mainhand {
1322 self = self.active_mainhand(Some(item));
1323 }
1324 if let Some(item) = active_offhand {
1325 self = self.active_offhand(Some(item));
1326 }
1327 let (inactive_mainhand, inactive_offhand) = to_pair(spec.inactive_hands, rng)?;
1328 if let Some(item) = inactive_mainhand {
1329 self = self.inactive_mainhand(Some(item));
1330 }
1331 if let Some(item) = inactive_offhand {
1332 self = self.inactive_offhand(Some(item));
1333 }
1334
1335 Ok(self)
1336 }
1337
1338 #[must_use = "Method consumes builder and returns updated builder."]
1339 pub fn with_asset(
1340 self,
1341 asset_specifier: &str,
1342 rng: &mut impl Rng,
1343 time: Option<&(TimeOfDay, Calendar)>,
1344 ) -> Result<Self, SpecError> {
1345 let spec =
1346 LoadoutSpec::load_cloned(asset_specifier).map_err(SpecError::LoadoutAssetError)?;
1347 self.with_loadout_spec(spec, rng, time)
1348 }
1349
1350 #[must_use = "Method consumes builder and returns updated builder."]
1358 pub fn with_asset_expect(
1359 self,
1360 asset_specifier: &str,
1361 rng: &mut impl Rng,
1362 time: Option<&(TimeOfDay, Calendar)>,
1363 ) -> Self {
1364 self.with_asset(asset_specifier, rng, time)
1365 .expect("failed loading loadout config")
1366 }
1367
1368 #[must_use = "Method consumes builder and returns updated builder."]
1371 pub fn defaults(self) -> Self {
1372 let rng = &mut rand::thread_rng();
1373 self.with_asset_expect("common.loadout.default", rng, None)
1374 }
1375
1376 #[must_use = "Method consumes builder and returns updated builder."]
1377 fn with_equipment(mut self, equip_slot: EquipSlot, item: Option<Item>) -> Self {
1378 assert!(
1380 item.as_ref()
1381 .is_none_or(|item| equip_slot.can_hold(&item.kind()))
1382 );
1383
1384 let time = Time(0.0);
1389
1390 self.0.swap(equip_slot, item, time);
1391 self
1392 }
1393
1394 #[must_use = "Method consumes builder and returns updated builder."]
1395 fn with_armor(self, armor_slot: ArmorSlot, item: Option<Item>) -> Self {
1396 self.with_equipment(EquipSlot::Armor(armor_slot), item)
1397 }
1398
1399 #[must_use = "Method consumes builder and returns updated builder."]
1400 pub fn active_mainhand(self, item: Option<Item>) -> Self {
1401 self.with_equipment(EquipSlot::ActiveMainhand, item)
1402 }
1403
1404 #[must_use = "Method consumes builder and returns updated builder."]
1405 pub fn active_offhand(self, item: Option<Item>) -> Self {
1406 self.with_equipment(EquipSlot::ActiveOffhand, item)
1407 }
1408
1409 #[must_use = "Method consumes builder and returns updated builder."]
1410 pub fn inactive_mainhand(self, item: Option<Item>) -> Self {
1411 self.with_equipment(EquipSlot::InactiveMainhand, item)
1412 }
1413
1414 #[must_use = "Method consumes builder and returns updated builder."]
1415 pub fn inactive_offhand(self, item: Option<Item>) -> Self {
1416 self.with_equipment(EquipSlot::InactiveOffhand, item)
1417 }
1418
1419 #[must_use = "Method consumes builder and returns updated builder."]
1420 pub fn shoulder(self, item: Option<Item>) -> Self {
1421 self.with_armor(ArmorSlot::Shoulders, item)
1422 }
1423
1424 #[must_use = "Method consumes builder and returns updated builder."]
1425 pub fn chest(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Chest, item) }
1426
1427 #[must_use = "Method consumes builder and returns updated builder."]
1428 pub fn belt(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Belt, item) }
1429
1430 #[must_use = "Method consumes builder and returns updated builder."]
1431 pub fn hands(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Hands, item) }
1432
1433 #[must_use = "Method consumes builder and returns updated builder."]
1434 pub fn pants(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Legs, item) }
1435
1436 #[must_use = "Method consumes builder and returns updated builder."]
1437 pub fn feet(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Feet, item) }
1438
1439 #[must_use = "Method consumes builder and returns updated builder."]
1440 pub fn back(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Back, item) }
1441
1442 #[must_use = "Method consumes builder and returns updated builder."]
1443 pub fn ring1(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Ring1, item) }
1444
1445 #[must_use = "Method consumes builder and returns updated builder."]
1446 pub fn ring2(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Ring2, item) }
1447
1448 #[must_use = "Method consumes builder and returns updated builder."]
1449 pub fn neck(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Neck, item) }
1450
1451 #[must_use = "Method consumes builder and returns updated builder."]
1452 pub fn lantern(self, item: Option<Item>) -> Self {
1453 self.with_equipment(EquipSlot::Lantern, item)
1454 }
1455
1456 #[must_use = "Method consumes builder and returns updated builder."]
1457 pub fn glider(self, item: Option<Item>) -> Self { self.with_equipment(EquipSlot::Glider, item) }
1458
1459 #[must_use = "Method consumes builder and returns updated builder."]
1460 pub fn head(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Head, item) }
1461
1462 #[must_use = "Method consumes builder and returns updated builder."]
1463 pub fn tabard(self, item: Option<Item>) -> Self { self.with_armor(ArmorSlot::Tabard, item) }
1464
1465 #[must_use = "Method consumes builder and returns updated builder."]
1466 pub fn bag(self, which: ArmorSlot, item: Option<Item>) -> Self { self.with_armor(which, item) }
1467
1468 #[must_use]
1469 pub fn build(self) -> Loadout { self.0 }
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474 use super::*;
1475 use crate::comp::{self, Body};
1476 use rand::thread_rng;
1477 use strum::IntoEnumIterator;
1478
1479 #[test]
1484 fn test_loadout_species() {
1485 macro_rules! test_species {
1486 ($species:tt : $body:tt) => {
1488 let mut rng = thread_rng();
1489 for s in comp::$species::ALL_SPECIES.iter() {
1490 let body = comp::$species::Body::random_with(&mut rng, s);
1491 let female_body = comp::$species::Body {
1492 body_type: comp::$species::BodyType::Female,
1493 ..body
1494 };
1495 let male_body = comp::$species::Body {
1496 body_type: comp::$species::BodyType::Male,
1497 ..body
1498 };
1499 std::mem::drop(LoadoutBuilder::from_default(
1500 &Body::$body(female_body),
1501 ));
1502 std::mem::drop(LoadoutBuilder::from_default(
1503 &Body::$body(male_body),
1504 ));
1505 }
1506 };
1507 ($base:tt : $body:tt, $($species:tt : $nextbody:tt),+ $(,)?) => {
1509 test_species!($base: $body);
1510 test_species!($($species: $nextbody),+);
1511 }
1512 }
1513
1514 test_species!(
1516 humanoid: Humanoid,
1517 quadruped_small: QuadrupedSmall,
1518 quadruped_medium: QuadrupedMedium,
1519 quadruped_low: QuadrupedLow,
1520 quadruped_small: QuadrupedSmall,
1521 bird_medium: BirdMedium,
1522 bird_large: BirdLarge,
1523 fish_small: FishSmall,
1524 fish_medium: FishMedium,
1525 biped_small: BipedSmall,
1526 biped_large: BipedLarge,
1527 theropod: Theropod,
1528 dragon: Dragon,
1529 golem: Golem,
1530 arthropod: Arthropod,
1531 );
1532 }
1533
1534 #[test]
1538 fn test_loadout_presets() {
1539 for preset in Preset::iter() {
1540 drop(LoadoutBuilder::empty().with_preset(preset));
1541 }
1542 }
1543
1544 #[test]
1551 fn validate_all_loadout_assets() {
1552 let loadouts = assets::load_rec_dir::<LoadoutSpec>("common.loadout")
1553 .expect("failed to load loadout directory");
1554 for loadout_id in loadouts.read().ids() {
1555 let loadout =
1556 LoadoutSpec::load_cloned(loadout_id).expect("failed to load loadout asset");
1557 loadout
1558 .validate(vec![loadout_id.to_string()])
1559 .unwrap_or_else(|e| panic!("{loadout_id} is broken: {e:?}"));
1560 }
1561 }
1562
1563 #[test]
1565 fn test_valid_assets() {
1566 let loadouts = assets::load_rec_dir::<LoadoutSpec>("test.loadout.ok")
1567 .expect("failed to load loadout directory");
1568
1569 for loadout_id in loadouts.read().ids() {
1570 let loadout =
1571 LoadoutSpec::load_cloned(loadout_id).expect("failed to load loadout asset");
1572 loadout
1573 .validate(vec![loadout_id.to_string()])
1574 .unwrap_or_else(|e| panic!("{loadout_id} is broken: {e:?}"));
1575 }
1576 }
1577}