summaryrefslogtreecommitdiff
path: root/src/random_tables.rs
blob: d059fd5a83307ee3353769d26ae8f6ecbe3ed7aa (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
use crate::dice;
use crate::rules::classes::Classes;
use include_dir::{include_dir, Dir};
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>,
}

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

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

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
    ///
    /// ```
    /// 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 classes = Classes::new().unwrap();
                                let class = classes.class(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,
        }
    }
}