#![allow(dead_code, unused_must_use)] // TODO: there is some structure duplication that occurs here: // the rare and box tables instantiate their own copies of the // generic drop tables as they need them to apply their modifiers // to their drops pub mod rare_drop_table; mod generic_weapon; mod generic_armor; mod generic_shield; mod generic_unit; mod tool_table; mod tech_table; mod box_drop_table; use std::collections::HashMap; use std::fs::File; use std::path::PathBuf; use std::io::Read; use serde::{Serialize, Deserialize}; use rand::{Rng, SeedableRng}; use maps::monster::MonsterType; use maps::room::{Difficulty, Episode}; use maps::area::MapArea; use entity::character::SectionID; use crate::generic_weapon::GenericWeaponTable; use crate::generic_armor::GenericArmorTable; use crate::generic_shield::GenericShieldTable; use crate::generic_unit::GenericUnitTable; use crate::tool_table::ToolTable; use crate::rare_drop_table::RareDropTable; use crate::box_drop_table::BoxDropTable; use maps::object::MapObject; use entity::item::{ItemType, weapon, armor, shield, unit, mag, tool, tech, esweapon}; fn data_file_path(episode: Episode, difficulty: Difficulty, section_id: SectionID, filename: &str) -> PathBuf { let mut path = PathBuf::from("data/drops/"); path.push(episode.to_string()); path.push(difficulty.to_string().to_lowercase()); path.push(section_id.to_string().to_lowercase()); path.push(filename); path } pub fn load_data_file(episode: Episode, difficulty: Difficulty, section_id: SectionID, filename: &str) -> T { let path = data_file_path(episode, difficulty, section_id, filename); let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s); toml::from_str::(s.as_str()).unwrap() } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub enum MonsterDropType { #[serde(rename = "weapon")] Weapon, #[serde(rename = "armor")] Armor, #[serde(rename = "shield")] Shield, #[serde(rename = "unit")] Unit, #[serde(rename = "none")] None, } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub struct MonsterDropStats { pub dar: u32, pub drop_type: MonsterDropType, pub min_meseta: u32, pub max_meseta: u32, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum ItemDropType { Weapon(weapon::Weapon), Armor(armor::Armor), Shield(shield::Shield), Unit(unit::Unit), Tool(tool::Tool), //Tools(Vec), TechniqueDisk(tech::TechniqueDisk), Mag(mag::Mag), Meseta(u32), } impl ItemDropType { pub fn parse_item_from_bytes(data: [u8; 16]) -> Option { let item_type = weapon::WeaponType::parse_type([data[0],data[1],data[2]]).map(ItemType::Weapon) .or_else(|_| armor::ArmorType::parse_type([data[0],data[1],data[2]]).map(ItemType::Armor)) .or_else(|_| shield::ShieldType::parse_type([data[0],data[1],data[2]]).map(ItemType::Shield)) .or_else(|_| unit::UnitType::parse_type([data[0],data[1],data[2]]).map(ItemType::Unit)) .or_else(|_| mag::MagType::parse_type([data[0],data[1],data[2]]).map(ItemType::Mag)) .or_else(|_| tool::ToolType::parse_type([data[0],data[1],data[2]]).map(ItemType::Tool)) .or_else(|_| esweapon::ESWeaponType::parse_type([data[0],data[1],data[2]]).map(ItemType::ESWeapon)).ok()?; match item_type { ItemType::Weapon(_w) => Some(ItemDropType::Weapon(weapon::Weapon::from_bytes(data).ok()?)), ItemType::Armor(_a) => Some(ItemDropType::Armor(armor::Armor::from_bytes(data).ok()?)), ItemType::Shield(_s) => Some(ItemDropType::Shield(shield::Shield::from_bytes(data).ok()?)), ItemType::Unit(_u) => Some(ItemDropType::Unit(unit::Unit::from_bytes(data).ok()?)), ItemType::Mag(_m) => Some(ItemDropType::Mag(mag::Mag::from_bytes(data).ok()?)), ItemType::Tool(_t) => Some(ItemDropType::Tool(tool::Tool::from_bytes(data).ok()?)), _ => None, } } } #[derive(Clone, Debug)] pub struct ItemDrop { pub map_area: MapArea, pub x: f32, pub y: f32, pub z: f32, pub item: ItemDropType, } pub trait DropTable { fn get_drop(&mut self, map_area: &MapArea, monster: &MonsterType) -> Option; fn get_box_drop(&mut self, map_area: &MapArea, object: &MapObject) -> Option; } pub struct StandardDropTable { monster_stats: HashMap, rare_table: RareDropTable, weapon_table: GenericWeaponTable, armor_table: GenericArmorTable, shield_table: GenericShieldTable, unit_table: GenericUnitTable, tool_table: ToolTable, box_table: BoxDropTable, rng: rand_chacha::ChaCha20Rng, } impl StandardDropTable { #[allow(clippy::new_ret_no_self)] pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> Box { let monster_stats: HashMap = load_data_file(episode, difficulty, section_id, "monster_dar.toml"); Box::new(StandardDropTable { monster_stats: monster_stats.into_iter().map(|(m, s)| (m.parse().unwrap(), s)).collect(), rare_table: RareDropTable::new(episode, difficulty, section_id), weapon_table: GenericWeaponTable::new(episode, difficulty, section_id), armor_table: GenericArmorTable::new(episode, difficulty, section_id), shield_table: GenericShieldTable::new(episode, difficulty, section_id), unit_table: GenericUnitTable::new(episode, difficulty, section_id), tool_table: ToolTable::new(episode, difficulty, section_id), box_table: BoxDropTable::new(episode, difficulty, section_id), rng: rand_chacha::ChaCha20Rng::from_entropy(), }) } pub fn builder() -> DropTableBuilder { DropTableBuilder { monster_stats: None, rare_table: None, weapon_table: None, armor_table: None, shield_table: None, unit_table: None, tool_table: None, box_table: None, rng: None, } } fn generate_meseta(&mut self, monster: &MonsterDropStats) -> Option { Some(ItemDropType::Meseta(self.rng.gen_range(monster.min_meseta, monster.max_meseta + 1))) } fn generate_typed_drop(&mut self, map_area: &MapArea, monster: &MonsterDropStats) -> Option { match monster.drop_type { MonsterDropType::Weapon => self.weapon_table.get_drop(map_area, &mut self.rng), MonsterDropType::Armor => self.armor_table.get_drop(map_area, &mut self.rng), MonsterDropType::Shield => self.shield_table.get_drop(map_area, &mut self.rng), MonsterDropType::Unit => self.unit_table.get_drop(map_area, &mut self.rng), MonsterDropType::None => None, } } } impl DropTable for StandardDropTable { fn get_drop(&mut self, map_area: &MapArea, monster: &MonsterType) -> Option { let monster_stat = *self.monster_stats.get(monster)?; let drop_anything = self.rng.gen_range(0, 100); if drop_anything > monster_stat.dar { return None; } if let Some(item) = self.rare_table.get_drop(map_area, monster, &mut self.rng) { return Some(item); } let drop_type = self.rng.gen_range(0, 3); match drop_type { 0 => { self.generate_meseta(&monster_stat) }, 1 => { self.tool_table.get_drop(map_area, &mut self.rng) }, 2 => { self.generate_typed_drop(map_area, &monster_stat) }, _ => panic!() } } fn get_box_drop(&mut self, map_area: &MapArea, object: &MapObject) -> Option { self.box_table.get_drop(map_area, object, &mut self.rng) } } pub struct DropTableBuilder { monster_stats: Option>, rare_table: Option, weapon_table: Option, armor_table: Option, shield_table: Option, unit_table: Option, tool_table: Option, box_table: Option, rng: Option, } // TODO: add the rest of these later I just need these ones right now impl DropTableBuilder { #[must_use] pub fn monster_stats(mut self, monster_stats: HashMap) -> DropTableBuilder { self.monster_stats = Some(monster_stats); self } #[must_use] pub fn monster_stat(mut self, monster_type: MonsterType, drop_stats: MonsterDropStats) -> DropTableBuilder { match &mut self.monster_stats { Some(monster_stats) => { monster_stats.insert(monster_type, drop_stats); }, None => { let mut monster_stats = HashMap::default(); monster_stats.insert(monster_type, drop_stats); self.monster_stats = Some(monster_stats); } } self } #[must_use] pub fn rare_table(mut self, rare_table: RareDropTable) -> DropTableBuilder { self.rare_table = Some(rare_table); self } pub fn build(self, episode: Episode, difficulty: Difficulty, section_id: SectionID) -> Box { Box::new(StandardDropTable { monster_stats: self.monster_stats.unwrap_or_else(|| { let monster_stats: HashMap = load_data_file(episode, difficulty, section_id, "monster_dar.toml"); monster_stats.into_iter().map(|(m, s)| (m.parse().unwrap(), s)).collect() }), rare_table: self.rare_table.unwrap_or_else(|| RareDropTable::new(episode, difficulty, section_id)), weapon_table: self.weapon_table.unwrap_or_else(|| GenericWeaponTable::new(episode, difficulty, section_id)), armor_table: self.armor_table.unwrap_or_else(|| GenericArmorTable::new(episode, difficulty, section_id)), shield_table: self.shield_table.unwrap_or_else(|| GenericShieldTable::new(episode, difficulty, section_id)), unit_table: self.unit_table.unwrap_or_else(|| GenericUnitTable::new(episode, difficulty, section_id)), tool_table: self.tool_table.unwrap_or_else(|| ToolTable::new(episode, difficulty, section_id)), box_table: self.box_table.unwrap_or_else(|| BoxDropTable::new(episode, difficulty, section_id)), rng: self.rng.unwrap_or_else(rand_chacha::ChaCha20Rng::from_entropy), }) } } #[cfg(test)] mod test { use super::*; use rand::seq::IteratorRandom; #[test] fn test_initializing_drop_table() { let mut rng = rand_chacha::ChaCha20Rng::from_entropy(); let episode = vec![Episode::One, Episode::Two].into_iter().choose(&mut rng).unwrap(); let difficulty = vec![Difficulty::Normal, Difficulty::Hard, Difficulty::VeryHard, Difficulty::Ultimate] .into_iter().choose(&mut rng).unwrap(); let section_id = vec![SectionID::Viridia, SectionID::Greenill, SectionID::Skyly, SectionID::Bluefull, SectionID::Purplenum, SectionID::Pinkal, SectionID::Redria, SectionID::Oran, SectionID::Yellowboze, SectionID::Whitill] .into_iter().choose(&mut rng).unwrap(); DropTable::new(episode, difficulty, section_id); } }