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