dmn/rules/
npcs.rs

1use crate::dice::roll_formula;
2use crate::random_tables::RANDOM_TABLES;
3use crate::rules::ability_scores::{AbilityScore, AbilityScoreCollection};
4use crate::rules::classes::Class;
5use crate::rules::races::Race;
6use log::debug;
7use std::collections::HashMap;
8// use std::fmt;
9
10pub struct Npc {
11    pub alignment: Option<String>,
12    pub race: Option<&'static Race>,
13    pub class: Option<&'static Class>,
14    pub ability_scores: Option<AbilityScoreCollection>,
15    pub persona: Option<String>,
16}
17
18impl Npc {
19    pub fn new(
20        alignment: Option<String>,
21        race: Option<&'static Race>,
22        class: Option<&'static Class>,
23        ability_scores: Option<AbilityScoreCollection>,
24        persona: Option<String>,
25    ) -> Self {
26        Npc {
27            alignment,
28            race,
29            class,
30            ability_scores,
31            persona,
32        }
33    }
34
35    pub fn roll_henchman_ability_scores(&mut self) {
36        rand::thread_rng();
37        let mut ability_score_rolls: HashMap<AbilityScore, i32> = HashMap::new();
38
39        for &ability in &[
40            AbilityScore::Strength,
41            AbilityScore::Intelligence,
42            AbilityScore::Wisdom,
43            AbilityScore::Dexterity,
44            AbilityScore::Constitution,
45            AbilityScore::Charisma,
46        ] {
47            debug!("Generating score for {}", &ability.abbr());
48            // Roll 3d6 down the line.
49            let mut roll_result = roll_formula("3d6").unwrap();
50            debug!("Rolled {}", roll_result.total());
51            let class_ref = self.class.unwrap();
52            let race_ref = self.race.unwrap();
53
54            // For ability scores which are prime requisites of the class,
55            // increase all dice by 1 which do not already show a 6.
56            if class_ref.prime_requisites.contains(&ability) {
57                roll_result.increase_sides_below_max(6, 1);
58            }
59            debug!("Bumped prime requisites, now at {}", roll_result.total());
60
61            // At this point we don't need the individual dice anymore.
62            let mut total = roll_result.total() as i32;
63
64            // Add NPC-specific class ability score modifiers.
65            total += class_ref
66                .npc_ability_score_modifiers
67                .get(&ability)
68                .copied()
69                .unwrap_or(0);
70            debug!("After adding NPC class modifiers, now at {}", total);
71
72            // Add racial ability score modifiers.
73            total += race_ref
74                .npc_ability_score_modifiers
75                .get(&ability)
76                .copied()
77                .unwrap_or(0);
78            debug!("After adding racial modifiers, now at {}", total);
79
80            // Ensure racial ability score limits are imposed.
81            // TODO: Use u8 for all of these, so no conversion from u32 will be needed.
82            let [min_score, max_score] = race_ref.ability_score_ranges.get(&ability).unwrap().male;
83            total = total.max(min_score as i32);
84            total = total.min(max_score as i32);
85
86            ability_score_rolls.insert(ability, total);
87        }
88
89        // Create an AbilityScoreCollection for the Npc, using the above results.
90        let mut score_collection = AbilityScoreCollection::new();
91        for (ability, value) in &ability_score_rolls {
92            score_collection.add_score(*ability, *value);
93        }
94
95        self.ability_scores = Some(score_collection);
96
97        // TODO: Verify legality of class based on alignment.
98        // TODO: Verify legality of class based on race.
99        // TODO: Verify legality of class based on ability scores.
100    }
101
102    pub fn randomize_persona(&mut self) {
103        let appearance = RANDOM_TABLES
104            .roll_table("npc_general_appearance")
105            .to_string();
106        let tendencies = RANDOM_TABLES
107            .roll_table("npc_general_tendencies")
108            .to_string();
109        let personality = RANDOM_TABLES.roll_table("npc_personality").to_string();
110        let disposition = RANDOM_TABLES.roll_table("npc_disposition").to_string();
111        let components = vec![appearance, tendencies, personality, disposition];
112        self.persona = Some(components.join(", "));
113    }
114
115    // TODO: Probably break this out later like this.
116    // fn increase_prime_requisites(&mut self, roll_result: &mut RollResult) {
117    //     let class_ref = self.class.unwrap();
118    //
119    //     // For ability scores which are prime requisites of the class,
120    //     // increase all dice by 1 which do not already show a 6.
121    //     if class_ref.prime_requisites.contains(&ability) {
122    //         roll_result.increase_sides_below_max(6, 1);
123    //     }
124    //     roll_result.increase_sides_below_max(6, 1);
125    // }
126}
127
128// impl fmt::Display for Npc {
129//     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130//         let values: Vec<&str> = vec![
131//             self.alignment.as_deref().unwrap_or(""),
132//             self.race.as_deref().unwrap_or(""),
133//             self.class.as_ref().map_or("", |class| &class.name),
134//         ]
135//         .into_iter()
136//         .filter(|&s| !s.is_empty())
137//         .collect();
138//
139//         let formatted_string = values.join(" ");
140//         write!(f, "{}", formatted_string)
141//     }
142// }
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::rules::classes::CLASSES;
148    use crate::rules::races::RACES;
149
150    #[test]
151    #[ignore]
152    #[should_panic(expected = "Ability score generation isn't testable yet.")]
153    fn test_roll_henchman_ability_scores() {
154        let class_ref = CLASSES.get("fighter").unwrap();
155        let race_ref = RACES.get("dwarf").unwrap();
156        let mut npc = Npc {
157            alignment: Some(String::from("Lawful Good")),
158            race: Some(race_ref),
159            class: Some(class_ref),
160            ability_scores: None,
161            persona: None,
162        };
163
164        // Roll ability scores for the Npc.
165        npc.roll_henchman_ability_scores();
166
167        // TODO: Need to actually test this process.
168        // Check if ability scores are modified correctly based on class requirements and modifiers.
169        // let ability_scores = npc.ability_scores.unwrap();
170    }
171}