use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::PathBuf; use std::convert::TryInto; use std::cmp::Ordering; use serde::Deserialize; use rand::{Rng, SeedableRng}; use rand::distributions::{WeightedIndex, Distribution}; use rand::seq::{SliceRandom, IteratorRandom}; use entity::character::SectionID; use maps::room::Difficulty; use entity::item::ItemDetail; use entity::item::weapon::{Weapon, WeaponType, WeaponSpecial, Attribute, WeaponAttribute}; use crate::ShopItem; use stats::items::WEAPON_STATS; const TIER1_SPECIAL: [WeaponSpecial; 8] = [WeaponSpecial::Draw, WeaponSpecial::Heart, WeaponSpecial::Ice, WeaponSpecial::Bind, WeaponSpecial::Heat, WeaponSpecial::Shock, WeaponSpecial::Dim, WeaponSpecial::Panic]; const TIER2_SPECIAL: [WeaponSpecial; 10] = [WeaponSpecial::Drain, WeaponSpecial::Mind, WeaponSpecial::Masters, WeaponSpecial::Charge, WeaponSpecial::Frost, WeaponSpecial::Hold, WeaponSpecial::Fire, WeaponSpecial::Thunder, WeaponSpecial::Shadow, WeaponSpecial::Riot]; #[derive(Debug)] pub struct WeaponShopItem { weapon: WeaponType, special: Option, grind: usize, attributes: [Option; 3], } impl PartialEq for WeaponShopItem { fn eq(&self, other: &Self) -> bool { self.weapon == other.weapon && self.special == other.special && self.grind == other.grind && self.attributes == other.attributes } } impl Eq for WeaponShopItem {} impl Ord for WeaponShopItem { fn cmp(&self, other: &Self) -> Ordering { self.weapon.value().cmp(&other.weapon.value()) } } impl PartialOrd for WeaponShopItem { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl From<&Weapon> for WeaponShopItem { fn from(weapon: &Weapon) -> WeaponShopItem { WeaponShopItem { weapon: weapon.weapon, special: weapon.special, grind: weapon.grind as usize, attributes: weapon.attrs, } } } fn special_stars(special: &WeaponSpecial) -> usize { match special { WeaponSpecial::Draw => 1, WeaponSpecial::Drain => 2, WeaponSpecial::Fill => 3, WeaponSpecial::Gush => 4, WeaponSpecial::Heart => 1, WeaponSpecial::Mind => 2, WeaponSpecial::Soul => 3, WeaponSpecial::Geist => 4, WeaponSpecial::Masters => 2, WeaponSpecial::Lords => 3, WeaponSpecial::Kings => 4, WeaponSpecial::Charge => 2, WeaponSpecial::Spirit => 3, WeaponSpecial::Berserk => 4, WeaponSpecial::Ice => 1, WeaponSpecial::Frost => 2, WeaponSpecial::Freeze => 3, WeaponSpecial::Blizzard => 4, WeaponSpecial::Bind => 1, WeaponSpecial::Hold => 2, WeaponSpecial::Seize => 3, WeaponSpecial::Arrest => 4, WeaponSpecial::Heat => 1, WeaponSpecial::Fire => 2, WeaponSpecial::Flame => 3, WeaponSpecial::Burning => 4, WeaponSpecial::Shock => 1, WeaponSpecial::Thunder => 2, WeaponSpecial::Storm => 3, WeaponSpecial::Tempest => 4, WeaponSpecial::Dim => 1, WeaponSpecial::Shadow => 2, WeaponSpecial::Dark => 3, WeaponSpecial::Hell => 4, WeaponSpecial::Panic => 1, WeaponSpecial::Riot => 2, WeaponSpecial::Havoc => 3, WeaponSpecial::Chaos => 4, WeaponSpecial::Devils => 3, WeaponSpecial::Demons => 4, } } impl ShopItem for WeaponShopItem { fn price(&self) -> usize { WEAPON_STATS.get(&self.weapon) .map(|weapon_stat| { let mut price = weapon_stat.atp_max as f32; price += self.grind as f32; price = (price * (price * 3.0)) / weapon_stat.shop_multiplier; let percent = self.attributes.iter() .fold(0.0, |acc, attr| { acc + attr.map(|a| a.value).unwrap_or(0) as f32 }); price = price + ((price / 300.0) * percent); let special = self.special.map(|special| { special_stars(&special) as f32 }).unwrap_or(0.0); price += special * special * 1000.0; price as usize }) .unwrap_or(0xFFFF) } fn as_bytes(&self) -> [u8; 12] { self.as_item().as_client_bytes()[0..12].try_into().unwrap() } fn as_item(&self) -> ItemDetail { ItemDetail::Weapon( Weapon { weapon: self.weapon, special: self.special, grind: self.grind as u8, attrs: [self.attributes[0], self.attributes[1], None], tekked: true, } ) } } impl WeaponShopItem { pub fn weapon_from_item(w: &Weapon) -> WeaponShopItem { WeaponShopItem { weapon: w.weapon, special: w.special, grind: w.grind as usize, attributes: w.attrs, } } } #[derive(Debug, Deserialize)] struct WeaponTableTierEntry { weapon: WeaponType, probability: usize, } #[derive(Debug, Deserialize)] struct WeaponTableTier { level: usize, weapons: Vec, } #[derive(Debug)] struct WeaponTable(Vec); #[derive(Debug, Deserialize)] struct GrindTier { level: usize, min: usize, max: usize, } #[derive(Debug)] struct GrindTable(Vec); #[derive(Debug, Deserialize)] struct AttributeTier { level: usize, percent_min: isize, percent_max: isize, none: usize, native: usize, abeast: usize, machine: usize, dark: usize, hit: usize, } #[derive(Debug)] struct AttributeTable(Vec); #[derive(Debug, Deserialize)] struct SpecialTierEntry { tier: usize, probability: usize, } #[derive(Debug, Deserialize)] struct SpecialTier { level: usize, special: Vec, } #[derive(Debug)] struct SpecialTable(Vec); /* trait WeaponTableLoader { fn load(difficulty: Difficulty, section_id: SectionID) -> WeaponTable where Self::Sized; } struct WeaponTableLoaderImpl; impl WeaponTableLoader for WeaponTableLoaderImpl { fn load(difficulty: Difficulty, section_id: SectionID) -> WeaponTable { let mut path = PathBuf::from("data/shops/"); path.push(difficulty.to_string().to_lowercase()); path.push(section_id.to_string().to_lowercase()); path.push("weapon.toml"); let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s); let table: Vec = toml::from_str(s.as_str()).unwrap(); println!("table {:?}", table); WeaponTable { } } } */ fn load_weapon_table(difficulty: Difficulty, section_id: SectionID) -> WeaponTable { let mut path = PathBuf::from("data/shops/"); path.push(difficulty.to_string().to_lowercase()); path.push(section_id.to_string().to_lowercase()); path.push("weapon.toml"); let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); let mut table: HashMap> = toml::from_str(s.as_str()).unwrap(); WeaponTable(table.remove("weapon_tier").unwrap()) } fn load_special_table() -> SpecialTable { let path = PathBuf::from("data/shops/special.toml"); let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); let mut table: HashMap> = toml::from_str(s.as_str()).unwrap(); SpecialTable(table.remove("specials").unwrap()) } fn load_grind_table() -> GrindTable { let path = PathBuf::from("data/shops/grind.toml"); let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); let mut table: HashMap> = toml::from_str(s.as_str()).unwrap(); GrindTable(table.remove("grind").unwrap()) } fn load_alt_grind_table() -> GrindTable { let path = PathBuf::from("data/shops/alt_grind.toml"); let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); let mut table: HashMap> = toml::from_str(s.as_str()).unwrap(); GrindTable(table.remove("grind").unwrap()) } fn load_attribute1_table() -> AttributeTable { let path = PathBuf::from("data/shops/attribute1.toml"); let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); let mut table: HashMap> = toml::from_str(s.as_str()).unwrap(); AttributeTable(table.remove("attributes").unwrap()) } fn load_attribute2_table() -> AttributeTable { let path = PathBuf::from("data/shops/attribute2.toml"); let mut f = File::open(path).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); let mut table: HashMap> = toml::from_str(s.as_str()).unwrap(); AttributeTable(table.remove("attributes").unwrap()) } fn number_of_weapons_to_generate(character_level: usize) -> usize { if character_level <= 10 { 10 } else if character_level <= 42 { 12 } else { 16 } } #[derive(Debug)] pub struct WeaponShop { _difficulty: Difficulty, section_id: SectionID, weapon: WeaponTable, special: SpecialTable, grind: GrindTable, alt_grind: GrindTable, attr1: AttributeTable, attr2: AttributeTable, rng: R, } impl WeaponShop { pub fn new(difficulty: Difficulty, section_id: SectionID) -> WeaponShop { WeaponShop { _difficulty: difficulty, section_id, weapon: load_weapon_table(difficulty, section_id), special: load_special_table(), grind: load_grind_table(), alt_grind: load_alt_grind_table(), attr1: load_attribute1_table(), attr2: load_attribute2_table(), rng: R::from_entropy(), } } fn generate_type(&mut self, level: usize) -> WeaponType { let tier = self.weapon.0.iter() .filter(|t| t.level <= level) .last() .unwrap(); let weapon_choice = WeightedIndex::new(tier.weapons.iter().map(|t| t.probability)).unwrap(); tier.weapons.get(weapon_choice.sample(&mut self.rng)).unwrap().weapon } fn generate_special(&mut self, level: usize) -> Option { let tier = self.special.0.iter() .filter(|t| t.level <= level) .last() .unwrap(); let special_tier_choice = WeightedIndex::new(tier.special.iter().map(|t| t.probability)).unwrap(); let special_tier_index = special_tier_choice.sample(&mut self.rng); let special_tier = tier.special.get(special_tier_index)?; match special_tier.tier { 1 => TIER1_SPECIAL.choose(&mut self.rng).cloned(), 2 => TIER2_SPECIAL.choose(&mut self.rng).cloned(), _ => None } } fn generate_grind(&mut self, level: usize) -> usize { let tier = self.grind.0.iter() .filter(|t| t.level <= level) .last() .unwrap(); self.rng.gen_range(tier.min, tier.max+1) } fn generate_alt_grind(&mut self, level: usize) -> usize { let tier = self.alt_grind.0.iter() .find(|t| t.level <= level) .unwrap(); self.rng.gen_range(tier.min, tier.max+1) } fn generate_attribute1(&mut self, level: usize) -> Option { let tier = self.attr1.0.iter() .filter(|t| t.level <= level) .last() .unwrap(); let attr_choice = WeightedIndex::new([tier.none, tier.native, tier.abeast, tier.machine, tier.dark, tier.hit]).unwrap(); let attr = match attr_choice.sample(&mut self.rng) { 0 => return None, 1 => Attribute::Native, 2 => Attribute::ABeast, 3 => Attribute::Machine, 4 => Attribute::Dark, 5 => Attribute::Hit, _ => panic!() }; let percent = (tier.percent_min..tier.percent_max+1) .filter(|p| p % 5 == 0) .choose(&mut self.rng)?; Some(WeaponAttribute { attr, value: percent as i8, }) } fn generate_attribute2(&mut self, level: usize) -> Option { let tier = self.attr2.0.iter() .filter(|t| t.level <= level) .last() .unwrap(); let attr_choice = WeightedIndex::new([tier.none, tier.native, tier.abeast, tier.machine, tier.dark, tier.hit]).unwrap(); let attr = match attr_choice.sample(&mut self.rng) { 0 => return None, 1 => Attribute::Native, 2 => Attribute::ABeast, 3 => Attribute::Machine, 4 => Attribute::Dark, 5 => Attribute::Hit, _ => panic!() }; let percent = (tier.percent_min..tier.percent_max+1) .filter(|p| p % 5 == 0) .choose(&mut self.rng)?; Some(WeaponAttribute { attr, value: percent as i8, }) } fn is_alt_grind(&self, weapon: &WeaponType) -> bool { matches!((self.section_id, weapon), (SectionID::Viridia, WeaponType::Shot) | (SectionID::Viridia, WeaponType::Spread) | (SectionID::Viridia, WeaponType::Cannon) | (SectionID::Viridia, WeaponType::Launcher) | (SectionID::Viridia, WeaponType::Arms) | (SectionID::Greenill, WeaponType::Rifle) | (SectionID::Greenill, WeaponType::Sniper) | (SectionID::Greenill, WeaponType::Blaster) | (SectionID::Greenill, WeaponType::Beam) | (SectionID::Greenill, WeaponType::Laser) | (SectionID::Skyly, WeaponType::Sword) | (SectionID::Skyly, WeaponType::Gigush) | (SectionID::Skyly, WeaponType::Breaker) | (SectionID::Skyly, WeaponType::Claymore) | (SectionID::Skyly, WeaponType::Calibur) | (SectionID::Bluefull, WeaponType::Partisan) | (SectionID::Bluefull, WeaponType::Halbert) | (SectionID::Bluefull, WeaponType::Glaive) | (SectionID::Bluefull, WeaponType::Berdys) | (SectionID::Bluefull, WeaponType::Gungnir) | (SectionID::Purplenum, WeaponType::Mechgun) | (SectionID::Purplenum, WeaponType::Assault) | (SectionID::Purplenum, WeaponType::Repeater) | (SectionID::Purplenum, WeaponType::Gatling) | (SectionID::Purplenum, WeaponType::Vulcan) | (SectionID::Pinkal, WeaponType::Cane) | (SectionID::Pinkal, WeaponType::Stick) | (SectionID::Pinkal, WeaponType::Mace) | (SectionID::Pinkal, WeaponType::Club) | (SectionID::Oran, WeaponType::Dagger) | (SectionID::Oran, WeaponType::Knife) | (SectionID::Oran, WeaponType::Blade) | (SectionID::Oran, WeaponType::Edge) | (SectionID::Oran, WeaponType::Ripper) | (SectionID::Whitill, WeaponType::Slicer) | (SectionID::Whitill, WeaponType::Spinner) | (SectionID::Whitill, WeaponType::Cutter) | (SectionID::Whitill, WeaponType::Sawcer) | (SectionID::Whitill, WeaponType::Diska)) } fn generate_weapon(&mut self, level: usize) -> WeaponShopItem { let weapon = self.generate_type(level); let grind = if self.is_alt_grind(&weapon) { self.generate_alt_grind(level) } else { self.generate_grind(level) }; let special = self.generate_special(level); let (attr1, attr2) = { match self.generate_attribute1(level) { Some(a1) => { let a2 = loop { let attr = self.generate_attribute2(level); match attr { Some(a2) => { if a2.attr != a1.attr { break Some(a2); } }, None => break None, } }; (Some(a1), a2) }, None => { let a2 = self.generate_attribute2(level); (a2, None) } } }; WeaponShopItem { weapon, grind, special, attributes: [attr1, attr2, None], } } pub fn generate_weapon_list(&mut self, level: usize) -> Vec { let mut x = (0..number_of_weapons_to_generate(level)) .map(|_| { self.generate_weapon(level) }) .collect::>(); x.sort(); x } } #[cfg(test)] mod test { use super::*; #[test] fn test_loading_weapon_shop() { WeaponShop::::new(Difficulty::Ultimate, SectionID::Pinkal); } #[test] fn test_generating_some_weapons() { let mut ws = WeaponShop::::new(Difficulty::Ultimate, SectionID::Pinkal); for i in 0..200 { ws.generate_weapon_list(i); } } }