dmn 0.0.2

CLI and library for DMs and other players of Advanced Dungeons & Dragons (AD&D)
Documentation
use crate::dice::roll_formula;
use crate::random_tables::RANDOM_TABLES;
use crate::rules::ability_scores::{AbilityScore, AbilityScoreCollection};
use crate::rules::classes::Class;
use crate::rules::races::Race;
use log::debug;
use std::collections::HashMap;
// use std::fmt;

pub struct Npc {
    pub alignment: Option<String>,
    pub race: Option<&'static Race>,
    pub class: Option<&'static Class>,
    pub ability_scores: Option<AbilityScoreCollection>,
    pub persona: Option<String>,
}

impl Npc {
    pub fn new(
        alignment: Option<String>,
        race: Option<&'static Race>,
        class: Option<&'static Class>,
        ability_scores: Option<AbilityScoreCollection>,
        persona: Option<String>,
    ) -> Self {
        Npc {
            alignment,
            race,
            class,
            ability_scores,
            persona,
        }
    }

    pub fn roll_henchman_ability_scores(&mut self) {
        rand::thread_rng();
        let mut ability_score_rolls: HashMap<AbilityScore, i32> = HashMap::new();

        for &ability in &[
            AbilityScore::Strength,
            AbilityScore::Intelligence,
            AbilityScore::Wisdom,
            AbilityScore::Dexterity,
            AbilityScore::Constitution,
            AbilityScore::Charisma,
        ] {
            debug!("Generating score for {}", &ability.abbr());
            // Roll 3d6 down the line.
            let mut roll_result = roll_formula("3d6").unwrap();
            debug!("Rolled {}", roll_result.total());
            let class_ref = self.class.unwrap();
            let race_ref = self.race.unwrap();

            // For ability scores which are prime requisites of the class,
            // increase all dice by 1 which do not already show a 6.
            if class_ref.prime_requisites.contains(&ability) {
                roll_result.increase_sides_below_max(6, 1);
            }
            debug!("Bumped prime requisites, now at {}", roll_result.total());

            // At this point we don't need the individual dice anymore.
            let mut total = roll_result.total() as i32;

            // Add NPC-specific class ability score modifiers.
            total += class_ref
                .npc_ability_score_modifiers
                .get(&ability)
                .copied()
                .unwrap_or(0);
            debug!("After adding NPC class modifiers, now at {}", total);

            // Add racial ability score modifiers.
            total += race_ref
                .npc_ability_score_modifiers
                .get(&ability)
                .copied()
                .unwrap_or(0);
            debug!("After adding racial modifiers, now at {}", total);

            // Ensure racial ability score limits are imposed.
            // TODO: Use u8 for all of these, so no conversion from u32 will be needed.
            let [min_score, max_score] = race_ref.ability_score_ranges.get(&ability).unwrap().male;
            total = total.max(min_score as i32);
            total = total.min(max_score as i32);

            ability_score_rolls.insert(ability, total);
        }

        // Create an AbilityScoreCollection for the Npc, using the above results.
        let mut score_collection = AbilityScoreCollection::new();
        for (ability, value) in &ability_score_rolls {
            score_collection.add_score(*ability, *value);
        }

        self.ability_scores = Some(score_collection);

        // TODO: Verify legality of class based on alignment.
        // TODO: Verify legality of class based on race.
        // TODO: Verify legality of class based on ability scores.
    }

    pub fn randomize_persona(&mut self) {
        let appearance = RANDOM_TABLES
            .roll_table("npc_general_appearance")
            .to_string();
        let tendencies = RANDOM_TABLES
            .roll_table("npc_general_tendencies")
            .to_string();
        let personality = RANDOM_TABLES.roll_table("npc_personality").to_string();
        let disposition = RANDOM_TABLES.roll_table("npc_disposition").to_string();
        let components = vec![appearance, tendencies, personality, disposition];
        self.persona = Some(components.join(", "));
    }

    // TODO: Probably break this out later like this.
    // fn increase_prime_requisites(&mut self, roll_result: &mut RollResult) {
    //     let class_ref = self.class.unwrap();
    //
    //     // For ability scores which are prime requisites of the class,
    //     // increase all dice by 1 which do not already show a 6.
    //     if class_ref.prime_requisites.contains(&ability) {
    //         roll_result.increase_sides_below_max(6, 1);
    //     }
    //     roll_result.increase_sides_below_max(6, 1);
    // }
}

// impl fmt::Display for Npc {
//     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
//         let values: Vec<&str> = vec![
//             self.alignment.as_deref().unwrap_or(""),
//             self.race.as_deref().unwrap_or(""),
//             self.class.as_ref().map_or("", |class| &class.name),
//         ]
//         .into_iter()
//         .filter(|&s| !s.is_empty())
//         .collect();
//
//         let formatted_string = values.join(" ");
//         write!(f, "{}", formatted_string)
//     }
// }

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rules::classes::CLASSES;
    use crate::rules::races::RACES;

    #[test]
    #[ignore]
    #[should_panic(expected = "Ability score generation isn't testable yet.")]
    fn test_roll_henchman_ability_scores() {
        let class_ref = CLASSES.get("fighter").unwrap();
        let race_ref = RACES.get("dwarf").unwrap();
        let mut npc = Npc {
            alignment: Some(String::from("Lawful Good")),
            race: Some(race_ref),
            class: Some(class_ref),
            ability_scores: None,
            persona: None,
        };

        // Roll ability scores for the Npc.
        npc.roll_henchman_ability_scores();

        // TODO: Need to actually test this process.
        // Check if ability scores are modified correctly based on class requirements and modifiers.
        // let ability_scores = npc.ability_scores.unwrap();
    }
}