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, } #[derive(Debug, Deserialize)] struct TableRow { roll: String, steps: Vec, } #[derive(Debug, Deserialize)] struct TableRowStep { class: Option, table: Option, text: Option, } // // 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(), // } // } // } pub struct RandomTables { tables: HashMap, } const YAML_DIR: Dir = include_dir!("src/data/random_tables/"); impl RandomTables { pub fn new() -> Result> { let mut tables_yaml = String::new(); for entry in YAML_DIR.files() { tables_yaml.push_str(entry.contents_utf8().unwrap()); } let tables: HashMap = 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::() { Some((num, num)) } else { None } } 2 => { if let (Ok(start), Ok(end)) = (parts[0].parse::(), parts[1].parse::()) { Some((start, end)) } else { None } } _ => None, } } }