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