summaryrefslogtreecommitdiff
path: root/src/random_tables.rs
blob: 9116659b9289d71f4dbd980039a4532a439370be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use crate::dice;
use crate::rules::classes::CLASSES;
use include_dir::{include_dir, Dir};
use lazy_static::lazy_static;
use serde::Deserialize;
use serde_yaml;
use std::collections::HashMap;
use std::error::Error;
use std::string::String;

#[derive(Debug, Deserialize)]
struct RandomTable {
    formula: String,
    rows: Vec<TableRow>,
}

#[derive(Debug, Deserialize)]
struct TableRow {
    roll: String,
    steps: Vec<TableRowStep>,
}

#[derive(Debug, Deserialize)]
struct TableRowStep {
    class: Option<String>,
    table: Option<String>,
    text: Option<String>,
}
//
// enum TableOutcome {
//     Text(String),
//     Class(Class),
// }
//
// impl TableOutcome {
//     fn to_string(&self) -> String {
//         match self {
//             TableOutcome::Text(text) => text.clone(),
//             TableOutcome::Class(class) => class.name.clone(),
//         }
//     }
// }

lazy_static! {
    pub static ref RANDOM_TABLES: RandomTables =
        RandomTables::new().expect("Failed to load random tables.");
}

const YAML_DIR: Dir = include_dir!("src/data/random_tables/");

pub struct RandomTables {
    tables: HashMap<String, RandomTable>,
}

impl RandomTables {
    pub fn new() -> Result<Self, Box<dyn Error>> {
        let mut tables_yaml = String::new();
        for entry in YAML_DIR.files() {
            tables_yaml.push_str(entry.contents_utf8().unwrap());
        }

        let tables: HashMap<String, RandomTable> = serde_yaml::from_str(tables_yaml.as_str())?;
        Ok(RandomTables { tables })
    }

    /// Rolls on a given random table.
    ///
    /// # Examples
    ///
    /// ```
    /// let random_tables = dmn::random_tables::RandomTables::new().unwrap();
    /// random_tables.roll_table("npc_alignment");
    /// ```
    pub fn roll_table(&self, table_name: &str) -> String {
        let random_table = self.tables.get(table_name);
        if let Some(table) = random_table {
            // TODO: Probably return an error instead of using #unwrap.
            let roll_result = dice::roll_formula(&table.formula).unwrap();
            for table_row in &table.rows {
                if let Some((start, end)) = RandomTables::parse_roll(&table_row.roll) {
                    if start <= roll_result.total() && roll_result.total() <= end {
                        let mut output_text = String::new();
                        for step in &table_row.steps {
                            if let Some(text) = &step.text {
                                output_text.push_str(text);
                            }

                            if let Some(table) = &step.table {
                                let inner_output = self.roll_table(table);
                                output_text.push_str(&inner_output);
                            }

                            if let Some(class_string) = &step.class {
                                let class =
                                    CLASSES.get(class_string).expect("Failed to load class.");
                                output_text.push_str(&*class.name);
                            }

                            output_text.push_str("\n");
                        }
                        return output_text.trim().replace("\n", ", ").to_string();
                    }
                } else {
                    panic!("Invalid roll format in yaml")
                }
            }
        }
        String::new()
    }

    /// Parses a roll string in the random tables yaml.
    // TODO: Add validator somewhere that ensures all possible rolls are
    //  covered by the YAML, and that none overlap.
    fn parse_roll(roll: &str) -> Option<(u32, u32)> {
        let parts: Vec<&str> = roll.split('-').collect();
        match parts.len() {
            1 => {
                if let Ok(num) = parts[0].parse::<u32>() {
                    Some((num, num))
                } else {
                    None
                }
            }
            2 => {
                if let (Ok(start), Ok(end)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
                    Some((start, end))
                } else {
                    None
                }
            }
            _ => None,
        }
    }
}