use std::collections::{HashMap, BTreeMap}; use serde::{Serialize, Deserialize}; use rand::Rng; use rand::distributions::{WeightedIndex, Distribution}; use rand::seq::SliceRandom; use entity::character::SectionID; use entity::item::weapon::{Weapon, WeaponType, Attribute, WeaponAttribute, WeaponSpecial}; use maps::room::{Difficulty, Episode}; use maps::area::MapArea; use crate::{ItemDropType, load_data_file}; #[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Copy, Clone, Ord, PartialOrd)] pub enum WeaponDropType { Saber, Sword, Dagger, Partisan, Slicer, Handgun, Rifle, Mechgun, Shot, Cane, Rod, Wand } #[derive(Debug, Serialize, Deserialize, Default, Copy, Clone)] pub struct WeaponRatio { rate: u32, rank: i32, inc: u32, } #[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct WeaponRatios { saber: WeaponRatio, sword: WeaponRatio, dagger: WeaponRatio, partisan: WeaponRatio, slicer: WeaponRatio, handgun: WeaponRatio, rifle: WeaponRatio, mechgun: WeaponRatio, shot: WeaponRatio, cane: WeaponRatio, rod: WeaponRatio, wand: WeaponRatio, } impl WeaponRatios { fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> WeaponRatios { load_data_file(episode, difficulty, section_id, "weapon_rate.toml") } } #[derive(Debug, Serialize, Deserialize)] struct GrindRates { grind_rate: [[u32; 9]; 4] } impl GrindRates { fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> GrindRates { load_data_file(episode, difficulty, section_id, "grind_rate.toml") } } #[derive(Debug, Serialize, Deserialize, Clone, Copy)] struct AttributeRate { none: u32, native: u32, abeast: u32, machine: u32, dark: u32, hit: u32, } #[derive(Debug, Serialize, Deserialize)] struct AttributeRates { area1: AttributeRate, area2: AttributeRate, area3: AttributeRate, area4: AttributeRate, area5: AttributeRate, area6: AttributeRate, area7: AttributeRate, area8: AttributeRate, area9: AttributeRate, area10: AttributeRate, } impl AttributeRates { fn get_by_area(&self, map_area: &MapArea) -> AttributeRate { match map_area.drop_area_value().unwrap() { 0 => self.area1, 1 => self.area2, 2 => self.area3, 3 => self.area4, 4 => self.area5, 5 => self.area6, 6 => self.area7, 7 => self.area8, 8 => self.area9, _ => self.area10, } } } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] struct PercentRate { p5: u32, p10: u32, p15: u32, p20: u32, p25: u32, p30: u32, p35: u32, p40: u32, p45: u32, p50: u32, p55: u32, p60: u32, p65: u32, p70: u32, p75: u32, p80: u32, p85: u32, p90: u32, } impl PercentRate { fn as_array(&self) -> [u32; 18] { [self.p5, self.p10, self.p15, self.p20, self.p25, self.p30, self.p35, self.p40, self.p45, self.p50, self.p55, self.p60, self.p65, self.p70, self.p75, self.p80, self.p85, self.p90] } } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] enum PercentPatternType { #[serde(rename = "pattern1")] Pattern1, #[serde(rename = "pattern2")] Pattern2, #[serde(rename = "pattern3")] Pattern3, #[serde(rename = "pattern4")] Pattern4, #[serde(rename = "pattern5")] Pattern5, #[serde(rename = "pattern6")] Pattern6, } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] struct AttributePercentPattern { attribute1: Option, attribute2: Option, attribute3: Option, } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] struct AreaPercentPatterns { area1: AttributePercentPattern, area2: AttributePercentPattern, area3: AttributePercentPattern, area4: AttributePercentPattern, area5: AttributePercentPattern, area6: AttributePercentPattern, area7: AttributePercentPattern, area8: AttributePercentPattern, area9: AttributePercentPattern, area10: AttributePercentPattern, } impl AreaPercentPatterns { fn get_by_area(&self, map_area: &MapArea) -> AttributePercentPattern { match map_area.drop_area_value().unwrap() { 0 => self.area1, 1 => self.area2, 2 => self.area3, 3 => self.area4, 4 => self.area5, 5 => self.area6, 6 => self.area7, 7 => self.area8, 8 => self.area9, _ => self.area10, } } } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] struct PercentRatePatterns { pattern1: PercentRate, pattern2: PercentRate, pattern3: PercentRate, pattern4: PercentRate, pattern5: PercentRate, pattern6: PercentRate, } impl PercentRatePatterns { fn get_by_pattern(&self, pattern: &PercentPatternType) -> PercentRate { match pattern { PercentPatternType::Pattern1 => self.pattern1, PercentPatternType::Pattern2 => self.pattern2, PercentPatternType::Pattern3 => self.pattern3, PercentPatternType::Pattern4 => self.pattern4, PercentPatternType::Pattern5 => self.pattern5, PercentPatternType::Pattern6 => self.pattern6 } } } pub struct AttributeTable { attribute_rates: AttributeRates, percent_rates: PercentRatePatterns, area_percent_patterns: AreaPercentPatterns, } impl AttributeTable { pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> AttributeTable { // TODO: new these let attribute_rates: AttributeRates = load_data_file(episode, difficulty, section_id, "attribute_rate.toml"); let percent_rates: PercentRatePatterns = load_data_file(episode, difficulty, section_id, "percent_rate.toml"); let area_percent_patterns: AreaPercentPatterns = load_data_file(episode, difficulty, section_id, "area_percent_pattern.toml"); AttributeTable { attribute_rates, percent_rates, area_percent_patterns, } } fn generate_attribute(&self, pattern: &PercentPatternType, rates: &AttributeRate, rng: &mut R) -> Option { let attribute_weights = WeightedIndex::new([rates.none, rates.native, rates.abeast, rates.machine, rates.dark, rates.hit]).unwrap(); let attr = match attribute_weights.sample(rng) { 0 => return None, 1 => Attribute::Native, 2 => Attribute::ABeast, 3 => Attribute::Machine, 4 => Attribute::Dark, 5 => Attribute::Hit, _ => panic!() }; let percents = self.percent_rates.get_by_pattern(pattern); let value_weights = WeightedIndex::new(percents.as_array()).unwrap(); let value = value_weights.sample(rng); let percent = ((value + 1) * 5) as i8; Some(WeaponAttribute { attr, value: percent }) } fn attributes(&self, percent_pattern: &AttributePercentPattern, attribute_rate: &AttributeRate, rng: &mut R) -> [Option; 3] { let mut percents = vec![ percent_pattern.attribute1.and_then(|pattern_type| { self.generate_attribute(&pattern_type, attribute_rate, rng) }), percent_pattern.attribute2.and_then(|pattern_type| { self.generate_attribute(&pattern_type, attribute_rate, rng) }), percent_pattern.attribute3.and_then(|pattern_type| { self.generate_attribute(&pattern_type, attribute_rate, rng) }), ]; percents.sort_by_key(|p| { p.as_ref().map(|a| a.attr) }); percents.dedup_by_key(|p| { p.as_ref().map(|a| a.attr) }); percents.iter() .fold(([None; 3], 0), |(mut acc, index), p| { // one day I'll be able to collece into an array if p.is_some() { acc[index] = *p; (acc, index + 1) } else { (acc, index) } }).0 } fn generate_attributes(&self, map_area: &MapArea, rng: &mut R) -> [Option; 3] { let percent_pattern = self.area_percent_patterns.get_by_area(map_area); let attribute_rate = self.attribute_rates.get_by_area(map_area); self.attributes(&percent_pattern, &attribute_rate, rng) } pub fn generate_rare_attributes(&self, map_area: &MapArea, rng: &mut R) -> [Option; 3] { let percent_pattern = AttributePercentPattern { attribute1: Some(PercentPatternType::Pattern6), attribute2: Some(PercentPatternType::Pattern6), attribute3: Some(PercentPatternType::Pattern6), }; let attribute_rate = self.attribute_rates.get_by_area(map_area); self.attributes(&percent_pattern, &attribute_rate, rng) } } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] struct SpecialRate { rank: u32, rate: u32, } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] struct SpecialRates { area1: SpecialRate, area2: SpecialRate, area3: SpecialRate, area4: SpecialRate, area5: SpecialRate, area6: SpecialRate, area7: SpecialRate, area8: SpecialRate, area9: SpecialRate, area10: SpecialRate, } impl SpecialRates { fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> SpecialRates { load_data_file(episode, difficulty, section_id, "weapon_special_rate.toml") } fn rate_by_area(&self, map_area: &MapArea) -> SpecialRate { match map_area.drop_area_value().unwrap() { 0 => self.area1, 1 => self.area2, 2 => self.area3, 3 => self.area4, 4 => self.area5, 5 => self.area6, 6 => self.area7, 7=> self.area8, 8 => self.area9, _ => self.area10, } } fn random_special_by_rank(&self, rank: u32, rng: &mut R) -> WeaponSpecial { let specials = match rank { 1 => vec![WeaponSpecial::Draw, WeaponSpecial::Heart, WeaponSpecial::Ice, WeaponSpecial::Bind, WeaponSpecial::Heat, WeaponSpecial::Shock, WeaponSpecial::Dim, WeaponSpecial::Panic], 2 => vec![WeaponSpecial::Drain, WeaponSpecial::Mind, WeaponSpecial::Frost, WeaponSpecial::Hold, WeaponSpecial::Fire, WeaponSpecial::Thunder, WeaponSpecial::Shadow, WeaponSpecial::Riot, WeaponSpecial::Masters, WeaponSpecial::Charge], 3 => vec![WeaponSpecial::Fill, WeaponSpecial::Soul, WeaponSpecial::Freeze, WeaponSpecial::Seize, WeaponSpecial::Flame, WeaponSpecial::Storm, WeaponSpecial::Dark, WeaponSpecial::Havoc, WeaponSpecial::Lords, WeaponSpecial::Charge, WeaponSpecial::Spirit, WeaponSpecial::Devils], 4 => vec![WeaponSpecial::Gush, WeaponSpecial::Geist, WeaponSpecial::Blizzard, WeaponSpecial::Arrest, WeaponSpecial::Burning, WeaponSpecial::Tempest, WeaponSpecial::Hell, WeaponSpecial::Chaos, WeaponSpecial::Kings, WeaponSpecial::Charge, WeaponSpecial::Berserk, WeaponSpecial::Demons], _ => panic!(), }; *specials.choose(rng).unwrap() } fn get_special(&self, map_area: &MapArea, rng: &mut R) -> Option { let rate = self.rate_by_area(map_area); if rng.gen_range(0, 100) < rate.rate { Some(self.random_special_by_rank(rate.rank, rng)) } else { None } } } pub struct GenericWeaponTable { rank_table: HashMap>, weapon_ratio: WeaponRatios, grind_rates: GrindRates, attribute_table: AttributeTable, special_table: SpecialRates, } impl GenericWeaponTable { pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> GenericWeaponTable { let mut rank_table = HashMap::new(); rank_table.insert(WeaponDropType::Saber, vec![WeaponType::Saber, WeaponType::Brand, WeaponType::Buster, WeaponType::Pallasch, WeaponType::Gladius]); rank_table.insert(WeaponDropType::Sword, vec![WeaponType::Sword, WeaponType::Gigush, WeaponType::Breaker, WeaponType::Claymore, WeaponType::Calibur]); rank_table.insert(WeaponDropType::Dagger, vec![WeaponType::Dagger, WeaponType::Knife, WeaponType::Blade, WeaponType::Edge, WeaponType::Ripper]); rank_table.insert(WeaponDropType::Partisan, vec![WeaponType::Partisan, WeaponType::Halbert, WeaponType::Glaive, WeaponType::Berdys, WeaponType::Gungnir]); rank_table.insert(WeaponDropType::Slicer, vec![WeaponType::Slicer, WeaponType::Spinner, WeaponType::Cutter, WeaponType::Sawcer, WeaponType::Diska]); rank_table.insert(WeaponDropType::Handgun, vec![WeaponType::Handgun, WeaponType::Autogun, WeaponType::Lockgun, WeaponType::Railgun, WeaponType::Raygun]); rank_table.insert(WeaponDropType::Rifle, vec![WeaponType::Rifle, WeaponType::Sniper, WeaponType::Blaster, WeaponType::Beam, WeaponType::Laser]); rank_table.insert(WeaponDropType::Mechgun, vec![WeaponType::Mechgun, WeaponType::Assault, WeaponType::Repeater, WeaponType::Gatling, WeaponType::Vulcan]); rank_table.insert(WeaponDropType::Shot, vec![WeaponType::Shot, WeaponType::Spread, WeaponType::Cannon, WeaponType::Launcher, WeaponType::Arms]); rank_table.insert(WeaponDropType::Cane, vec![WeaponType::Cane, WeaponType::Stick, WeaponType::Mace, WeaponType::Club]); rank_table.insert(WeaponDropType::Rod, vec![WeaponType::Rod, WeaponType::Pole, WeaponType::Pillar, WeaponType::Striker]); rank_table.insert(WeaponDropType::Wand, vec![WeaponType::Wand, WeaponType::Staff, WeaponType::Baton, WeaponType::Scepter]); GenericWeaponTable { rank_table, weapon_ratio: WeaponRatios::new(episode, difficulty, section_id), grind_rates: GrindRates::new(episode, difficulty, section_id), attribute_table: AttributeTable::new(episode, difficulty, section_id), special_table: SpecialRates::new(episode, difficulty, section_id), } } fn area_rank(&self, ratio: &WeaponRatio, map_area: &MapArea) -> (u32, u32) { if ratio.rank >= 0 { (map_area.drop_area_value().unwrap_or(0), ratio.rank as u32) } else { ((map_area.drop_area_value().unwrap_or(0) as i32 + ratio.rank) as u32, 0) } } fn get_possible_weapon_types(&self, map_area: &MapArea) -> BTreeMap { let mut valid_weapons = BTreeMap::new(); let item_rate_pairs = vec![ (WeaponDropType::Saber, self.weapon_ratio.saber), (WeaponDropType::Sword, self.weapon_ratio.sword), (WeaponDropType::Dagger, self.weapon_ratio.dagger), (WeaponDropType::Partisan, self.weapon_ratio.partisan), (WeaponDropType::Slicer, self.weapon_ratio.slicer), (WeaponDropType::Handgun, self.weapon_ratio.handgun), (WeaponDropType::Rifle, self.weapon_ratio.rifle), (WeaponDropType::Mechgun, self.weapon_ratio.mechgun), (WeaponDropType::Shot, self.weapon_ratio.shot), (WeaponDropType::Cane, self.weapon_ratio.cane), (WeaponDropType::Rod, self.weapon_ratio.rod), (WeaponDropType::Wand, self.weapon_ratio.wand), ]; for (item_type, ratio) in item_rate_pairs.into_iter() { if ratio.rate > 0 && map_area.drop_area_value().map(|k| k as i32 + ratio.rank).unwrap_or(-1) >= 0 { valid_weapons.insert(item_type, ratio); } } valid_weapons } fn weapon_type(&self, possible_weapon_types: &BTreeMap, _map_area: &MapArea, rng: &mut R) -> WeaponDropType { let mut weapon_rates = possible_weapon_types.iter() .map(|(weapon, stat)| { (weapon, stat.rate) }); let weapon_choice = WeightedIndex::new(weapon_rates.clone().map(|wr| wr.1)).unwrap(); *weapon_rates.nth(weapon_choice.sample(rng)).unwrap().0 } fn weapon_rank(&self, ratio: &WeaponRatio, map_area: &MapArea) -> u32 { let (area, rank) = self.area_rank(ratio, map_area); let weapon_rank = rank + area / ratio.inc; std::cmp::max(weapon_rank, 0) } fn actual_weapon(&self, weapon_type: &WeaponDropType, weapon_rank: u32) -> WeaponType { let weapons = self.rank_table.get(weapon_type).unwrap(); weapons[std::cmp::min(weapons.len() - 1, weapon_rank as usize)] } fn get_grind(&self, ratio: &WeaponRatio, map_area: &MapArea, rng: &mut R) -> usize { let (area, _) = self.area_rank(ratio, map_area); let pattern = std::cmp::min(area % ratio.inc, 3); let weights = self.grind_rates.grind_rate[pattern as usize]; let grind_choice = WeightedIndex::new(weights).unwrap(); grind_choice.sample(rng) } pub fn get_drop(&self, map_area: &MapArea, rng: &mut R) -> Option { let possible_weapon_types = self.get_possible_weapon_types(map_area); let weapon_type = self.weapon_type(&possible_weapon_types, map_area, rng); let ratio = possible_weapon_types.get(&weapon_type).unwrap(); let weapon_rank = self.weapon_rank(ratio, map_area); let weapon_grind = self.get_grind(ratio, map_area, rng); let weapon_attributes = self.attribute_table.generate_attributes(map_area, rng); let weapon_special = self.special_table.get_special(map_area, rng); let actual_weapon = self.actual_weapon(&weapon_type, weapon_rank); Some(ItemDropType::Weapon(Weapon { weapon: actual_weapon, special: weapon_special, grind: weapon_grind as u8, attrs: weapon_attributes, tekked: weapon_special.is_none(), })) } } #[cfg(test)] mod test { use super::*; use rand::{SeedableRng}; #[test] fn test_weapon_generation() { let mut rng = rand_chacha::ChaCha20Rng::from_seed([23;32]); let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Normal, SectionID::Skyly); assert!(gwt.get_drop(&MapArea::Forest1, &mut rng) == Some(ItemDropType::Weapon(Weapon { weapon: WeaponType::Cane, special: None, grind: 0, attrs: [None, None, None], tekked: true, }))); let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Hard, SectionID::Skyly); assert!(gwt.get_drop(&MapArea::Caves2, &mut rng) == Some(ItemDropType::Weapon(Weapon { weapon: WeaponType::Sniper, special: None, grind: 2, attrs: [None, None, None], tekked: true, }))); let gwt = GenericWeaponTable::new(Episode::One, Difficulty::VeryHard, SectionID::Skyly); assert!(gwt.get_drop(&MapArea::Mines1, &mut rng) == Some(ItemDropType::Weapon(Weapon { weapon: WeaponType::Club, special: Some(WeaponSpecial::Berserk), grind: 0, attrs: [None, None, None], tekked: false, }))); let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Ultimate, SectionID::Skyly); assert!(gwt.get_drop(&MapArea::DarkFalz, &mut rng) == Some(ItemDropType::Weapon(Weapon { weapon: WeaponType::Vulcan, special: None, grind: 0, attrs: [Some(WeaponAttribute {attr: Attribute::ABeast, value: 30}), Some(WeaponAttribute {attr: Attribute::Dark, value: 30}), None], tekked: true, }))); } }