1use rakata_extract::{tables, TwoDaCache};
13use rakata_formats::twoda::TwoDa;
14
15use crate::utc::{Utc, UtcClass, UtcEquipmentItem, UtcInventoryItem, UtcSpecialAbility};
16
17#[allow(clippy::enum_variant_names)]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum DecodedFeat {
38 ArmourProfLight {
40 feat_id: u16,
42 },
43 ArmourProfMedium {
45 feat_id: u16,
47 },
48 ArmourProfHeavy {
50 feat_id: u16,
52 },
53 WeaponProfBlaster {
55 feat_id: u16,
57 },
58 WeaponProfBlasterRifle {
60 feat_id: u16,
62 },
63 WeaponProfHeavyWeapons {
65 feat_id: u16,
67 },
68 WeaponProfMeleeWeapons {
70 feat_id: u16,
72 },
73 WeaponProfLightsaber {
75 feat_id: u16,
77 },
78 WeaponProfGrenade {
82 feat_id: u16,
84 },
85 WeaponProfSimpleWeapons {
88 feat_id: u16,
90 },
91 ProficiencyAll {
95 feat_id: u16,
97 },
98 WeaponFocusBlaster {
100 feat_id: u16,
102 },
103 WeaponFocusBlasterRifle {
105 feat_id: u16,
107 },
108 WeaponFocusGrenade {
110 feat_id: u16,
112 },
113 WeaponFocusHeavyWeapons {
115 feat_id: u16,
117 },
118 WeaponFocusLightsaber {
120 feat_id: u16,
122 },
123 WeaponFocusMeleeWeapons {
125 feat_id: u16,
127 },
128 WeaponFocusSimpleWeapons {
131 feat_id: u16,
133 },
134 WeaponSpecBlaster {
136 feat_id: u16,
138 },
139 WeaponSpecBlasterRifle {
141 feat_id: u16,
143 },
144 WeaponSpecGrenade {
146 feat_id: u16,
148 },
149 WeaponSpecHeavyWeapons {
151 feat_id: u16,
153 },
154 WeaponSpecLightsaber {
156 feat_id: u16,
158 },
159 WeaponSpecMeleeWeapons {
161 feat_id: u16,
163 },
164 WeaponSpecSimpleWeapons {
167 feat_id: u16,
169 },
170 PowerAttack {
172 feat_id: u16,
174 },
175 ImprovedPowerAttack {
177 feat_id: u16,
179 },
180 MasterPowerAttack {
182 feat_id: u16,
184 },
185 PowerBlast {
187 feat_id: u16,
189 },
190 ImprovedPowerBlast {
192 feat_id: u16,
194 },
195 MasterPowerBlast {
197 feat_id: u16,
199 },
200 CriticalStrike {
202 feat_id: u16,
204 },
205 ImprovedCriticalStrike {
207 feat_id: u16,
209 },
210 MasterCriticalStrike {
212 feat_id: u16,
214 },
215 Flurry {
217 feat_id: u16,
219 },
220 ImprovedFlurry {
222 feat_id: u16,
224 },
225 SniperShot {
227 feat_id: u16,
229 },
230 ImprovedSniperShot {
232 feat_id: u16,
234 },
235 MasterSniperShot {
237 feat_id: u16,
239 },
240 RapidShot {
242 feat_id: u16,
244 },
245 ImprovedRapidShot {
247 feat_id: u16,
249 },
250 MultiShot {
252 feat_id: u16,
254 },
255 WhirlwindAttack {
257 feat_id: u16,
259 },
260 TwoWeaponFighting {
262 feat_id: u16,
264 },
265 TwoWeaponAdvanced {
267 feat_id: u16,
269 },
270 TwoWeaponMastery {
272 feat_id: u16,
274 },
275 JediDefense {
277 feat_id: u16,
279 },
280 AdvancedJediDefense {
282 feat_id: u16,
284 },
285 MasterJediDefense {
287 feat_id: u16,
289 },
290 ForceFocus {
293 feat_id: u16,
295 },
296 ForceFocusAdvanced {
298 feat_id: u16,
300 },
301 ForceFocusMastery {
303 feat_id: u16,
305 },
306 ForceFocusAlter {
309 feat_id: u16,
311 },
312 ForceFocusControl {
315 feat_id: u16,
317 },
318 ForceJump {
320 feat_id: u16,
322 },
323 ForceJumpAdvanced {
325 feat_id: u16,
327 },
328 ForceJumpMastery {
330 feat_id: u16,
332 },
333 ForceImmunityFear {
335 feat_id: u16,
337 },
338 ForceImmunityStun {
340 feat_id: u16,
342 },
343 ForceImmunityParalysis {
345 feat_id: u16,
347 },
348 BattleMeditation {
351 feat_id: u16,
353 },
354 ForceCamoflage {
357 feat_id: u16,
359 },
360 SneakAttack1D6 {
362 feat_id: u16,
364 },
365 SneakAttack2D6 {
367 feat_id: u16,
369 },
370 SneakAttack3D6 {
372 feat_id: u16,
374 },
375 SneakAttack4D6 {
377 feat_id: u16,
379 },
380 SneakAttack5D6 {
382 feat_id: u16,
384 },
385 SneakAttack6D6 {
387 feat_id: u16,
389 },
390 UncannyDodge1 {
392 feat_id: u16,
394 },
395 UncannyDodge2 {
397 feat_id: u16,
399 },
400 Toughness {
402 feat_id: u16,
404 },
405 ImprovedToughness {
407 feat_id: u16,
409 },
410 MasterToughness {
412 feat_id: u16,
414 },
415 Conditioning {
417 feat_id: u16,
419 },
420 ImprovedConditioning {
422 feat_id: u16,
424 },
425 MasterConditioning {
427 feat_id: u16,
429 },
430 ImplantLevel1 {
432 feat_id: u16,
434 },
435 ImplantLevel2 {
437 feat_id: u16,
439 },
440 ImplantLevel3 {
442 feat_id: u16,
444 },
445 DroidUpgrade1 {
447 feat_id: u16,
449 },
450 DroidUpgrade2 {
452 feat_id: u16,
454 },
455 DroidUpgrade3 {
457 feat_id: u16,
459 },
460 BlasterIntegration {
463 feat_id: u16,
465 },
466 LogicUpgradeCombat {
468 feat_id: u16,
470 },
471 LogicUpgradeTactician {
473 feat_id: u16,
475 },
476 Dueling {
478 feat_id: u16,
480 },
481 AdvancedDueling {
483 feat_id: u16,
485 },
486 MasterDueling {
488 feat_id: u16,
490 },
491 JediSense {
494 feat_id: u16,
496 },
497 KnightSense {
499 feat_id: u16,
501 },
502 MasterSense {
504 feat_id: u16,
506 },
507 GuardStance {
510 feat_id: u16,
512 },
513 AdvancedGuardStance {
515 feat_id: u16,
517 },
518 MasterGuardStance {
520 feat_id: u16,
522 },
523 SkillFocusAwareness {
527 feat_id: u16,
529 },
530 SkillFocusTreatInjury {
533 feat_id: u16,
535 },
536 SkillFocusStealth {
538 feat_id: u16,
540 },
541 Cautious {
544 feat_id: u16,
546 },
547 Perceptive {
550 feat_id: u16,
552 },
553 Empathy {
555 feat_id: u16,
557 },
558 GearHead {
560 feat_id: u16,
562 },
563 WookieEndurance {
565 feat_id: u16,
567 },
568 ScoundrelsLuck {
570 feat_id: u16,
572 },
573 Unknown {
578 feat_id: u16,
580 feat_label: Option<String>,
584 },
585}
586
587impl DecodedFeat {
588 pub fn feat_id(&self) -> u16 {
590 match self {
591 Self::ArmourProfLight { feat_id }
592 | Self::ArmourProfMedium { feat_id }
593 | Self::ArmourProfHeavy { feat_id }
594 | Self::WeaponProfBlaster { feat_id }
595 | Self::WeaponProfBlasterRifle { feat_id }
596 | Self::WeaponProfHeavyWeapons { feat_id }
597 | Self::WeaponProfMeleeWeapons { feat_id }
598 | Self::WeaponProfLightsaber { feat_id }
599 | Self::WeaponProfGrenade { feat_id }
600 | Self::WeaponProfSimpleWeapons { feat_id }
601 | Self::ProficiencyAll { feat_id }
602 | Self::WeaponFocusBlaster { feat_id }
603 | Self::WeaponFocusBlasterRifle { feat_id }
604 | Self::WeaponFocusGrenade { feat_id }
605 | Self::WeaponFocusHeavyWeapons { feat_id }
606 | Self::WeaponFocusLightsaber { feat_id }
607 | Self::WeaponFocusMeleeWeapons { feat_id }
608 | Self::WeaponFocusSimpleWeapons { feat_id }
609 | Self::WeaponSpecBlaster { feat_id }
610 | Self::WeaponSpecBlasterRifle { feat_id }
611 | Self::WeaponSpecGrenade { feat_id }
612 | Self::WeaponSpecHeavyWeapons { feat_id }
613 | Self::WeaponSpecLightsaber { feat_id }
614 | Self::WeaponSpecMeleeWeapons { feat_id }
615 | Self::WeaponSpecSimpleWeapons { feat_id }
616 | Self::PowerAttack { feat_id }
617 | Self::ImprovedPowerAttack { feat_id }
618 | Self::MasterPowerAttack { feat_id }
619 | Self::PowerBlast { feat_id }
620 | Self::ImprovedPowerBlast { feat_id }
621 | Self::MasterPowerBlast { feat_id }
622 | Self::CriticalStrike { feat_id }
623 | Self::ImprovedCriticalStrike { feat_id }
624 | Self::MasterCriticalStrike { feat_id }
625 | Self::Flurry { feat_id }
626 | Self::ImprovedFlurry { feat_id }
627 | Self::SniperShot { feat_id }
628 | Self::ImprovedSniperShot { feat_id }
629 | Self::MasterSniperShot { feat_id }
630 | Self::RapidShot { feat_id }
631 | Self::ImprovedRapidShot { feat_id }
632 | Self::MultiShot { feat_id }
633 | Self::WhirlwindAttack { feat_id }
634 | Self::TwoWeaponFighting { feat_id }
635 | Self::TwoWeaponAdvanced { feat_id }
636 | Self::TwoWeaponMastery { feat_id }
637 | Self::JediDefense { feat_id }
638 | Self::AdvancedJediDefense { feat_id }
639 | Self::MasterJediDefense { feat_id }
640 | Self::ForceFocus { feat_id }
641 | Self::ForceFocusAdvanced { feat_id }
642 | Self::ForceFocusMastery { feat_id }
643 | Self::ForceFocusAlter { feat_id }
644 | Self::ForceFocusControl { feat_id }
645 | Self::ForceJump { feat_id }
646 | Self::ForceJumpAdvanced { feat_id }
647 | Self::ForceJumpMastery { feat_id }
648 | Self::ForceImmunityFear { feat_id }
649 | Self::ForceImmunityStun { feat_id }
650 | Self::ForceImmunityParalysis { feat_id }
651 | Self::BattleMeditation { feat_id }
652 | Self::ForceCamoflage { feat_id }
653 | Self::SneakAttack1D6 { feat_id }
654 | Self::SneakAttack2D6 { feat_id }
655 | Self::SneakAttack3D6 { feat_id }
656 | Self::SneakAttack4D6 { feat_id }
657 | Self::SneakAttack5D6 { feat_id }
658 | Self::SneakAttack6D6 { feat_id }
659 | Self::UncannyDodge1 { feat_id }
660 | Self::UncannyDodge2 { feat_id }
661 | Self::Toughness { feat_id }
662 | Self::ImprovedToughness { feat_id }
663 | Self::MasterToughness { feat_id }
664 | Self::Conditioning { feat_id }
665 | Self::ImprovedConditioning { feat_id }
666 | Self::MasterConditioning { feat_id }
667 | Self::ImplantLevel1 { feat_id }
668 | Self::ImplantLevel2 { feat_id }
669 | Self::ImplantLevel3 { feat_id }
670 | Self::DroidUpgrade1 { feat_id }
671 | Self::DroidUpgrade2 { feat_id }
672 | Self::DroidUpgrade3 { feat_id }
673 | Self::BlasterIntegration { feat_id }
674 | Self::LogicUpgradeCombat { feat_id }
675 | Self::LogicUpgradeTactician { feat_id }
676 | Self::Dueling { feat_id }
677 | Self::AdvancedDueling { feat_id }
678 | Self::MasterDueling { feat_id }
679 | Self::JediSense { feat_id }
680 | Self::KnightSense { feat_id }
681 | Self::MasterSense { feat_id }
682 | Self::GuardStance { feat_id }
683 | Self::AdvancedGuardStance { feat_id }
684 | Self::MasterGuardStance { feat_id }
685 | Self::SkillFocusAwareness { feat_id }
686 | Self::SkillFocusTreatInjury { feat_id }
687 | Self::SkillFocusStealth { feat_id }
688 | Self::Cautious { feat_id }
689 | Self::Perceptive { feat_id }
690 | Self::Empathy { feat_id }
691 | Self::GearHead { feat_id }
692 | Self::WookieEndurance { feat_id }
693 | Self::ScoundrelsLuck { feat_id }
694 | Self::Unknown { feat_id, .. } => *feat_id,
695 }
696 }
697}
698
699fn dispatch_feat(feat_id: u16, feat_table: Option<&TwoDa>) -> DecodedFeat {
709 let label = feat_table.and_then(|table| {
710 let row = usize::from(feat_id);
711 if row >= table.rows.len() {
712 return None;
713 }
714 table.cell(row, "label").map(str::to_string)
715 });
716 match label.as_deref() {
717 Some("ARMOUR_PROF_LIGHT") => DecodedFeat::ArmourProfLight { feat_id },
719 Some("ARMOUR_PROF_MEDIUM") => DecodedFeat::ArmourProfMedium { feat_id },
720 Some("ARMOUR_PROF_HEAVY") => DecodedFeat::ArmourProfHeavy { feat_id },
721 Some("WEAPON_PROF_BLASTER") => DecodedFeat::WeaponProfBlaster { feat_id },
724 Some("WEAPON_PROF_BLASTER_RIFLE") => DecodedFeat::WeaponProfBlasterRifle { feat_id },
725 Some("WEAPON_PROF_HEAVY_WEAPONS") => DecodedFeat::WeaponProfHeavyWeapons { feat_id },
726 Some("WEAPON_PROF_MELEE_WEAPONS") => DecodedFeat::WeaponProfMeleeWeapons { feat_id },
727 Some("WEAPON_PROF_LIGHTSABER") => DecodedFeat::WeaponProfLightsaber { feat_id },
728 Some("XXXX_WEAPON_PROF_GRENADE") => DecodedFeat::WeaponProfGrenade { feat_id },
729 Some("XXXX_WEAPON_PROF_SIMPLE_WEAPONS") => DecodedFeat::WeaponProfSimpleWeapons { feat_id },
730 Some("PROFICIENCY_ALL") => DecodedFeat::ProficiencyAll { feat_id },
732 Some("WEAPON_FOCUS_BLASTER") => DecodedFeat::WeaponFocusBlaster { feat_id },
734 Some("WEAPON_FOCUS_BLASTER_RIFLE") => DecodedFeat::WeaponFocusBlasterRifle { feat_id },
735 Some("XXXX_WEAPON_FOCUS_GRENADE") => DecodedFeat::WeaponFocusGrenade { feat_id },
736 Some("WEAPON_FOCUS_HEAVY_WEAPONS") => DecodedFeat::WeaponFocusHeavyWeapons { feat_id },
737 Some("WEAPON_FOCUS_LIGHTSABER") => DecodedFeat::WeaponFocusLightsaber { feat_id },
738 Some("WEAPON_FOCUS_MELEE_WEAPONS") => DecodedFeat::WeaponFocusMeleeWeapons { feat_id },
739 Some("XXXX_WEAPON_FOCUS_SIMPLE_WEAPONS") => {
740 DecodedFeat::WeaponFocusSimpleWeapons { feat_id }
741 }
742 Some("WEAPON_SPEC_BLASTER") => DecodedFeat::WeaponSpecBlaster { feat_id },
744 Some("WEAPON_SPEC_BLASTER_RIFLE") => DecodedFeat::WeaponSpecBlasterRifle { feat_id },
745 Some("XXXX_WEAPON_SPEC_GRENADE") => DecodedFeat::WeaponSpecGrenade { feat_id },
746 Some("WEAPON_SPEC_HEAVY_WEAPONS") => DecodedFeat::WeaponSpecHeavyWeapons { feat_id },
747 Some("WEAPON_SPEC_LIGHTSABER") => DecodedFeat::WeaponSpecLightsaber { feat_id },
748 Some("WEAPON_SPEC_MELEE_WEAPONS") => DecodedFeat::WeaponSpecMeleeWeapons { feat_id },
749 Some("XXXX_WEAPON_SPEC_SIMPLE_WEAPONS") => DecodedFeat::WeaponSpecSimpleWeapons { feat_id },
750 Some("POWER_ATTACK") => DecodedFeat::PowerAttack { feat_id },
752 Some("IMPROVED_POWER_ATTACK") => DecodedFeat::ImprovedPowerAttack { feat_id },
753 Some("MASTER_POWER_ATTACK") => DecodedFeat::MasterPowerAttack { feat_id },
754 Some("POWER_BLAST") => DecodedFeat::PowerBlast { feat_id },
756 Some("IMPROVED_POWER_BLAST") => DecodedFeat::ImprovedPowerBlast { feat_id },
757 Some("MASTER_POWER_BLAST") => DecodedFeat::MasterPowerBlast { feat_id },
758 Some("CRITICAL_STRIKE") => DecodedFeat::CriticalStrike { feat_id },
760 Some("IMPROVED_CRITICAL_STRIKE") => DecodedFeat::ImprovedCriticalStrike { feat_id },
761 Some("MASTER_CRITICAL_STRIKE") => DecodedFeat::MasterCriticalStrike { feat_id },
762 Some("FLURRY") => DecodedFeat::Flurry { feat_id },
764 Some("IMPROVED_FLURRY") => DecodedFeat::ImprovedFlurry { feat_id },
765 Some("SNIPER_SHOT") => DecodedFeat::SniperShot { feat_id },
767 Some("IMPROVED_SNIPER_SHOT") => DecodedFeat::ImprovedSniperShot { feat_id },
768 Some("MASTER_SNIPER_SHOT") => DecodedFeat::MasterSniperShot { feat_id },
769 Some("RAPID_SHOT") => DecodedFeat::RapidShot { feat_id },
771 Some("IMPROVED_RAPID_SHOT") => DecodedFeat::ImprovedRapidShot { feat_id },
772 Some("MULTI_SHOT") => DecodedFeat::MultiShot { feat_id },
774 Some("WHIRLWIND_ATTACK") => DecodedFeat::WhirlwindAttack { feat_id },
775 Some("TWO_WEAPON_FIGHTING") => DecodedFeat::TwoWeaponFighting { feat_id },
777 Some("TWO_WEAPON_ADVANCED") => DecodedFeat::TwoWeaponAdvanced { feat_id },
778 Some("TWO_WEAPON_MASTERY") => DecodedFeat::TwoWeaponMastery { feat_id },
779 Some("JEDI_DEFENSE") => DecodedFeat::JediDefense { feat_id },
781 Some("ADVANCED_JEDI_DEFENSE") => DecodedFeat::AdvancedJediDefense { feat_id },
782 Some("MASTER_JEDI_DEFENSE") => DecodedFeat::MasterJediDefense { feat_id },
783 Some("FORCE_FOCUS") => DecodedFeat::ForceFocus { feat_id },
785 Some("FORCE_FOCUS_ADVANCED") => DecodedFeat::ForceFocusAdvanced { feat_id },
786 Some("FORCE_FOCUS_MASTERY") => DecodedFeat::ForceFocusMastery { feat_id },
787 Some("XXXX_FORCE_FOCUS_ALTER") => DecodedFeat::ForceFocusAlter { feat_id },
788 Some("XXXX_FORCE_FOCUS_CONTROL") => DecodedFeat::ForceFocusControl { feat_id },
789 Some("FORCE_JUMP") => DecodedFeat::ForceJump { feat_id },
791 Some("FORCE_JUMP_ADVANCED") => DecodedFeat::ForceJumpAdvanced { feat_id },
792 Some("FORCE_JUMP_MASTERY") => DecodedFeat::ForceJumpMastery { feat_id },
793 Some("FORCE_IMMUNITY_FEAR") => DecodedFeat::ForceImmunityFear { feat_id },
795 Some("FORCE_IMMUNITY_STUN") => DecodedFeat::ForceImmunityStun { feat_id },
796 Some("FORCE_IMMUNITY_PARALYSIS") => DecodedFeat::ForceImmunityParalysis { feat_id },
797 Some("BATTLE_MEDITATION") => DecodedFeat::BattleMeditation { feat_id },
799 Some("FORCE_CAMOFLAGE") => DecodedFeat::ForceCamoflage { feat_id },
801 Some("SNEAK_ATTACK_1D6") => DecodedFeat::SneakAttack1D6 { feat_id },
805 Some("SNEAK_ATTACK_2D6") => DecodedFeat::SneakAttack2D6 { feat_id },
806 Some("SNEAK_ATTACK_3D6") => DecodedFeat::SneakAttack3D6 { feat_id },
807 Some("SNEAK_ATTACK_4D6") => DecodedFeat::SneakAttack4D6 { feat_id },
808 Some("SNEAK_ATTACK_5D6") => DecodedFeat::SneakAttack5D6 { feat_id },
809 Some("SNEAK_ATTACK_6D6") => DecodedFeat::SneakAttack6D6 { feat_id },
810 Some("UNCANNY_DODGE_1") => DecodedFeat::UncannyDodge1 { feat_id },
812 Some("UNCANNY_DODGE_2") => DecodedFeat::UncannyDodge2 { feat_id },
813 Some("TOUGHNESS") => DecodedFeat::Toughness { feat_id },
815 Some("IMPROVED_TOUGHNESS") => DecodedFeat::ImprovedToughness { feat_id },
816 Some("MASTER_TOUGHNESS") => DecodedFeat::MasterToughness { feat_id },
817 Some("CONDITIONING") => DecodedFeat::Conditioning { feat_id },
819 Some("IMPROVED_CONDITIONING") => DecodedFeat::ImprovedConditioning { feat_id },
820 Some("MASTER_CONDITIONING") => DecodedFeat::MasterConditioning { feat_id },
821 Some("IMPLANT_LEVEL_1") => DecodedFeat::ImplantLevel1 { feat_id },
823 Some("IMPLANT_LEVEL_2") => DecodedFeat::ImplantLevel2 { feat_id },
824 Some("IMPLANT_LEVEL_3") => DecodedFeat::ImplantLevel3 { feat_id },
825 Some("DROID_UPGRADE_1") => DecodedFeat::DroidUpgrade1 { feat_id },
827 Some("DROID_UPGRADE_2") => DecodedFeat::DroidUpgrade2 { feat_id },
828 Some("DROID_UPGRADE_3") => DecodedFeat::DroidUpgrade3 { feat_id },
829 Some("BLASTER_INTEGRATION") => DecodedFeat::BlasterIntegration { feat_id },
830 Some("LOGIC_UPGRADE_COMBAT") => DecodedFeat::LogicUpgradeCombat { feat_id },
831 Some("LOGIC_UPGRADE_TACTICIAN") => DecodedFeat::LogicUpgradeTactician { feat_id },
832 Some("DUELING") => DecodedFeat::Dueling { feat_id },
834 Some("ADVANCED_DUELING") => DecodedFeat::AdvancedDueling { feat_id },
835 Some("MASTER_DUELING") => DecodedFeat::MasterDueling { feat_id },
836 Some("JEDI_SENSE") => DecodedFeat::JediSense { feat_id },
838 Some("KNIGHT_SENSE") => DecodedFeat::KnightSense { feat_id },
839 Some("MASTER_SENSE") => DecodedFeat::MasterSense { feat_id },
840 Some("XXXX_GUARD_STANCE") => DecodedFeat::GuardStance { feat_id },
842 Some("XXXX_ADVANCED_GUARD_STANCE") => DecodedFeat::AdvancedGuardStance { feat_id },
843 Some("XXXX_MASTER_GUARD_STANCE") => DecodedFeat::MasterGuardStance { feat_id },
844 Some("XXXX_SKILL_FOCUS_AWARENESS") => DecodedFeat::SkillFocusAwareness { feat_id },
846 Some("XXXX_SKILL_FOCUS_TREAT_INJURY") => DecodedFeat::SkillFocusTreatInjury { feat_id },
847 Some("XXXX_SKILL_FOCUS_STEALTH") => DecodedFeat::SkillFocusStealth { feat_id },
848 Some("CAUTIOUS") => DecodedFeat::Cautious { feat_id },
851 Some("XXXX_PERCEPTIVE") => DecodedFeat::Perceptive { feat_id },
852 Some("EMPATHY") => DecodedFeat::Empathy { feat_id },
853 Some("GEAR_HEAD") => DecodedFeat::GearHead { feat_id },
854 Some("WOOKIE_ENDURANCE") => DecodedFeat::WookieEndurance { feat_id },
855 Some("SCOUNDRELS_LUCK") => DecodedFeat::ScoundrelsLuck { feat_id },
856 _ => DecodedFeat::Unknown {
860 feat_id,
861 feat_label: label,
862 },
863 }
864}
865
866#[derive(Debug, Clone)]
877pub struct UtcProjection<'a> {
878 utc: &'a Utc,
879}
880
881#[derive(Debug)]
893pub struct UtcSnapshot<'a> {
894 utc: &'a Utc,
895 race_info: Option<RaceInfo>,
896 appearance_info: Option<AppearanceInfo>,
897 portrait_info: Option<PortraitInfo>,
898 soundset_info: Option<SoundsetInfo>,
899 decoded_classes: Vec<DecodedClass>,
900 decoded_special_abilities: Vec<DecodedSpecialAbility>,
901 decoded_feats: Vec<DecodedFeat>,
902}
903
904#[derive(Debug, Clone, PartialEq, Eq)]
923pub enum DecodedClass {
924 Soldier {
926 class_id: i32,
928 level: i16,
931 powers: Vec<u16>,
933 },
934 Scout {
936 class_id: i32,
938 level: i16,
940 powers: Vec<u16>,
942 },
943 Scoundrel {
945 class_id: i32,
947 level: i16,
949 powers: Vec<u16>,
951 },
952 JediGuardian {
954 class_id: i32,
956 level: i16,
958 powers: Vec<u16>,
960 },
961 JediConsular {
963 class_id: i32,
965 level: i16,
967 powers: Vec<u16>,
969 },
970 JediSentinel {
972 class_id: i32,
974 level: i16,
976 powers: Vec<u16>,
978 },
979 CombatDroid {
981 class_id: i32,
983 level: i16,
985 powers: Vec<u16>,
987 },
988 ExpertDroid {
990 class_id: i32,
992 level: i16,
994 powers: Vec<u16>,
996 },
997 Minion {
999 class_id: i32,
1001 level: i16,
1003 powers: Vec<u16>,
1005 },
1006 Unknown {
1011 class_id: i32,
1013 class_label: Option<String>,
1017 level: i16,
1019 powers: Vec<u16>,
1021 },
1022}
1023
1024impl DecodedClass {
1025 pub fn class_id(&self) -> i32 {
1028 match self {
1029 Self::Soldier { class_id, .. }
1030 | Self::Scout { class_id, .. }
1031 | Self::Scoundrel { class_id, .. }
1032 | Self::JediGuardian { class_id, .. }
1033 | Self::JediConsular { class_id, .. }
1034 | Self::JediSentinel { class_id, .. }
1035 | Self::CombatDroid { class_id, .. }
1036 | Self::ExpertDroid { class_id, .. }
1037 | Self::Minion { class_id, .. }
1038 | Self::Unknown { class_id, .. } => *class_id,
1039 }
1040 }
1041
1042 pub fn level(&self) -> i16 {
1044 match self {
1045 Self::Soldier { level, .. }
1046 | Self::Scout { level, .. }
1047 | Self::Scoundrel { level, .. }
1048 | Self::JediGuardian { level, .. }
1049 | Self::JediConsular { level, .. }
1050 | Self::JediSentinel { level, .. }
1051 | Self::CombatDroid { level, .. }
1052 | Self::ExpertDroid { level, .. }
1053 | Self::Minion { level, .. }
1054 | Self::Unknown { level, .. } => *level,
1055 }
1056 }
1057
1058 pub fn powers(&self) -> &[u16] {
1061 match self {
1062 Self::Soldier { powers, .. }
1063 | Self::Scout { powers, .. }
1064 | Self::Scoundrel { powers, .. }
1065 | Self::JediGuardian { powers, .. }
1066 | Self::JediConsular { powers, .. }
1067 | Self::JediSentinel { powers, .. }
1068 | Self::CombatDroid { powers, .. }
1069 | Self::ExpertDroid { powers, .. }
1070 | Self::Minion { powers, .. }
1071 | Self::Unknown { powers, .. } => powers,
1072 }
1073 }
1074}
1075
1076#[derive(Debug, Clone, Default)]
1080struct RaceInfo {
1081 label: Option<String>,
1082}
1083
1084#[derive(Debug, Clone, Default)]
1086struct AppearanceInfo {
1087 label: Option<String>,
1088}
1089
1090#[derive(Debug, Clone, Default)]
1092struct PortraitInfo {
1093 label: Option<String>,
1094}
1095
1096#[derive(Debug, Clone, Default)]
1098struct SoundsetInfo {
1099 label: Option<String>,
1100}
1101
1102impl RaceInfo {
1103 fn from_row(table: &TwoDa, race_id: u8) -> Option<Self> {
1104 let row = usize::from(race_id);
1105 if row >= table.rows.len() {
1106 return None;
1107 }
1108 Some(Self {
1109 label: table.cell(row, "label").map(str::to_string),
1110 })
1111 }
1112}
1113
1114impl AppearanceInfo {
1115 fn from_row(table: &TwoDa, appearance_id: u16) -> Option<Self> {
1116 let row = usize::from(appearance_id);
1117 if row >= table.rows.len() {
1118 return None;
1119 }
1120 Some(Self {
1121 label: table.cell(row, "label").map(str::to_string),
1122 })
1123 }
1124}
1125
1126impl PortraitInfo {
1127 fn from_row(table: &TwoDa, portrait_id: u16) -> Option<Self> {
1128 let row = usize::from(portrait_id);
1129 if row >= table.rows.len() {
1130 return None;
1131 }
1132 Some(Self {
1133 label: table.cell(row, "label").map(str::to_string),
1134 })
1135 }
1136}
1137
1138impl SoundsetInfo {
1139 fn from_row(table: &TwoDa, soundset_id: u16) -> Option<Self> {
1140 let row = usize::from(soundset_id);
1141 if row >= table.rows.len() {
1142 return None;
1143 }
1144 Some(Self {
1145 label: table.cell(row, "label").map(str::to_string),
1146 })
1147 }
1148}
1149
1150#[derive(Debug, Clone, PartialEq, Eq)]
1169pub enum DecodedSpecialAbility {
1170 BodyFuel {
1173 spell_id: u16,
1175 flags: u8,
1177 caster_level: u8,
1179 },
1180 Rage {
1182 spell_id: u16,
1184 flags: u8,
1186 caster_level: u8,
1188 },
1189 MonsterSlamAttack {
1192 spell_id: u16,
1194 flags: u8,
1196 caster_level: u8,
1198 },
1199 Unknown {
1204 spell_id: u16,
1206 spell_label: Option<String>,
1210 flags: u8,
1212 caster_level: u8,
1214 },
1215}
1216
1217impl DecodedSpecialAbility {
1218 pub fn spell_id(&self) -> u16 {
1220 match self {
1221 Self::BodyFuel { spell_id, .. }
1222 | Self::Rage { spell_id, .. }
1223 | Self::MonsterSlamAttack { spell_id, .. }
1224 | Self::Unknown { spell_id, .. } => *spell_id,
1225 }
1226 }
1227
1228 pub fn flags(&self) -> u8 {
1230 match self {
1231 Self::BodyFuel { flags, .. }
1232 | Self::Rage { flags, .. }
1233 | Self::MonsterSlamAttack { flags, .. }
1234 | Self::Unknown { flags, .. } => *flags,
1235 }
1236 }
1237
1238 pub fn caster_level(&self) -> u8 {
1240 match self {
1241 Self::BodyFuel { caster_level, .. }
1242 | Self::Rage { caster_level, .. }
1243 | Self::MonsterSlamAttack { caster_level, .. }
1244 | Self::Unknown { caster_level, .. } => *caster_level,
1245 }
1246 }
1247}
1248
1249fn dispatch_special_ability(
1254 ability: &UtcSpecialAbility,
1255 spells_table: Option<&TwoDa>,
1256) -> DecodedSpecialAbility {
1257 let label = spells_table.and_then(|table| {
1258 let row = usize::from(ability.spell_id);
1259 if row >= table.rows.len() {
1260 return None;
1261 }
1262 table.cell(row, "label").map(str::to_string)
1263 });
1264 let spell_id = ability.spell_id;
1265 let flags = ability.spell_flags;
1266 let caster_level = ability.spell_caster_level;
1267 match label.as_deref() {
1268 Some("SPECIAL_ABILITY_BODY_FUEL") => DecodedSpecialAbility::BodyFuel {
1269 spell_id,
1270 flags,
1271 caster_level,
1272 },
1273 Some("SPECIAL_ABILITY_RAGE") => DecodedSpecialAbility::Rage {
1274 spell_id,
1275 flags,
1276 caster_level,
1277 },
1278 Some("MONSTER_ABILITY_SLAM_ATTACK") => DecodedSpecialAbility::MonsterSlamAttack {
1279 spell_id,
1280 flags,
1281 caster_level,
1282 },
1283 _ => DecodedSpecialAbility::Unknown {
1284 spell_id,
1285 spell_label: label,
1286 flags,
1287 caster_level,
1288 },
1289 }
1290}
1291
1292fn dispatch_class(class: &UtcClass, classes_table: Option<&TwoDa>) -> DecodedClass {
1302 let label = classes_table.and_then(|table| {
1303 let row = usize::try_from(class.class_id).ok()?;
1304 if row >= table.rows.len() {
1305 return None;
1306 }
1307 table.cell(row, "label").map(str::to_string)
1308 });
1309 let class_id = class.class_id;
1310 let level = class.class_level;
1311 let powers = class.powers.clone();
1312 match label.as_deref() {
1313 Some("Soldier") => DecodedClass::Soldier {
1314 class_id,
1315 level,
1316 powers,
1317 },
1318 Some("Scout") => DecodedClass::Scout {
1319 class_id,
1320 level,
1321 powers,
1322 },
1323 Some("Scoundrel") => DecodedClass::Scoundrel {
1324 class_id,
1325 level,
1326 powers,
1327 },
1328 Some("JediGuardian") => DecodedClass::JediGuardian {
1329 class_id,
1330 level,
1331 powers,
1332 },
1333 Some("JediConsular") => DecodedClass::JediConsular {
1334 class_id,
1335 level,
1336 powers,
1337 },
1338 Some("JediSentinel") => DecodedClass::JediSentinel {
1339 class_id,
1340 level,
1341 powers,
1342 },
1343 Some("CombatDroid") => DecodedClass::CombatDroid {
1344 class_id,
1345 level,
1346 powers,
1347 },
1348 Some("ExpertDroid") => DecodedClass::ExpertDroid {
1349 class_id,
1350 level,
1351 powers,
1352 },
1353 Some("Minion") => DecodedClass::Minion {
1354 class_id,
1355 level,
1356 powers,
1357 },
1358 _ => DecodedClass::Unknown {
1363 class_id,
1364 class_label: label,
1365 level,
1366 powers,
1367 },
1368 }
1369}
1370
1371impl<'a> UtcProjection<'a> {
1372 pub fn as_utc(&self) -> &'a Utc {
1374 self.utc
1375 }
1376
1377 pub fn snapshot(&self, cache: &mut TwoDaCache) -> UtcSnapshot<'a> {
1389 let race_info = cache
1390 .twoda(tables::RACIALTYPES)
1391 .ok()
1392 .and_then(|table| RaceInfo::from_row(table, self.utc.race_id));
1393 let appearance_info = cache
1394 .twoda(tables::APPEARANCE)
1395 .ok()
1396 .and_then(|table| AppearanceInfo::from_row(table, self.utc.appearance_id));
1397 let portrait_info = cache
1398 .twoda(tables::PORTRAITS)
1399 .ok()
1400 .and_then(|table| PortraitInfo::from_row(table, self.utc.portrait_id));
1401 let soundset_info = cache
1402 .twoda("soundset")
1403 .ok()
1404 .and_then(|table| SoundsetInfo::from_row(table, self.utc.soundset_id));
1405 let decoded_classes = {
1406 let classes_table = cache.twoda(tables::CLASSES).ok();
1407 self.utc
1408 .classes
1409 .iter()
1410 .map(|class| dispatch_class(class, classes_table))
1411 .collect()
1412 };
1413 let decoded_special_abilities = {
1414 let spells_table = cache.twoda("spells").ok();
1415 self.utc
1416 .special_abilities
1417 .iter()
1418 .map(|ability| dispatch_special_ability(ability, spells_table))
1419 .collect()
1420 };
1421 let decoded_feats = {
1422 let feat_table = cache.twoda(tables::FEAT).ok();
1423 self.utc
1424 .feats
1425 .iter()
1426 .map(|feat_id| dispatch_feat(*feat_id, feat_table))
1427 .collect()
1428 };
1429 UtcSnapshot {
1430 utc: self.utc,
1431 race_info,
1432 appearance_info,
1433 portrait_info,
1434 soundset_info,
1435 decoded_classes,
1436 decoded_special_abilities,
1437 decoded_feats,
1438 }
1439 }
1440}
1441
1442impl<'a> UtcSnapshot<'a> {
1443 pub fn as_utc(&self) -> &'a Utc {
1445 self.utc
1446 }
1447
1448 pub fn race_label(&self) -> Option<&str> {
1454 self.race_info.as_ref()?.label.as_deref()
1455 }
1456
1457 pub fn appearance_label(&self) -> Option<&str> {
1464 self.appearance_info.as_ref()?.label.as_deref()
1465 }
1466
1467 pub fn portrait_label(&self) -> Option<&str> {
1474 self.portrait_info.as_ref()?.label.as_deref()
1475 }
1476
1477 pub fn soundset_label(&self) -> Option<&str> {
1484 self.soundset_info.as_ref()?.label.as_deref()
1485 }
1486
1487 pub fn alignment(&self) -> u8 {
1495 self.utc.alignment.min(100)
1496 }
1497
1498 pub fn classes(&self) -> &[DecodedClass] {
1507 &self.decoded_classes
1508 }
1509
1510 pub fn total_level(&self) -> i32 {
1517 self.decoded_classes
1518 .iter()
1519 .map(|class| i32::from(class.level()))
1520 .sum()
1521 }
1522
1523 pub fn has_class(&self, filter: ClassKindFilter) -> bool {
1530 self.decoded_classes
1531 .iter()
1532 .any(|class| filter.matches(class))
1533 }
1534
1535 pub fn is_force_user(&self) -> bool {
1540 self.has_class(ClassKindFilter::AnyJedi)
1541 }
1542
1543 pub fn is_droid(&self) -> bool {
1547 self.has_class(ClassKindFilter::AnyDroid)
1548 }
1549
1550 pub fn feats(&self) -> &[DecodedFeat] {
1560 &self.decoded_feats
1561 }
1562
1563 pub fn has_feat(&self, filter: FeatKindFilter) -> bool {
1570 self.decoded_feats.iter().any(|feat| filter.matches(feat))
1571 }
1572
1573 pub fn special_abilities(&self) -> &[DecodedSpecialAbility] {
1584 &self.decoded_special_abilities
1585 }
1586
1587 pub fn equipment(&self) -> &[UtcEquipmentItem] {
1602 &self.utc.equipment
1603 }
1604
1605 pub fn inventory(&self) -> &[UtcInventoryItem] {
1616 &self.utc.inventory
1617 }
1618}
1619
1620#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1632pub enum ClassKindFilter {
1633 Soldier,
1635 Scout,
1637 Scoundrel,
1639 JediGuardian,
1641 JediConsular,
1643 JediSentinel,
1645 CombatDroid,
1647 ExpertDroid,
1649 Minion,
1651 AnyJedi,
1653 AnyDroid,
1655}
1656
1657impl ClassKindFilter {
1658 fn matches(self, class: &DecodedClass) -> bool {
1659 match self {
1660 Self::Soldier => matches!(class, DecodedClass::Soldier { .. }),
1661 Self::Scout => matches!(class, DecodedClass::Scout { .. }),
1662 Self::Scoundrel => matches!(class, DecodedClass::Scoundrel { .. }),
1663 Self::JediGuardian => matches!(class, DecodedClass::JediGuardian { .. }),
1664 Self::JediConsular => matches!(class, DecodedClass::JediConsular { .. }),
1665 Self::JediSentinel => matches!(class, DecodedClass::JediSentinel { .. }),
1666 Self::CombatDroid => matches!(class, DecodedClass::CombatDroid { .. }),
1667 Self::ExpertDroid => matches!(class, DecodedClass::ExpertDroid { .. }),
1668 Self::Minion => matches!(class, DecodedClass::Minion { .. }),
1669 Self::AnyJedi => matches!(
1670 class,
1671 DecodedClass::JediGuardian { .. }
1672 | DecodedClass::JediConsular { .. }
1673 | DecodedClass::JediSentinel { .. }
1674 ),
1675 Self::AnyDroid => matches!(
1676 class,
1677 DecodedClass::CombatDroid { .. } | DecodedClass::ExpertDroid { .. }
1678 ),
1679 }
1680 }
1681}
1682
1683#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1695pub enum FeatKindFilter {
1696 AnyArmourProficiency,
1698 AnyWeaponProficiency,
1701 AnyWeaponFocus,
1703 AnyWeaponSpecialization,
1705 AnyPowerAttack,
1707 AnyPowerBlast,
1709 AnyCriticalStrike,
1712 AnyFlurry,
1714 AnySniperShot,
1716 AnyRapidShot,
1718 AnyTwoWeapon,
1720 AnyJediDefense,
1722 AnyForceFocus,
1724 AnyForceJump,
1726 AnyForceImmunity,
1728 AnySneakAttack,
1730 AnyUncannyDodge,
1732 AnyToughness,
1734 AnyConditioning,
1736 AnyImplant,
1738 AnyDroidUpgrade,
1740 AnyDueling,
1742 AnySense,
1744 AnyGuardStance,
1747 AnySkillFocus,
1750}
1751
1752impl FeatKindFilter {
1753 fn matches(self, feat: &DecodedFeat) -> bool {
1754 match self {
1755 Self::AnyArmourProficiency => matches!(
1756 feat,
1757 DecodedFeat::ArmourProfLight { .. }
1758 | DecodedFeat::ArmourProfMedium { .. }
1759 | DecodedFeat::ArmourProfHeavy { .. }
1760 ),
1761 Self::AnyWeaponProficiency => matches!(
1762 feat,
1763 DecodedFeat::WeaponProfBlaster { .. }
1764 | DecodedFeat::WeaponProfBlasterRifle { .. }
1765 | DecodedFeat::WeaponProfHeavyWeapons { .. }
1766 | DecodedFeat::WeaponProfMeleeWeapons { .. }
1767 | DecodedFeat::WeaponProfLightsaber { .. }
1768 | DecodedFeat::WeaponProfGrenade { .. }
1769 | DecodedFeat::WeaponProfSimpleWeapons { .. }
1770 | DecodedFeat::ProficiencyAll { .. }
1771 ),
1772 Self::AnyWeaponFocus => matches!(
1773 feat,
1774 DecodedFeat::WeaponFocusBlaster { .. }
1775 | DecodedFeat::WeaponFocusBlasterRifle { .. }
1776 | DecodedFeat::WeaponFocusGrenade { .. }
1777 | DecodedFeat::WeaponFocusHeavyWeapons { .. }
1778 | DecodedFeat::WeaponFocusLightsaber { .. }
1779 | DecodedFeat::WeaponFocusMeleeWeapons { .. }
1780 | DecodedFeat::WeaponFocusSimpleWeapons { .. }
1781 ),
1782 Self::AnyWeaponSpecialization => matches!(
1783 feat,
1784 DecodedFeat::WeaponSpecBlaster { .. }
1785 | DecodedFeat::WeaponSpecBlasterRifle { .. }
1786 | DecodedFeat::WeaponSpecGrenade { .. }
1787 | DecodedFeat::WeaponSpecHeavyWeapons { .. }
1788 | DecodedFeat::WeaponSpecLightsaber { .. }
1789 | DecodedFeat::WeaponSpecMeleeWeapons { .. }
1790 | DecodedFeat::WeaponSpecSimpleWeapons { .. }
1791 ),
1792 Self::AnyPowerAttack => matches!(
1793 feat,
1794 DecodedFeat::PowerAttack { .. }
1795 | DecodedFeat::ImprovedPowerAttack { .. }
1796 | DecodedFeat::MasterPowerAttack { .. }
1797 ),
1798 Self::AnyPowerBlast => matches!(
1799 feat,
1800 DecodedFeat::PowerBlast { .. }
1801 | DecodedFeat::ImprovedPowerBlast { .. }
1802 | DecodedFeat::MasterPowerBlast { .. }
1803 ),
1804 Self::AnyCriticalStrike => matches!(
1805 feat,
1806 DecodedFeat::CriticalStrike { .. }
1807 | DecodedFeat::ImprovedCriticalStrike { .. }
1808 | DecodedFeat::MasterCriticalStrike { .. }
1809 ),
1810 Self::AnyFlurry => {
1811 matches!(
1812 feat,
1813 DecodedFeat::Flurry { .. } | DecodedFeat::ImprovedFlurry { .. }
1814 )
1815 }
1816 Self::AnySniperShot => matches!(
1817 feat,
1818 DecodedFeat::SniperShot { .. }
1819 | DecodedFeat::ImprovedSniperShot { .. }
1820 | DecodedFeat::MasterSniperShot { .. }
1821 ),
1822 Self::AnyRapidShot => matches!(
1823 feat,
1824 DecodedFeat::RapidShot { .. } | DecodedFeat::ImprovedRapidShot { .. }
1825 ),
1826 Self::AnyTwoWeapon => matches!(
1827 feat,
1828 DecodedFeat::TwoWeaponFighting { .. }
1829 | DecodedFeat::TwoWeaponAdvanced { .. }
1830 | DecodedFeat::TwoWeaponMastery { .. }
1831 ),
1832 Self::AnyJediDefense => matches!(
1833 feat,
1834 DecodedFeat::JediDefense { .. }
1835 | DecodedFeat::AdvancedJediDefense { .. }
1836 | DecodedFeat::MasterJediDefense { .. }
1837 ),
1838 Self::AnyForceFocus => matches!(
1839 feat,
1840 DecodedFeat::ForceFocus { .. }
1841 | DecodedFeat::ForceFocusAdvanced { .. }
1842 | DecodedFeat::ForceFocusMastery { .. }
1843 | DecodedFeat::ForceFocusAlter { .. }
1844 | DecodedFeat::ForceFocusControl { .. }
1845 ),
1846 Self::AnyForceJump => matches!(
1847 feat,
1848 DecodedFeat::ForceJump { .. }
1849 | DecodedFeat::ForceJumpAdvanced { .. }
1850 | DecodedFeat::ForceJumpMastery { .. }
1851 ),
1852 Self::AnyForceImmunity => matches!(
1853 feat,
1854 DecodedFeat::ForceImmunityFear { .. }
1855 | DecodedFeat::ForceImmunityStun { .. }
1856 | DecodedFeat::ForceImmunityParalysis { .. }
1857 ),
1858 Self::AnySneakAttack => matches!(
1859 feat,
1860 DecodedFeat::SneakAttack1D6 { .. }
1861 | DecodedFeat::SneakAttack2D6 { .. }
1862 | DecodedFeat::SneakAttack3D6 { .. }
1863 | DecodedFeat::SneakAttack4D6 { .. }
1864 | DecodedFeat::SneakAttack5D6 { .. }
1865 | DecodedFeat::SneakAttack6D6 { .. }
1866 ),
1867 Self::AnyUncannyDodge => matches!(
1868 feat,
1869 DecodedFeat::UncannyDodge1 { .. } | DecodedFeat::UncannyDodge2 { .. }
1870 ),
1871 Self::AnyToughness => matches!(
1872 feat,
1873 DecodedFeat::Toughness { .. }
1874 | DecodedFeat::ImprovedToughness { .. }
1875 | DecodedFeat::MasterToughness { .. }
1876 ),
1877 Self::AnyConditioning => matches!(
1878 feat,
1879 DecodedFeat::Conditioning { .. }
1880 | DecodedFeat::ImprovedConditioning { .. }
1881 | DecodedFeat::MasterConditioning { .. }
1882 ),
1883 Self::AnyImplant => matches!(
1884 feat,
1885 DecodedFeat::ImplantLevel1 { .. }
1886 | DecodedFeat::ImplantLevel2 { .. }
1887 | DecodedFeat::ImplantLevel3 { .. }
1888 ),
1889 Self::AnyDroidUpgrade => matches!(
1890 feat,
1891 DecodedFeat::DroidUpgrade1 { .. }
1892 | DecodedFeat::DroidUpgrade2 { .. }
1893 | DecodedFeat::DroidUpgrade3 { .. }
1894 ),
1895 Self::AnyDueling => matches!(
1896 feat,
1897 DecodedFeat::Dueling { .. }
1898 | DecodedFeat::AdvancedDueling { .. }
1899 | DecodedFeat::MasterDueling { .. }
1900 ),
1901 Self::AnySense => matches!(
1902 feat,
1903 DecodedFeat::JediSense { .. }
1904 | DecodedFeat::KnightSense { .. }
1905 | DecodedFeat::MasterSense { .. }
1906 ),
1907 Self::AnyGuardStance => matches!(
1908 feat,
1909 DecodedFeat::GuardStance { .. }
1910 | DecodedFeat::AdvancedGuardStance { .. }
1911 | DecodedFeat::MasterGuardStance { .. }
1912 ),
1913 Self::AnySkillFocus => matches!(
1914 feat,
1915 DecodedFeat::SkillFocusAwareness { .. }
1916 | DecodedFeat::SkillFocusTreatInjury { .. }
1917 | DecodedFeat::SkillFocusStealth { .. }
1918 ),
1919 }
1920 }
1921}
1922
1923impl Utc {
1924 pub fn project(&self) -> UtcProjection<'_> {
1933 UtcProjection { utc: self }
1934 }
1935
1936 pub fn snapshot(&self, cache: &mut TwoDaCache) -> UtcSnapshot<'_> {
1944 self.project().snapshot(cache)
1945 }
1946}
1947
1948#[cfg(test)]
1949mod tests {
1950 use super::*;
1951 use rakata_core::{ResourceType, ResourceTypeCode};
1952 use rakata_extract::{OverrideSource, Resolver, ResolverSourceRef};
1953 use rakata_formats::twoda::{write_twoda_to_vec, TwoDaRow};
1954
1955 fn label_only_2da(rows: &[(usize, &str)]) -> TwoDa {
1956 let max_row = rows.iter().map(|(idx, _)| *idx).max().unwrap_or(0);
1957 let mut table_rows = Vec::with_capacity(max_row + 1);
1958 for row_index in 0..=max_row {
1959 let label = rows
1960 .iter()
1961 .find(|(idx, _)| *idx == row_index)
1962 .map(|(_, label)| (*label).to_string())
1963 .unwrap_or_default();
1964 table_rows.push(TwoDaRow {
1965 label: row_index.to_string(),
1966 cells: vec![label],
1967 });
1968 }
1969 TwoDa {
1970 headers: vec!["label".to_string()],
1971 rows: table_rows,
1972 }
1973 }
1974
1975 fn add_2da_entry(overrides: &mut OverrideSource, name: &str, table: &TwoDa) {
1976 let bytes = write_twoda_to_vec(table).expect("serialize 2da fixture");
1977 overrides
1978 .add_entry(
1979 name,
1980 ResourceTypeCode::from(ResourceType::TwoDa),
1981 bytes,
1982 "test",
1983 )
1984 .expect("add override entry");
1985 }
1986
1987 #[test]
1988 fn project_returns_projection_wrapping_source() {
1989 let utc = Utc::default();
1990 let projection = utc.project();
1991 assert!(std::ptr::eq(projection.as_utc(), &utc));
1992 }
1993
1994 #[test]
1995 fn snapshot_returns_snapshot_wrapping_source() {
1996 let utc = Utc::default();
1997 let resolver = Resolver::new();
1998 let mut cache = TwoDaCache::new(&resolver);
1999
2000 let snapshot = utc.snapshot(&mut cache);
2001 assert!(std::ptr::eq(snapshot.as_utc(), &utc));
2002 }
2003
2004 #[test]
2005 fn sugar_snapshot_matches_project_then_snapshot() {
2006 let utc = Utc::default();
2007 let resolver = Resolver::new();
2008
2009 let mut sugar_cache = TwoDaCache::new(&resolver);
2010 let sugar = utc.snapshot(&mut sugar_cache);
2011
2012 let mut explicit_cache = TwoDaCache::new(&resolver);
2013 let explicit = utc.project().snapshot(&mut explicit_cache);
2014
2015 assert!(std::ptr::eq(sugar.as_utc(), explicit.as_utc()));
2016 }
2017
2018 #[test]
2019 fn one_projection_feeds_independent_snapshots() {
2020 let utc = Utc::default();
2021 let resolver = Resolver::new();
2022 let projection = utc.project();
2023
2024 let mut cache_a = TwoDaCache::new(&resolver);
2025 let mut cache_b = TwoDaCache::new(&resolver);
2026
2027 let snap_a = projection.snapshot(&mut cache_a);
2028 let snap_b = projection.snapshot(&mut cache_b);
2029
2030 assert!(std::ptr::eq(snap_a.as_utc(), &utc));
2031 assert!(std::ptr::eq(snap_b.as_utc(), &utc));
2032 }
2033
2034 #[test]
2035 fn race_label_resolves_against_racialtypes_2da() {
2036 let utc = Utc {
2037 race_id: 6,
2038 ..Utc::default()
2039 };
2040 let racialtypes = label_only_2da(&[(6, "Human")]);
2041 let mut overrides = OverrideSource::new();
2042 add_2da_entry(&mut overrides, "racialtypes", &racialtypes);
2043 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2044 let mut cache = TwoDaCache::new(&resolver);
2045
2046 let snapshot = utc.snapshot(&mut cache);
2047 assert_eq!(snapshot.race_label(), Some("Human"));
2048 }
2049
2050 #[test]
2051 fn race_label_returns_none_when_race_id_is_out_of_range() {
2052 let utc = Utc {
2054 race_id: 99,
2055 ..Utc::default()
2056 };
2057 let racialtypes = label_only_2da(&[(0, "Human"), (1, "Droid")]);
2058 let mut overrides = OverrideSource::new();
2059 add_2da_entry(&mut overrides, "racialtypes", &racialtypes);
2060 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2061 let mut cache = TwoDaCache::new(&resolver);
2062
2063 let snapshot = utc.snapshot(&mut cache);
2064 assert!(snapshot.race_label().is_none());
2065 }
2066
2067 #[test]
2068 fn race_label_returns_none_when_racialtypes_unavailable() {
2069 let utc = Utc {
2070 race_id: 6,
2071 ..Utc::default()
2072 };
2073 let resolver = Resolver::new();
2075 let mut cache = TwoDaCache::new(&resolver);
2076
2077 let snapshot = utc.snapshot(&mut cache);
2078 assert!(snapshot.race_label().is_none());
2079 }
2080
2081 #[test]
2082 fn appearance_portrait_soundset_labels_resolve_via_their_2das() {
2083 let utc = Utc {
2084 appearance_id: 12,
2085 portrait_id: 34,
2086 soundset_id: 56,
2087 ..Utc::default()
2088 };
2089 let appearance = label_only_2da(&[(12, "Male_Caucasian_Tough")]);
2090 let portraits = label_only_2da(&[(34, "po_pmhc01")]);
2091 let soundset = label_only_2da(&[(56, "n_default")]);
2092 let mut overrides = OverrideSource::new();
2093 add_2da_entry(&mut overrides, "appearance", &appearance);
2094 add_2da_entry(&mut overrides, "portraits", &portraits);
2095 add_2da_entry(&mut overrides, "soundset", &soundset);
2096 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2097 let mut cache = TwoDaCache::new(&resolver);
2098
2099 let snapshot = utc.snapshot(&mut cache);
2100 assert_eq!(snapshot.appearance_label(), Some("Male_Caucasian_Tough"));
2101 assert_eq!(snapshot.portrait_label(), Some("po_pmhc01"));
2102 assert_eq!(snapshot.soundset_label(), Some("n_default"));
2103 }
2104
2105 #[test]
2106 fn alignment_returns_raw_value_when_in_range() {
2107 let utc = Utc {
2108 alignment: 50,
2109 ..Utc::default()
2110 };
2111 let resolver = Resolver::new();
2112 let mut cache = TwoDaCache::new(&resolver);
2113
2114 let snapshot = utc.snapshot(&mut cache);
2115 assert_eq!(snapshot.alignment(), 50);
2116 }
2117
2118 #[test]
2119 fn alignment_clamps_at_one_hundred_to_match_engine_load_behavior() {
2120 let utc = Utc {
2124 alignment: 200,
2125 ..Utc::default()
2126 };
2127 let resolver = Resolver::new();
2128 let mut cache = TwoDaCache::new(&resolver);
2129
2130 let snapshot = utc.snapshot(&mut cache);
2131 assert_eq!(snapshot.alignment(), 100);
2132 }
2133
2134 fn class_entry(class_id: i32, level: i16, powers: Vec<u16>) -> UtcClass {
2137 UtcClass {
2138 class_id,
2139 class_level: level,
2140 powers,
2141 }
2142 }
2143
2144 #[test]
2145 fn classes_dispatch_every_vanilla_label_to_its_typed_variant() {
2146 let utc = Utc {
2149 classes: vec![
2150 class_entry(0, 1, vec![]),
2151 class_entry(1, 1, vec![]),
2152 class_entry(2, 1, vec![]),
2153 class_entry(3, 1, vec![]),
2154 class_entry(4, 1, vec![]),
2155 class_entry(5, 1, vec![]),
2156 class_entry(6, 1, vec![]),
2157 class_entry(7, 1, vec![]),
2158 class_entry(8, 1, vec![]),
2159 ],
2160 ..Utc::default()
2161 };
2162 let classes = label_only_2da(&[
2163 (0, "Soldier"),
2164 (1, "Scout"),
2165 (2, "Scoundrel"),
2166 (3, "JediGuardian"),
2167 (4, "JediConsular"),
2168 (5, "JediSentinel"),
2169 (6, "CombatDroid"),
2170 (7, "ExpertDroid"),
2171 (8, "Minion"),
2172 ]);
2173 let mut overrides = OverrideSource::new();
2174 add_2da_entry(&mut overrides, "classes", &classes);
2175 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2176 let mut cache = TwoDaCache::new(&resolver);
2177
2178 let snapshot = utc.snapshot(&mut cache);
2179 let kinds: Vec<&str> = snapshot
2180 .classes()
2181 .iter()
2182 .map(|class| match class {
2183 DecodedClass::Soldier { .. } => "Soldier",
2184 DecodedClass::Scout { .. } => "Scout",
2185 DecodedClass::Scoundrel { .. } => "Scoundrel",
2186 DecodedClass::JediGuardian { .. } => "JediGuardian",
2187 DecodedClass::JediConsular { .. } => "JediConsular",
2188 DecodedClass::JediSentinel { .. } => "JediSentinel",
2189 DecodedClass::CombatDroid { .. } => "CombatDroid",
2190 DecodedClass::ExpertDroid { .. } => "ExpertDroid",
2191 DecodedClass::Minion { .. } => "Minion",
2192 DecodedClass::Unknown { .. } => "<unknown>",
2193 })
2194 .collect();
2195 assert_eq!(
2196 kinds,
2197 vec![
2198 "Soldier",
2199 "Scout",
2200 "Scoundrel",
2201 "JediGuardian",
2202 "JediConsular",
2203 "JediSentinel",
2204 "CombatDroid",
2205 "ExpertDroid",
2206 "Minion",
2207 ]
2208 );
2209 }
2210
2211 #[test]
2212 fn classes_dispatch_by_label_so_mod_renumbered_class_still_routes() {
2213 let utc = Utc {
2217 classes: vec![class_entry(42, 5, vec![])],
2218 ..Utc::default()
2219 };
2220 let classes = label_only_2da(&[(42, "JediGuardian")]);
2221 let mut overrides = OverrideSource::new();
2222 add_2da_entry(&mut overrides, "classes", &classes);
2223 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2224 let mut cache = TwoDaCache::new(&resolver);
2225
2226 let snapshot = utc.snapshot(&mut cache);
2227 match &snapshot.classes()[0] {
2228 DecodedClass::JediGuardian {
2229 class_id, level, ..
2230 } => {
2231 assert_eq!(*class_id, 42);
2234 assert_eq!(*level, 5);
2235 }
2236 other => panic!("expected JediGuardian, got {other:?}"),
2237 }
2238 }
2239
2240 #[test]
2241 fn unknown_class_carries_resolved_label_when_2da_row_exists() {
2242 let utc = Utc {
2245 classes: vec![class_entry(9, 3, vec![])],
2246 ..Utc::default()
2247 };
2248 let classes = label_only_2da(&[(9, "DarkJediMaster")]);
2249 let mut overrides = OverrideSource::new();
2250 add_2da_entry(&mut overrides, "classes", &classes);
2251 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2252 let mut cache = TwoDaCache::new(&resolver);
2253
2254 let snapshot = utc.snapshot(&mut cache);
2255 match &snapshot.classes()[0] {
2256 DecodedClass::Unknown {
2257 class_id,
2258 class_label,
2259 level,
2260 ..
2261 } => {
2262 assert_eq!(*class_id, 9);
2263 assert_eq!(class_label.as_deref(), Some("DarkJediMaster"));
2264 assert_eq!(*level, 3);
2265 }
2266 other => panic!("expected Unknown, got {other:?}"),
2267 }
2268 }
2269
2270 #[test]
2271 fn unknown_class_carries_no_label_when_class_id_is_out_of_range() {
2272 let utc = Utc {
2275 classes: vec![class_entry(99, 1, vec![])],
2276 ..Utc::default()
2277 };
2278 let classes = label_only_2da(&[(0, "Soldier")]);
2279 let mut overrides = OverrideSource::new();
2280 add_2da_entry(&mut overrides, "classes", &classes);
2281 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2282 let mut cache = TwoDaCache::new(&resolver);
2283
2284 let snapshot = utc.snapshot(&mut cache);
2285 match &snapshot.classes()[0] {
2286 DecodedClass::Unknown {
2287 class_id,
2288 class_label,
2289 ..
2290 } => {
2291 assert_eq!(*class_id, 99);
2292 assert!(class_label.is_none());
2293 }
2294 other => panic!("expected Unknown, got {other:?}"),
2295 }
2296 }
2297
2298 #[test]
2299 fn unknown_class_carries_no_label_when_classes_2da_unavailable() {
2300 let utc = Utc {
2303 classes: vec![class_entry(3, 5, vec![])],
2304 ..Utc::default()
2305 };
2306 let resolver = Resolver::new();
2307 let mut cache = TwoDaCache::new(&resolver);
2308
2309 let snapshot = utc.snapshot(&mut cache);
2310 match &snapshot.classes()[0] {
2311 DecodedClass::Unknown {
2312 class_id,
2313 class_label,
2314 level,
2315 ..
2316 } => {
2317 assert_eq!(*class_id, 3);
2318 assert!(class_label.is_none());
2319 assert_eq!(*level, 5);
2320 }
2321 other => panic!("expected Unknown, got {other:?}"),
2322 }
2323 }
2324
2325 #[test]
2326 fn class_powers_preserve_source_order_per_entry() {
2327 let utc = Utc {
2328 classes: vec![class_entry(4, 6, vec![100, 200, 50, 200])],
2329 ..Utc::default()
2330 };
2331 let classes = label_only_2da(&[(4, "JediConsular")]);
2332 let mut overrides = OverrideSource::new();
2333 add_2da_entry(&mut overrides, "classes", &classes);
2334 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2335 let mut cache = TwoDaCache::new(&resolver);
2336
2337 let snapshot = utc.snapshot(&mut cache);
2338 assert_eq!(snapshot.classes()[0].powers(), &[100, 200, 50, 200]);
2341 }
2342
2343 #[test]
2344 fn total_level_sums_across_typed_and_unknown_entries() {
2345 let utc = Utc {
2349 classes: vec![
2350 class_entry(3, 19, vec![]),
2351 class_entry(5, 3, vec![]),
2352 class_entry(99, 4, vec![]),
2353 ],
2354 ..Utc::default()
2355 };
2356 let classes = label_only_2da(&[(0, ""), (3, "JediGuardian"), (5, "JediSentinel")]);
2357 let mut overrides = OverrideSource::new();
2358 add_2da_entry(&mut overrides, "classes", &classes);
2359 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2360 let mut cache = TwoDaCache::new(&resolver);
2361
2362 let snapshot = utc.snapshot(&mut cache);
2363 assert_eq!(snapshot.total_level(), 26);
2364 }
2365
2366 #[test]
2367 fn one_projection_yields_diverging_class_kinds_per_scope() {
2368 let utc = Utc {
2373 classes: vec![class_entry(3, 10, vec![])],
2374 ..Utc::default()
2375 };
2376
2377 let vanilla_classes = label_only_2da(&[(3, "JediGuardian")]);
2378 let mut vanilla_overrides = OverrideSource::new();
2379 add_2da_entry(&mut vanilla_overrides, "classes", &vanilla_classes);
2380 let vanilla_resolver =
2381 Resolver::new().with_source(ResolverSourceRef::Override(&vanilla_overrides));
2382
2383 let mod_classes = label_only_2da(&[(3, "MandalorianWarrior")]);
2384 let mut mod_overrides = OverrideSource::new();
2385 add_2da_entry(&mut mod_overrides, "classes", &mod_classes);
2386 let mod_resolver = Resolver::new().with_source(ResolverSourceRef::Override(&mod_overrides));
2387
2388 let projection = utc.project();
2389 let mut vanilla_cache = TwoDaCache::new(&vanilla_resolver);
2390 let mut mod_cache = TwoDaCache::new(&mod_resolver);
2391
2392 let vanilla = projection.snapshot(&mut vanilla_cache);
2393 let modded = projection.snapshot(&mut mod_cache);
2394
2395 assert!(matches!(
2396 vanilla.classes()[0],
2397 DecodedClass::JediGuardian { .. }
2398 ));
2399 match &modded.classes()[0] {
2400 DecodedClass::Unknown { class_label, .. } => {
2401 assert_eq!(class_label.as_deref(), Some("MandalorianWarrior"));
2402 }
2403 other => panic!("expected Unknown, got {other:?}"),
2404 }
2405 }
2406
2407 #[test]
2410 fn equipment_and_inventory_passthrough_source_slices() {
2411 use rakata_core::ResRef;
2412
2413 let equip_resref = ResRef::new("g_w_blstrpstl001").expect("valid resref");
2414 let inv_resref = ResRef::new("g_i_medeqpmnt01").expect("valid resref");
2415 let utc = Utc {
2416 equipment: vec![UtcEquipmentItem::new(2, equip_resref)],
2417 inventory: vec![UtcInventoryItem::new(0, inv_resref)],
2418 ..Utc::default()
2419 };
2420 let resolver = Resolver::new();
2421 let mut cache = TwoDaCache::new(&resolver);
2422
2423 let snapshot = utc.snapshot(&mut cache);
2424 assert_eq!(snapshot.equipment().len(), 1);
2425 assert_eq!(snapshot.equipment()[0].slot_id, 2);
2426 assert_eq!(snapshot.equipment()[0].resref, equip_resref);
2427 assert_eq!(snapshot.inventory().len(), 1);
2428 assert_eq!(snapshot.inventory()[0].entry_id, 0);
2429 assert_eq!(snapshot.inventory()[0].resref, inv_resref);
2430 }
2431
2432 fn ability_entry(spell_id: u16, flags: u8, caster_level: u8) -> UtcSpecialAbility {
2435 UtcSpecialAbility {
2436 spell_id,
2437 spell_flags: flags,
2438 spell_caster_level: caster_level,
2439 }
2440 }
2441
2442 fn vanilla_spells_2da() -> TwoDa {
2443 label_only_2da(&[
2446 (52, "SPECIAL_ABILITY_BODY_FUEL"),
2447 (63, "SPECIAL_ABILITY_RAGE"),
2448 (83, "MONSTER_ABILITY_SLAM_ATTACK"),
2449 ])
2450 }
2451
2452 #[test]
2453 fn special_abilities_dispatch_three_vanilla_labels_to_typed_variants() {
2454 let utc = Utc {
2455 special_abilities: vec![
2456 ability_entry(52, 0, 1),
2457 ability_entry(63, 0, 1),
2458 ability_entry(83, 0, 1),
2459 ],
2460 ..Utc::default()
2461 };
2462 let mut overrides = OverrideSource::new();
2463 add_2da_entry(&mut overrides, "spells", &vanilla_spells_2da());
2464 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2465 let mut cache = TwoDaCache::new(&resolver);
2466
2467 let snapshot = utc.snapshot(&mut cache);
2468 assert!(matches!(
2469 snapshot.special_abilities()[0],
2470 DecodedSpecialAbility::BodyFuel { .. }
2471 ));
2472 assert!(matches!(
2473 snapshot.special_abilities()[1],
2474 DecodedSpecialAbility::Rage { .. }
2475 ));
2476 assert!(matches!(
2477 snapshot.special_abilities()[2],
2478 DecodedSpecialAbility::MonsterSlamAttack { .. }
2479 ));
2480 }
2481
2482 #[test]
2483 fn special_abilities_preserve_stacked_duplicates() {
2484 let utc = Utc {
2487 special_abilities: (0..5).map(|_| ability_entry(52, 0, 1)).collect(),
2488 ..Utc::default()
2489 };
2490 let mut overrides = OverrideSource::new();
2491 add_2da_entry(&mut overrides, "spells", &vanilla_spells_2da());
2492 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2493 let mut cache = TwoDaCache::new(&resolver);
2494
2495 let snapshot = utc.snapshot(&mut cache);
2496 assert_eq!(snapshot.special_abilities().len(), 5);
2497 for entry in snapshot.special_abilities() {
2498 assert!(matches!(entry, DecodedSpecialAbility::BodyFuel { .. }));
2499 }
2500 }
2501
2502 #[test]
2503 fn out_of_range_spell_id_surfaces_as_unknown_with_no_label() {
2504 let utc = Utc {
2508 special_abilities: vec![ability_entry(299, 1, 1)],
2509 ..Utc::default()
2510 };
2511 let mut overrides = OverrideSource::new();
2512 add_2da_entry(&mut overrides, "spells", &vanilla_spells_2da());
2513 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2514 let mut cache = TwoDaCache::new(&resolver);
2515
2516 let snapshot = utc.snapshot(&mut cache);
2517 match &snapshot.special_abilities()[0] {
2518 DecodedSpecialAbility::Unknown {
2519 spell_id,
2520 spell_label,
2521 flags,
2522 caster_level,
2523 } => {
2524 assert_eq!(*spell_id, 299);
2525 assert!(spell_label.is_none());
2526 assert_eq!(*flags, 1);
2527 assert_eq!(*caster_level, 1);
2528 }
2529 other => panic!("expected Unknown, got {other:?}"),
2530 }
2531 }
2532
2533 #[test]
2534 fn empty_special_ability_list_decodes_to_empty_slice() {
2535 let utc = Utc::default();
2536 let resolver = Resolver::new();
2537 let mut cache = TwoDaCache::new(&resolver);
2538
2539 let snapshot = utc.snapshot(&mut cache);
2540 assert!(snapshot.special_abilities().is_empty());
2541 }
2542
2543 fn utc_with_classes(class_ids: &[i32]) -> Utc {
2546 Utc {
2547 classes: class_ids
2548 .iter()
2549 .map(|id| class_entry(*id, 1, vec![]))
2550 .collect(),
2551 ..Utc::default()
2552 }
2553 }
2554
2555 fn vanilla_classes_2da() -> TwoDa {
2556 label_only_2da(&[
2557 (0, "Soldier"),
2558 (1, "Scout"),
2559 (2, "Scoundrel"),
2560 (3, "JediGuardian"),
2561 (4, "JediConsular"),
2562 (5, "JediSentinel"),
2563 (6, "CombatDroid"),
2564 (7, "ExpertDroid"),
2565 (8, "Minion"),
2566 ])
2567 }
2568
2569 fn vanilla_class_cache(overrides: &mut OverrideSource) {
2570 let classes = vanilla_classes_2da();
2571 add_2da_entry(overrides, "classes", &classes);
2572 }
2573
2574 #[test]
2575 fn is_force_user_matches_any_jedi_class() {
2576 for jedi_id in [3, 4, 5] {
2577 let utc = utc_with_classes(&[jedi_id]);
2578 let mut overrides = OverrideSource::new();
2579 vanilla_class_cache(&mut overrides);
2580 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2581 let mut cache = TwoDaCache::new(&resolver);
2582
2583 let snapshot = utc.snapshot(&mut cache);
2584 assert!(
2585 snapshot.is_force_user(),
2586 "class_id {jedi_id} should be a force user"
2587 );
2588 }
2589 }
2590
2591 #[test]
2592 fn is_force_user_false_for_non_jedi_classes() {
2593 for non_jedi_id in [0, 1, 2, 6, 7, 8] {
2594 let utc = utc_with_classes(&[non_jedi_id]);
2595 let mut overrides = OverrideSource::new();
2596 vanilla_class_cache(&mut overrides);
2597 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2598 let mut cache = TwoDaCache::new(&resolver);
2599
2600 let snapshot = utc.snapshot(&mut cache);
2601 assert!(
2602 !snapshot.is_force_user(),
2603 "class_id {non_jedi_id} should not be a force user"
2604 );
2605 }
2606 }
2607
2608 #[test]
2609 fn is_droid_matches_combat_and_expert_droid() {
2610 for droid_id in [6, 7] {
2611 let utc = utc_with_classes(&[droid_id]);
2612 let mut overrides = OverrideSource::new();
2613 vanilla_class_cache(&mut overrides);
2614 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2615 let mut cache = TwoDaCache::new(&resolver);
2616
2617 let snapshot = utc.snapshot(&mut cache);
2618 assert!(snapshot.is_droid(), "class_id {droid_id} should be a droid");
2619 }
2620 }
2621
2622 #[test]
2623 fn is_droid_false_for_organic_classes() {
2624 for organic_id in [0, 1, 2, 3, 4, 5, 8] {
2625 let utc = utc_with_classes(&[organic_id]);
2626 let mut overrides = OverrideSource::new();
2627 vanilla_class_cache(&mut overrides);
2628 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2629 let mut cache = TwoDaCache::new(&resolver);
2630
2631 let snapshot = utc.snapshot(&mut cache);
2632 assert!(
2633 !snapshot.is_droid(),
2634 "class_id {organic_id} should not be a droid"
2635 );
2636 }
2637 }
2638
2639 #[test]
2640 fn has_class_matches_specific_typed_variants() {
2641 let utc = utc_with_classes(&[0, 3]);
2642 let mut overrides = OverrideSource::new();
2643 vanilla_class_cache(&mut overrides);
2644 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2645 let mut cache = TwoDaCache::new(&resolver);
2646
2647 let snapshot = utc.snapshot(&mut cache);
2648 assert!(snapshot.has_class(ClassKindFilter::Soldier));
2649 assert!(snapshot.has_class(ClassKindFilter::JediGuardian));
2650 assert!(!snapshot.has_class(ClassKindFilter::Scout));
2651 assert!(!snapshot.has_class(ClassKindFilter::Minion));
2652 }
2653
2654 #[test]
2655 fn has_class_never_matches_unknown_entries() {
2656 let utc = utc_with_classes(&[99]);
2659 let mut overrides = OverrideSource::new();
2660 vanilla_class_cache(&mut overrides);
2661 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2662 let mut cache = TwoDaCache::new(&resolver);
2663
2664 let snapshot = utc.snapshot(&mut cache);
2665 assert!(!snapshot.has_class(ClassKindFilter::Soldier));
2666 assert!(!snapshot.has_class(ClassKindFilter::AnyJedi));
2667 assert!(!snapshot.has_class(ClassKindFilter::AnyDroid));
2668 assert!(!snapshot.is_force_user());
2669 assert!(!snapshot.is_droid());
2670 }
2671
2672 #[test]
2673 fn empty_class_list_helpers_all_return_false() {
2674 let utc = Utc::default();
2675 let resolver = Resolver::new();
2676 let mut cache = TwoDaCache::new(&resolver);
2677
2678 let snapshot = utc.snapshot(&mut cache);
2679 assert!(!snapshot.is_force_user());
2680 assert!(!snapshot.is_droid());
2681 assert!(!snapshot.has_class(ClassKindFilter::JediGuardian));
2682 }
2683
2684 #[test]
2685 fn empty_class_list_decodes_to_empty_classes_slice() {
2686 let utc = Utc::default();
2687 let resolver = Resolver::new();
2688 let mut cache = TwoDaCache::new(&resolver);
2689
2690 let snapshot = utc.snapshot(&mut cache);
2691 assert!(snapshot.classes().is_empty());
2692 assert_eq!(snapshot.total_level(), 0);
2693 }
2694
2695 #[test]
2696 fn one_projection_yields_diverging_race_labels_per_scope() {
2697 let utc = Utc {
2702 race_id: 6,
2703 ..Utc::default()
2704 };
2705
2706 let vanilla_races = label_only_2da(&[(6, "Human")]);
2707 let mut vanilla_overrides = OverrideSource::new();
2708 add_2da_entry(&mut vanilla_overrides, "racialtypes", &vanilla_races);
2709 let vanilla_resolver =
2710 Resolver::new().with_source(ResolverSourceRef::Override(&vanilla_overrides));
2711
2712 let mod_races = label_only_2da(&[(6, "Mandalorian")]);
2713 let mut mod_overrides = OverrideSource::new();
2714 add_2da_entry(&mut mod_overrides, "racialtypes", &mod_races);
2715 let mod_resolver = Resolver::new().with_source(ResolverSourceRef::Override(&mod_overrides));
2716
2717 let projection = utc.project();
2718 let mut vanilla_cache = TwoDaCache::new(&vanilla_resolver);
2719 let mut mod_cache = TwoDaCache::new(&mod_resolver);
2720
2721 let vanilla = projection.snapshot(&mut vanilla_cache);
2722 let modded = projection.snapshot(&mut mod_cache);
2723
2724 assert_eq!(vanilla.race_label(), Some("Human"));
2725 assert_eq!(modded.race_label(), Some("Mandalorian"));
2726 }
2727
2728 fn weapon_feats_2da() -> TwoDa {
2731 label_only_2da(&[
2732 (4, "ARMOUR_PROF_HEAVY"),
2733 (5, "ARMOUR_PROF_LIGHT"),
2734 (6, "ARMOUR_PROF_MEDIUM"),
2735 (32, "WEAPON_FOCUS_BLASTER"),
2736 (33, "WEAPON_FOCUS_BLASTER_RIFLE"),
2737 (34, "XXXX_WEAPON_FOCUS_GRENADE"),
2738 (35, "WEAPON_FOCUS_HEAVY_WEAPONS"),
2739 (36, "WEAPON_FOCUS_LIGHTSABER"),
2740 (37, "WEAPON_FOCUS_MELEE_WEAPONS"),
2741 (38, "XXXX_WEAPON_FOCUS_SIMPLE_WEAPONS"),
2742 (39, "WEAPON_PROF_BLASTER"),
2743 (40, "WEAPON_PROF_BLASTER_RIFLE"),
2744 (41, "XXXX_WEAPON_PROF_GRENADE"),
2745 (42, "WEAPON_PROF_HEAVY_WEAPONS"),
2746 (43, "WEAPON_PROF_LIGHTSABER"),
2747 (44, "WEAPON_PROF_MELEE_WEAPONS"),
2748 (45, "XXXX_WEAPON_PROF_SIMPLE_WEAPONS"),
2749 (46, "WEAPON_SPEC_BLASTER"),
2750 (47, "WEAPON_SPEC_BLASTER_RIFLE"),
2751 (48, "XXXX_WEAPON_SPEC_GRENADE"),
2752 (49, "WEAPON_SPEC_HEAVY_WEAPONS"),
2753 (50, "WEAPON_SPEC_LIGHTSABER"),
2754 (51, "WEAPON_SPEC_MELEE_WEAPONS"),
2755 (52, "XXXX_WEAPON_SPEC_SIMPLE_WEAPONS"),
2756 (93, "PROFICIENCY_ALL"),
2757 ])
2758 }
2759
2760 fn snapshot_feat_kind(feat: &DecodedFeat) -> &'static str {
2761 match feat {
2762 DecodedFeat::ArmourProfLight { .. } => "ArmourProfLight",
2763 DecodedFeat::ArmourProfMedium { .. } => "ArmourProfMedium",
2764 DecodedFeat::ArmourProfHeavy { .. } => "ArmourProfHeavy",
2765 DecodedFeat::WeaponProfBlaster { .. } => "WeaponProfBlaster",
2766 DecodedFeat::WeaponProfBlasterRifle { .. } => "WeaponProfBlasterRifle",
2767 DecodedFeat::WeaponProfHeavyWeapons { .. } => "WeaponProfHeavyWeapons",
2768 DecodedFeat::WeaponProfMeleeWeapons { .. } => "WeaponProfMeleeWeapons",
2769 DecodedFeat::WeaponProfLightsaber { .. } => "WeaponProfLightsaber",
2770 DecodedFeat::WeaponProfGrenade { .. } => "WeaponProfGrenade",
2771 DecodedFeat::WeaponProfSimpleWeapons { .. } => "WeaponProfSimpleWeapons",
2772 DecodedFeat::ProficiencyAll { .. } => "ProficiencyAll",
2773 DecodedFeat::WeaponFocusBlaster { .. } => "WeaponFocusBlaster",
2774 DecodedFeat::WeaponFocusBlasterRifle { .. } => "WeaponFocusBlasterRifle",
2775 DecodedFeat::WeaponFocusGrenade { .. } => "WeaponFocusGrenade",
2776 DecodedFeat::WeaponFocusHeavyWeapons { .. } => "WeaponFocusHeavyWeapons",
2777 DecodedFeat::WeaponFocusLightsaber { .. } => "WeaponFocusLightsaber",
2778 DecodedFeat::WeaponFocusMeleeWeapons { .. } => "WeaponFocusMeleeWeapons",
2779 DecodedFeat::WeaponFocusSimpleWeapons { .. } => "WeaponFocusSimpleWeapons",
2780 DecodedFeat::WeaponSpecBlaster { .. } => "WeaponSpecBlaster",
2781 DecodedFeat::WeaponSpecBlasterRifle { .. } => "WeaponSpecBlasterRifle",
2782 DecodedFeat::WeaponSpecGrenade { .. } => "WeaponSpecGrenade",
2783 DecodedFeat::WeaponSpecHeavyWeapons { .. } => "WeaponSpecHeavyWeapons",
2784 DecodedFeat::WeaponSpecLightsaber { .. } => "WeaponSpecLightsaber",
2785 DecodedFeat::WeaponSpecMeleeWeapons { .. } => "WeaponSpecMeleeWeapons",
2786 DecodedFeat::WeaponSpecSimpleWeapons { .. } => "WeaponSpecSimpleWeapons",
2787 DecodedFeat::PowerAttack { .. } => "PowerAttack",
2788 DecodedFeat::ImprovedPowerAttack { .. } => "ImprovedPowerAttack",
2789 DecodedFeat::MasterPowerAttack { .. } => "MasterPowerAttack",
2790 DecodedFeat::PowerBlast { .. } => "PowerBlast",
2791 DecodedFeat::ImprovedPowerBlast { .. } => "ImprovedPowerBlast",
2792 DecodedFeat::MasterPowerBlast { .. } => "MasterPowerBlast",
2793 DecodedFeat::CriticalStrike { .. } => "CriticalStrike",
2794 DecodedFeat::ImprovedCriticalStrike { .. } => "ImprovedCriticalStrike",
2795 DecodedFeat::MasterCriticalStrike { .. } => "MasterCriticalStrike",
2796 DecodedFeat::Flurry { .. } => "Flurry",
2797 DecodedFeat::ImprovedFlurry { .. } => "ImprovedFlurry",
2798 DecodedFeat::SniperShot { .. } => "SniperShot",
2799 DecodedFeat::ImprovedSniperShot { .. } => "ImprovedSniperShot",
2800 DecodedFeat::MasterSniperShot { .. } => "MasterSniperShot",
2801 DecodedFeat::RapidShot { .. } => "RapidShot",
2802 DecodedFeat::ImprovedRapidShot { .. } => "ImprovedRapidShot",
2803 DecodedFeat::MultiShot { .. } => "MultiShot",
2804 DecodedFeat::WhirlwindAttack { .. } => "WhirlwindAttack",
2805 DecodedFeat::TwoWeaponFighting { .. } => "TwoWeaponFighting",
2806 DecodedFeat::TwoWeaponAdvanced { .. } => "TwoWeaponAdvanced",
2807 DecodedFeat::TwoWeaponMastery { .. } => "TwoWeaponMastery",
2808 DecodedFeat::JediDefense { .. } => "JediDefense",
2809 DecodedFeat::AdvancedJediDefense { .. } => "AdvancedJediDefense",
2810 DecodedFeat::MasterJediDefense { .. } => "MasterJediDefense",
2811 DecodedFeat::ForceFocus { .. } => "ForceFocus",
2812 DecodedFeat::ForceFocusAdvanced { .. } => "ForceFocusAdvanced",
2813 DecodedFeat::ForceFocusMastery { .. } => "ForceFocusMastery",
2814 DecodedFeat::ForceFocusAlter { .. } => "ForceFocusAlter",
2815 DecodedFeat::ForceFocusControl { .. } => "ForceFocusControl",
2816 DecodedFeat::ForceJump { .. } => "ForceJump",
2817 DecodedFeat::ForceJumpAdvanced { .. } => "ForceJumpAdvanced",
2818 DecodedFeat::ForceJumpMastery { .. } => "ForceJumpMastery",
2819 DecodedFeat::ForceImmunityFear { .. } => "ForceImmunityFear",
2820 DecodedFeat::ForceImmunityStun { .. } => "ForceImmunityStun",
2821 DecodedFeat::ForceImmunityParalysis { .. } => "ForceImmunityParalysis",
2822 DecodedFeat::BattleMeditation { .. } => "BattleMeditation",
2823 DecodedFeat::ForceCamoflage { .. } => "ForceCamoflage",
2824 DecodedFeat::SneakAttack1D6 { .. } => "SneakAttack1D6",
2825 DecodedFeat::SneakAttack2D6 { .. } => "SneakAttack2D6",
2826 DecodedFeat::SneakAttack3D6 { .. } => "SneakAttack3D6",
2827 DecodedFeat::SneakAttack4D6 { .. } => "SneakAttack4D6",
2828 DecodedFeat::SneakAttack5D6 { .. } => "SneakAttack5D6",
2829 DecodedFeat::SneakAttack6D6 { .. } => "SneakAttack6D6",
2830 DecodedFeat::UncannyDodge1 { .. } => "UncannyDodge1",
2831 DecodedFeat::UncannyDodge2 { .. } => "UncannyDodge2",
2832 DecodedFeat::Toughness { .. } => "Toughness",
2833 DecodedFeat::ImprovedToughness { .. } => "ImprovedToughness",
2834 DecodedFeat::MasterToughness { .. } => "MasterToughness",
2835 DecodedFeat::Conditioning { .. } => "Conditioning",
2836 DecodedFeat::ImprovedConditioning { .. } => "ImprovedConditioning",
2837 DecodedFeat::MasterConditioning { .. } => "MasterConditioning",
2838 DecodedFeat::ImplantLevel1 { .. } => "ImplantLevel1",
2839 DecodedFeat::ImplantLevel2 { .. } => "ImplantLevel2",
2840 DecodedFeat::ImplantLevel3 { .. } => "ImplantLevel3",
2841 DecodedFeat::DroidUpgrade1 { .. } => "DroidUpgrade1",
2842 DecodedFeat::DroidUpgrade2 { .. } => "DroidUpgrade2",
2843 DecodedFeat::DroidUpgrade3 { .. } => "DroidUpgrade3",
2844 DecodedFeat::BlasterIntegration { .. } => "BlasterIntegration",
2845 DecodedFeat::LogicUpgradeCombat { .. } => "LogicUpgradeCombat",
2846 DecodedFeat::LogicUpgradeTactician { .. } => "LogicUpgradeTactician",
2847 DecodedFeat::Dueling { .. } => "Dueling",
2848 DecodedFeat::AdvancedDueling { .. } => "AdvancedDueling",
2849 DecodedFeat::MasterDueling { .. } => "MasterDueling",
2850 DecodedFeat::JediSense { .. } => "JediSense",
2851 DecodedFeat::KnightSense { .. } => "KnightSense",
2852 DecodedFeat::MasterSense { .. } => "MasterSense",
2853 DecodedFeat::GuardStance { .. } => "GuardStance",
2854 DecodedFeat::AdvancedGuardStance { .. } => "AdvancedGuardStance",
2855 DecodedFeat::MasterGuardStance { .. } => "MasterGuardStance",
2856 DecodedFeat::SkillFocusAwareness { .. } => "SkillFocusAwareness",
2857 DecodedFeat::SkillFocusTreatInjury { .. } => "SkillFocusTreatInjury",
2858 DecodedFeat::SkillFocusStealth { .. } => "SkillFocusStealth",
2859 DecodedFeat::Cautious { .. } => "Cautious",
2860 DecodedFeat::Perceptive { .. } => "Perceptive",
2861 DecodedFeat::Empathy { .. } => "Empathy",
2862 DecodedFeat::GearHead { .. } => "GearHead",
2863 DecodedFeat::WookieEndurance { .. } => "WookieEndurance",
2864 DecodedFeat::ScoundrelsLuck { .. } => "ScoundrelsLuck",
2865 DecodedFeat::Unknown { .. } => "<unknown>",
2866 }
2867 }
2868
2869 #[test]
2870 fn feats_dispatch_armour_proficiency_labels_to_typed_variants() {
2871 let utc = Utc {
2872 feats: vec![4, 5, 6],
2873 ..Utc::default()
2874 };
2875 let mut overrides = OverrideSource::new();
2876 add_2da_entry(&mut overrides, "feat", &weapon_feats_2da());
2877 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2878 let mut cache = TwoDaCache::new(&resolver);
2879
2880 let snapshot = utc.snapshot(&mut cache);
2881 let kinds: Vec<&str> = snapshot.feats().iter().map(snapshot_feat_kind).collect();
2882 assert_eq!(
2883 kinds,
2884 vec!["ArmourProfHeavy", "ArmourProfLight", "ArmourProfMedium"]
2885 );
2886 }
2887
2888 #[test]
2889 fn feats_dispatch_weapon_proficiency_labels_to_typed_variants() {
2890 let utc = Utc {
2893 feats: vec![39, 40, 41, 42, 43, 44, 45, 93],
2894 ..Utc::default()
2895 };
2896 let mut overrides = OverrideSource::new();
2897 add_2da_entry(&mut overrides, "feat", &weapon_feats_2da());
2898 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2899 let mut cache = TwoDaCache::new(&resolver);
2900
2901 let snapshot = utc.snapshot(&mut cache);
2902 let kinds: Vec<&str> = snapshot.feats().iter().map(snapshot_feat_kind).collect();
2903 assert_eq!(
2904 kinds,
2905 vec![
2906 "WeaponProfBlaster",
2907 "WeaponProfBlasterRifle",
2908 "WeaponProfGrenade",
2909 "WeaponProfHeavyWeapons",
2910 "WeaponProfLightsaber",
2911 "WeaponProfMeleeWeapons",
2912 "WeaponProfSimpleWeapons",
2913 "ProficiencyAll",
2914 ]
2915 );
2916 }
2917
2918 #[test]
2919 fn feats_dispatch_weapon_focus_labels_to_typed_variants() {
2920 let utc = Utc {
2921 feats: vec![32, 33, 34, 35, 36, 37, 38],
2922 ..Utc::default()
2923 };
2924 let mut overrides = OverrideSource::new();
2925 add_2da_entry(&mut overrides, "feat", &weapon_feats_2da());
2926 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
2927 let mut cache = TwoDaCache::new(&resolver);
2928
2929 let snapshot = utc.snapshot(&mut cache);
2930 let kinds: Vec<&str> = snapshot.feats().iter().map(snapshot_feat_kind).collect();
2931 assert_eq!(
2932 kinds,
2933 vec![
2934 "WeaponFocusBlaster",
2935 "WeaponFocusBlasterRifle",
2936 "WeaponFocusGrenade",
2937 "WeaponFocusHeavyWeapons",
2938 "WeaponFocusLightsaber",
2939 "WeaponFocusMeleeWeapons",
2940 "WeaponFocusSimpleWeapons",
2941 ]
2942 );
2943 }
2944
2945 fn combat_feats_2da() -> TwoDa {
2946 label_only_2da(&[
2947 (3, "TWO_WEAPON_FIGHTING"),
2948 (8, "CRITICAL_STRIKE"),
2949 (9, "TWO_WEAPON_ADVANCED"),
2950 (11, "FLURRY"),
2951 (17, "IMPROVED_POWER_ATTACK"),
2952 (18, "IMPROVED_POWER_BLAST"),
2953 (19, "IMPROVED_CRITICAL_STRIKE"),
2954 (20, "IMPROVED_SNIPER_SHOT"),
2955 (26, "MULTI_SHOT"),
2956 (28, "POWER_ATTACK"),
2957 (29, "POWER_BLAST"),
2958 (30, "RAPID_SHOT"),
2959 (31, "SNIPER_SHOT"),
2960 (53, "WHIRLWIND_ATTACK"),
2961 (77, "MASTER_SNIPER_SHOT"),
2962 (81, "MASTER_CRITICAL_STRIKE"),
2963 (82, "MASTER_POWER_BLAST"),
2964 (83, "MASTER_POWER_ATTACK"),
2965 (85, "TWO_WEAPON_MASTERY"),
2966 (91, "IMPROVED_FLURRY"),
2967 (92, "IMPROVED_RAPID_SHOT"),
2968 ])
2969 }
2970
2971 fn long_tail_feats_2da() -> TwoDa {
2972 label_only_2da(&[
2973 (2, "XXXX_ADVANCED_GUARD_STANCE"),
2974 (7, "CAUTIOUS"),
2975 (10, "EMPATHY"),
2976 (12, "GEAR_HEAD"),
2977 (25, "XXXX_MASTER_GUARD_STANCE"),
2978 (27, "XXXX_PERCEPTIVE"),
2979 (54, "XXXX_GUARD_STANCE"),
2980 (71, "XXXX_SKILL_FOCUS_STEALTH"),
2981 (72, "XXXX_SKILL_FOCUS_AWARENESS"),
2982 (76, "XXXX_SKILL_FOCUS_TREAT_INJURY"),
2983 (78, "DROID_UPGRADE_1"),
2984 (79, "DROID_UPGRADE_2"),
2985 (80, "DROID_UPGRADE_3"),
2986 (95, "WOOKIE_ENDURANCE"),
2987 (96, "BLASTER_INTEGRATION"),
2988 (104, "SCOUNDRELS_LUCK"),
2989 (107, "JEDI_SENSE"),
2990 (108, "KNIGHT_SENSE"),
2991 (109, "MASTER_SENSE"),
2992 (110, "LOGIC_UPGRADE_COMBAT"),
2993 (111, "LOGIC_UPGRADE_TACTICIAN"),
2994 (113, "DUELING"),
2995 (114, "ADVANCED_DUELING"),
2996 (115, "MASTER_DUELING"),
2997 ])
2998 }
2999
3000 #[test]
3001 fn feats_dispatch_long_tail_labels_to_typed_variants() {
3002 let utc = Utc {
3006 feats: vec![
3007 2, 7, 10, 12, 25, 27, 54, 71, 72, 76, 78, 79, 80, 95, 96, 104, 107, 108, 109, 110,
3008 111, 113, 114, 115,
3009 ],
3010 ..Utc::default()
3011 };
3012 let mut overrides = OverrideSource::new();
3013 add_2da_entry(&mut overrides, "feat", &long_tail_feats_2da());
3014 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3015 let mut cache = TwoDaCache::new(&resolver);
3016
3017 let snapshot = utc.snapshot(&mut cache);
3018 let kinds: Vec<&str> = snapshot.feats().iter().map(snapshot_feat_kind).collect();
3019 assert_eq!(
3020 kinds,
3021 vec![
3022 "AdvancedGuardStance",
3023 "Cautious",
3024 "Empathy",
3025 "GearHead",
3026 "MasterGuardStance",
3027 "Perceptive",
3028 "GuardStance",
3029 "SkillFocusStealth",
3030 "SkillFocusAwareness",
3031 "SkillFocusTreatInjury",
3032 "DroidUpgrade1",
3033 "DroidUpgrade2",
3034 "DroidUpgrade3",
3035 "WookieEndurance",
3036 "BlasterIntegration",
3037 "ScoundrelsLuck",
3038 "JediSense",
3039 "KnightSense",
3040 "MasterSense",
3041 "LogicUpgradeCombat",
3042 "LogicUpgradeTactician",
3043 "Dueling",
3044 "AdvancedDueling",
3045 "MasterDueling",
3046 ]
3047 );
3048 }
3049
3050 fn survival_feats_2da() -> TwoDa {
3051 label_only_2da(&[
3052 (13, "CONDITIONING"),
3053 (14, "IMPLANT_LEVEL_1"),
3054 (15, "IMPLANT_LEVEL_2"),
3055 (16, "IMPLANT_LEVEL_3"),
3056 (21, "IMPROVED_CONDITIONING"),
3057 (22, "MASTER_CONDITIONING"),
3058 (56, "UNCANNY_DODGE_1"),
3059 (57, "UNCANNY_DODGE_2"),
3060 (60, "SNEAK_ATTACK_1D6"),
3061 (61, "SNEAK_ATTACK_2D6"),
3062 (62, "SNEAK_ATTACK_3D6"),
3063 (63, "SNEAK_ATTACK_4D6"),
3064 (64, "SNEAK_ATTACK_5D6"),
3065 (65, "SNEAK_ATTACK_6D6"),
3066 (84, "TOUGHNESS"),
3067 (123, "IMPROVED_TOUGHNESS"),
3068 (124, "MASTER_TOUGHNESS"),
3069 ])
3070 }
3071
3072 #[test]
3073 fn feats_dispatch_survival_labels_to_typed_variants() {
3074 let utc = Utc {
3075 feats: vec![
3076 13, 14, 15, 16, 21, 22, 56, 57, 60, 61, 62, 63, 64, 65, 84, 123, 124,
3077 ],
3078 ..Utc::default()
3079 };
3080 let mut overrides = OverrideSource::new();
3081 add_2da_entry(&mut overrides, "feat", &survival_feats_2da());
3082 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3083 let mut cache = TwoDaCache::new(&resolver);
3084
3085 let snapshot = utc.snapshot(&mut cache);
3086 let kinds: Vec<&str> = snapshot.feats().iter().map(snapshot_feat_kind).collect();
3087 assert_eq!(
3088 kinds,
3089 vec![
3090 "Conditioning",
3091 "ImplantLevel1",
3092 "ImplantLevel2",
3093 "ImplantLevel3",
3094 "ImprovedConditioning",
3095 "MasterConditioning",
3096 "UncannyDodge1",
3097 "UncannyDodge2",
3098 "SneakAttack1D6",
3099 "SneakAttack2D6",
3100 "SneakAttack3D6",
3101 "SneakAttack4D6",
3102 "SneakAttack5D6",
3103 "SneakAttack6D6",
3104 "Toughness",
3105 "ImprovedToughness",
3106 "MasterToughness",
3107 ]
3108 );
3109 }
3110
3111 fn force_feats_2da() -> TwoDa {
3112 label_only_2da(&[
3113 (1, "ADVANCED_JEDI_DEFENSE"),
3114 (24, "MASTER_JEDI_DEFENSE"),
3115 (55, "JEDI_DEFENSE"),
3116 (86, "XXXX_FORCE_FOCUS_ALTER"),
3117 (87, "XXXX_FORCE_FOCUS_CONTROL"),
3118 (88, "FORCE_FOCUS"),
3119 (89, "FORCE_FOCUS_ADVANCED"),
3120 (90, "FORCE_FOCUS_MASTERY"),
3121 (94, "BATTLE_MEDITATION"),
3122 (97, "FORCE_CAMOFLAGE"),
3123 (98, "FORCE_IMMUNITY_FEAR"),
3124 (99, "FORCE_IMMUNITY_STUN"),
3125 (100, "FORCE_IMMUNITY_PARALYSIS"),
3126 (101, "FORCE_JUMP"),
3127 (102, "FORCE_JUMP_ADVANCED"),
3128 (103, "FORCE_JUMP_MASTERY"),
3129 ])
3130 }
3131
3132 #[test]
3133 fn feats_dispatch_force_jedi_labels_to_typed_variants() {
3134 let utc = Utc {
3135 feats: vec![
3136 1, 24, 55, 86, 87, 88, 89, 90, 94, 97, 98, 99, 100, 101, 102, 103,
3137 ],
3138 ..Utc::default()
3139 };
3140 let mut overrides = OverrideSource::new();
3141 add_2da_entry(&mut overrides, "feat", &force_feats_2da());
3142 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3143 let mut cache = TwoDaCache::new(&resolver);
3144
3145 let snapshot = utc.snapshot(&mut cache);
3146 let kinds: Vec<&str> = snapshot.feats().iter().map(snapshot_feat_kind).collect();
3147 assert_eq!(
3148 kinds,
3149 vec![
3150 "AdvancedJediDefense",
3151 "MasterJediDefense",
3152 "JediDefense",
3153 "ForceFocusAlter",
3154 "ForceFocusControl",
3155 "ForceFocus",
3156 "ForceFocusAdvanced",
3157 "ForceFocusMastery",
3158 "BattleMeditation",
3159 "ForceCamoflage",
3160 "ForceImmunityFear",
3161 "ForceImmunityStun",
3162 "ForceImmunityParalysis",
3163 "ForceJump",
3164 "ForceJumpAdvanced",
3165 "ForceJumpMastery",
3166 ]
3167 );
3168 }
3169
3170 #[test]
3171 fn feats_dispatch_combat_ladders_to_typed_variants() {
3172 let utc = Utc {
3175 feats: vec![
3176 3, 8, 9, 11, 17, 18, 19, 20, 26, 28, 29, 30, 31, 53, 77, 81, 82, 83, 85, 91, 92,
3177 ],
3178 ..Utc::default()
3179 };
3180 let mut overrides = OverrideSource::new();
3181 add_2da_entry(&mut overrides, "feat", &combat_feats_2da());
3182 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3183 let mut cache = TwoDaCache::new(&resolver);
3184
3185 let snapshot = utc.snapshot(&mut cache);
3186 let kinds: Vec<&str> = snapshot.feats().iter().map(snapshot_feat_kind).collect();
3187 assert_eq!(
3188 kinds,
3189 vec![
3190 "TwoWeaponFighting",
3191 "CriticalStrike",
3192 "TwoWeaponAdvanced",
3193 "Flurry",
3194 "ImprovedPowerAttack",
3195 "ImprovedPowerBlast",
3196 "ImprovedCriticalStrike",
3197 "ImprovedSniperShot",
3198 "MultiShot",
3199 "PowerAttack",
3200 "PowerBlast",
3201 "RapidShot",
3202 "SniperShot",
3203 "WhirlwindAttack",
3204 "MasterSniperShot",
3205 "MasterCriticalStrike",
3206 "MasterPowerBlast",
3207 "MasterPowerAttack",
3208 "TwoWeaponMastery",
3209 "ImprovedFlurry",
3210 "ImprovedRapidShot",
3211 ]
3212 );
3213 }
3214
3215 #[test]
3216 fn feats_dispatch_weapon_specialization_labels_to_typed_variants() {
3217 let utc = Utc {
3218 feats: vec![46, 47, 48, 49, 50, 51, 52],
3219 ..Utc::default()
3220 };
3221 let mut overrides = OverrideSource::new();
3222 add_2da_entry(&mut overrides, "feat", &weapon_feats_2da());
3223 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3224 let mut cache = TwoDaCache::new(&resolver);
3225
3226 let snapshot = utc.snapshot(&mut cache);
3227 let kinds: Vec<&str> = snapshot.feats().iter().map(snapshot_feat_kind).collect();
3228 assert_eq!(
3229 kinds,
3230 vec![
3231 "WeaponSpecBlaster",
3232 "WeaponSpecBlasterRifle",
3233 "WeaponSpecGrenade",
3234 "WeaponSpecHeavyWeapons",
3235 "WeaponSpecLightsaber",
3236 "WeaponSpecMeleeWeapons",
3237 "WeaponSpecSimpleWeapons",
3238 ]
3239 );
3240 }
3241
3242 #[test]
3243 fn feats_dispatch_by_label_so_mod_renumbered_feat_still_routes() {
3244 let utc = Utc {
3249 feats: vec![500],
3250 ..Utc::default()
3251 };
3252 let feat = label_only_2da(&[(500, "ARMOUR_PROF_LIGHT")]);
3253 let mut overrides = OverrideSource::new();
3254 add_2da_entry(&mut overrides, "feat", &feat);
3255 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3256 let mut cache = TwoDaCache::new(&resolver);
3257
3258 let snapshot = utc.snapshot(&mut cache);
3259 match &snapshot.feats()[0] {
3260 DecodedFeat::ArmourProfLight { feat_id } => {
3261 assert_eq!(*feat_id, 500);
3264 }
3265 other => panic!("expected ArmourProfLight, got {other:?}"),
3266 }
3267 }
3268
3269 #[test]
3270 fn feat_with_unknown_label_surfaces_as_unknown_carrying_resolved_label() {
3271 let utc = Utc {
3274 feats: vec![5000],
3275 ..Utc::default()
3276 };
3277 let feat = label_only_2da(&[(5000, "MOD_MYSTERY_FEAT")]);
3278 let mut overrides = OverrideSource::new();
3279 add_2da_entry(&mut overrides, "feat", &feat);
3280 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3281 let mut cache = TwoDaCache::new(&resolver);
3282
3283 let snapshot = utc.snapshot(&mut cache);
3284 match &snapshot.feats()[0] {
3285 DecodedFeat::Unknown {
3286 feat_id,
3287 feat_label,
3288 } => {
3289 assert_eq!(*feat_id, 5000);
3290 assert_eq!(feat_label.as_deref(), Some("MOD_MYSTERY_FEAT"));
3291 }
3292 other => panic!("expected Unknown, got {other:?}"),
3293 }
3294 }
3295
3296 #[test]
3297 fn feat_decodes_to_unknown_with_no_label_when_id_out_of_range() {
3298 let utc = Utc {
3301 feats: vec![99],
3302 ..Utc::default()
3303 };
3304 let feat = label_only_2da(&[(0, ""), (1, "ADVANCED_JEDI_DEFENSE")]);
3305 let mut overrides = OverrideSource::new();
3306 add_2da_entry(&mut overrides, "feat", &feat);
3307 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3308 let mut cache = TwoDaCache::new(&resolver);
3309
3310 let snapshot = utc.snapshot(&mut cache);
3311 match &snapshot.feats()[0] {
3312 DecodedFeat::Unknown {
3313 feat_id,
3314 feat_label,
3315 } => {
3316 assert_eq!(*feat_id, 99);
3317 assert!(feat_label.is_none());
3318 }
3319 other => panic!("expected Unknown, got {other:?}"),
3320 }
3321 }
3322
3323 #[test]
3324 fn feat_decodes_to_unknown_with_no_label_when_feat_2da_unavailable() {
3325 let utc = Utc {
3329 feats: vec![5, 93],
3330 ..Utc::default()
3331 };
3332 let resolver = Resolver::new();
3333 let mut cache = TwoDaCache::new(&resolver);
3334
3335 let snapshot = utc.snapshot(&mut cache);
3336 for entry in snapshot.feats() {
3337 match entry {
3338 DecodedFeat::Unknown { feat_label, .. } => assert!(feat_label.is_none()),
3339 other => panic!("expected Unknown without label, got {other:?}"),
3340 }
3341 }
3342 }
3343
3344 #[test]
3345 fn empty_feat_list_decodes_to_empty_slice() {
3346 let utc = Utc::default();
3347 let resolver = Resolver::new();
3348 let mut cache = TwoDaCache::new(&resolver);
3349
3350 let snapshot = utc.snapshot(&mut cache);
3351 assert!(snapshot.feats().is_empty());
3352 }
3353
3354 #[test]
3355 fn feat_id_accessor_returns_raw_id_regardless_of_variant() {
3356 let unknown = DecodedFeat::Unknown {
3357 feat_id: 84,
3358 feat_label: Some("TOUGHNESS".to_string()),
3359 };
3360 assert_eq!(unknown.feat_id(), 84);
3361 let typed = DecodedFeat::ArmourProfLight { feat_id: 5 };
3362 assert_eq!(typed.feat_id(), 5);
3363 }
3364
3365 #[test]
3366 fn feats_preserve_stacked_duplicates_as_typed_variant_slots() {
3367 let utc = Utc {
3370 feats: vec![5, 5, 5],
3371 ..Utc::default()
3372 };
3373 let mut overrides = OverrideSource::new();
3374 add_2da_entry(&mut overrides, "feat", &weapon_feats_2da());
3375 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3376 let mut cache = TwoDaCache::new(&resolver);
3377
3378 let snapshot = utc.snapshot(&mut cache);
3379 assert_eq!(snapshot.feats().len(), 3);
3380 for entry in snapshot.feats() {
3381 assert!(matches!(entry, DecodedFeat::ArmourProfLight { feat_id: 5 }));
3382 }
3383 }
3384
3385 #[test]
3386 fn one_projection_yields_diverging_feat_dispatch_per_scope() {
3387 let utc = Utc {
3392 feats: vec![5],
3393 ..Utc::default()
3394 };
3395
3396 let mut vanilla_overrides = OverrideSource::new();
3397 add_2da_entry(&mut vanilla_overrides, "feat", &weapon_feats_2da());
3398 let vanilla_resolver =
3399 Resolver::new().with_source(ResolverSourceRef::Override(&vanilla_overrides));
3400
3401 let mod_feat = label_only_2da(&[(5, "MOD_FIBER_ARMOUR")]);
3402 let mut mod_overrides = OverrideSource::new();
3403 add_2da_entry(&mut mod_overrides, "feat", &mod_feat);
3404 let mod_resolver = Resolver::new().with_source(ResolverSourceRef::Override(&mod_overrides));
3405
3406 let projection = utc.project();
3407 let mut vanilla_cache = TwoDaCache::new(&vanilla_resolver);
3408 let mut mod_cache = TwoDaCache::new(&mod_resolver);
3409
3410 let vanilla = projection.snapshot(&mut vanilla_cache);
3411 let modded = projection.snapshot(&mut mod_cache);
3412
3413 assert!(matches!(
3414 vanilla.feats()[0],
3415 DecodedFeat::ArmourProfLight { feat_id: 5 }
3416 ));
3417 match &modded.feats()[0] {
3418 DecodedFeat::Unknown {
3419 feat_id,
3420 feat_label,
3421 } => {
3422 assert_eq!(*feat_id, 5);
3423 assert_eq!(feat_label.as_deref(), Some("MOD_FIBER_ARMOUR"));
3424 }
3425 other => panic!("expected Unknown, got {other:?}"),
3426 }
3427 }
3428
3429 #[test]
3432 fn has_feat_matches_each_family_filter() {
3433 let mut overrides = OverrideSource::new();
3436 let combined = label_only_2da(&[
3439 (5, "ARMOUR_PROF_LIGHT"),
3440 (28, "POWER_ATTACK"),
3441 (39, "WEAPON_PROF_BLASTER"),
3442 (32, "WEAPON_FOCUS_BLASTER"),
3443 (46, "WEAPON_SPEC_BLASTER"),
3444 (29, "POWER_BLAST"),
3445 (8, "CRITICAL_STRIKE"),
3446 (11, "FLURRY"),
3447 (31, "SNIPER_SHOT"),
3448 (30, "RAPID_SHOT"),
3449 (3, "TWO_WEAPON_FIGHTING"),
3450 (55, "JEDI_DEFENSE"),
3451 (88, "FORCE_FOCUS"),
3452 (101, "FORCE_JUMP"),
3453 (98, "FORCE_IMMUNITY_FEAR"),
3454 (60, "SNEAK_ATTACK_1D6"),
3455 (56, "UNCANNY_DODGE_1"),
3456 (84, "TOUGHNESS"),
3457 (13, "CONDITIONING"),
3458 (14, "IMPLANT_LEVEL_1"),
3459 (78, "DROID_UPGRADE_1"),
3460 (113, "DUELING"),
3461 (107, "JEDI_SENSE"),
3462 (54, "XXXX_GUARD_STANCE"),
3463 (72, "XXXX_SKILL_FOCUS_AWARENESS"),
3464 ]);
3465 add_2da_entry(&mut overrides, "feat", &combined);
3466 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3467
3468 let utc = Utc {
3469 feats: vec![
3470 5, 28, 39, 32, 46, 29, 8, 11, 31, 30, 3, 55, 88, 101, 98, 60, 56, 84, 13, 14, 78,
3471 113, 107, 54, 72,
3472 ],
3473 ..Utc::default()
3474 };
3475 let mut cache = TwoDaCache::new(&resolver);
3476 let snapshot = utc.snapshot(&mut cache);
3477
3478 let cases = [
3479 (FeatKindFilter::AnyArmourProficiency, true),
3480 (FeatKindFilter::AnyWeaponProficiency, true),
3481 (FeatKindFilter::AnyWeaponFocus, true),
3482 (FeatKindFilter::AnyWeaponSpecialization, true),
3483 (FeatKindFilter::AnyPowerAttack, true),
3484 (FeatKindFilter::AnyPowerBlast, true),
3485 (FeatKindFilter::AnyCriticalStrike, true),
3486 (FeatKindFilter::AnyFlurry, true),
3487 (FeatKindFilter::AnySniperShot, true),
3488 (FeatKindFilter::AnyRapidShot, true),
3489 (FeatKindFilter::AnyTwoWeapon, true),
3490 (FeatKindFilter::AnyJediDefense, true),
3491 (FeatKindFilter::AnyForceFocus, true),
3492 (FeatKindFilter::AnyForceJump, true),
3493 (FeatKindFilter::AnyForceImmunity, true),
3494 (FeatKindFilter::AnySneakAttack, true),
3495 (FeatKindFilter::AnyUncannyDodge, true),
3496 (FeatKindFilter::AnyToughness, true),
3497 (FeatKindFilter::AnyConditioning, true),
3498 (FeatKindFilter::AnyImplant, true),
3499 (FeatKindFilter::AnyDroidUpgrade, true),
3500 (FeatKindFilter::AnyDueling, true),
3501 (FeatKindFilter::AnySense, true),
3502 (FeatKindFilter::AnyGuardStance, true),
3503 (FeatKindFilter::AnySkillFocus, true),
3504 ];
3505 for (filter, expected) in cases {
3506 assert_eq!(
3507 snapshot.has_feat(filter),
3508 expected,
3509 "filter {filter:?} expected {expected}"
3510 );
3511 }
3512 }
3513
3514 #[test]
3515 fn has_feat_returns_false_for_empty_feat_list() {
3516 let utc = Utc::default();
3517 let resolver = Resolver::new();
3518 let mut cache = TwoDaCache::new(&resolver);
3519 let snapshot = utc.snapshot(&mut cache);
3520 for filter in [
3521 FeatKindFilter::AnyArmourProficiency,
3522 FeatKindFilter::AnyPowerAttack,
3523 FeatKindFilter::AnyForceJump,
3524 FeatKindFilter::AnySneakAttack,
3525 ] {
3526 assert!(!snapshot.has_feat(filter));
3527 }
3528 }
3529
3530 #[test]
3531 fn has_feat_never_matches_unknown_entries() {
3532 let utc = Utc {
3536 feats: vec![5000],
3537 ..Utc::default()
3538 };
3539 let feat = label_only_2da(&[(5000, "MOD_POWER_ATTACK_REWORK")]);
3540 let mut overrides = OverrideSource::new();
3541 add_2da_entry(&mut overrides, "feat", &feat);
3542 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3543 let mut cache = TwoDaCache::new(&resolver);
3544 let snapshot = utc.snapshot(&mut cache);
3545
3546 assert!(matches!(snapshot.feats()[0], DecodedFeat::Unknown { .. }));
3547 assert!(!snapshot.has_feat(FeatKindFilter::AnyPowerAttack));
3548 assert!(!snapshot.has_feat(FeatKindFilter::AnyArmourProficiency));
3549 }
3550
3551 #[test]
3552 fn has_feat_distinguishes_specific_ladder_from_neighbour() {
3553 let utc = Utc {
3557 feats: vec![28],
3558 ..Utc::default()
3559 };
3560 let feat = label_only_2da(&[(28, "POWER_ATTACK")]);
3561 let mut overrides = OverrideSource::new();
3562 add_2da_entry(&mut overrides, "feat", &feat);
3563 let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&overrides));
3564 let mut cache = TwoDaCache::new(&resolver);
3565 let snapshot = utc.snapshot(&mut cache);
3566
3567 assert!(snapshot.has_feat(FeatKindFilter::AnyPowerAttack));
3568 assert!(!snapshot.has_feat(FeatKindFilter::AnyPowerBlast));
3569 assert!(!snapshot.has_feat(FeatKindFilter::AnyCriticalStrike));
3570 assert!(!snapshot.has_feat(FeatKindFilter::AnyForceJump));
3571 }
3572}