You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

571 lines
18 KiB

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 crate::entity::character::SectionID;
use crate::ship::room::Difficulty;
use crate::entity::item::ItemDetail;
use crate::entity::item::weapon::{Weapon, WeaponType, WeaponSpecial, Attribute, WeaponAttribute};
use crate::ship::shops::ShopItem;
use crate::ship::item_stats::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<WeaponSpecial>,
grind: usize,
attributes: [Option<WeaponAttribute>; 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<Ordering> {
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<WeaponTableTierEntry>,
}
#[derive(Debug)]
struct WeaponTable(Vec<WeaponTableTier>);
#[derive(Debug, Deserialize)]
struct GrindTier {
level: usize,
min: usize,
max: usize,
}
#[derive(Debug)]
struct GrindTable(Vec<GrindTier>);
#[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<AttributeTier>);
#[derive(Debug, Deserialize)]
struct SpecialTierEntry {
tier: usize,
probability: usize,
}
#[derive(Debug, Deserialize)]
struct SpecialTier {
level: usize,
special: Vec<SpecialTierEntry>,
}
#[derive(Debug)]
struct SpecialTable(Vec<SpecialTier>);
/*
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<WeaponTableTier> = 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<String, Vec<WeaponTableTier>> = 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<String, Vec<SpecialTier>> = 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<String, Vec<GrindTier>> = 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<String, Vec<GrindTier>> = 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<String, Vec<AttributeTier>> = 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<String, Vec<AttributeTier>> = 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<R: Rng + SeedableRng> {
_difficulty: Difficulty,
section_id: SectionID,
weapon: WeaponTable,
special: SpecialTable,
grind: GrindTable,
alt_grind: GrindTable,
attr1: AttributeTable,
attr2: AttributeTable,
rng: R,
}
impl<R: Rng + SeedableRng> WeaponShop<R> {
pub fn new(difficulty: Difficulty, section_id: SectionID) -> WeaponShop<R> {
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<WeaponSpecial> {
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<WeaponAttribute> {
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<WeaponAttribute> {
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<WeaponShopItem> {
let mut x = (0..number_of_weapons_to_generate(level))
.map(|_| {
self.generate_weapon(level)
})
.collect::<Vec<WeaponShopItem>>();
x.sort();
x
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_loading_weapon_shop() {
WeaponShop::<rand_chacha::ChaCha20Rng>::new(Difficulty::Ultimate, SectionID::Pinkal);
}
#[test]
fn test_generating_some_weapons() {
let mut ws = WeaponShop::<rand_chacha::ChaCha20Rng>::new(Difficulty::Ultimate, SectionID::Pinkal);
for i in 0..200 {
ws.generate_weapon_list(i);
}
}
}