559 lines
17 KiB
Rust
559 lines
17 KiB
Rust
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))
|
|
}
|
|
}
|
|
|
|
|
|
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,
|
|
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 = WeightedIndex::new(tier.special.iter().map(|t| t.probability)).unwrap();
|
|
match special_tier.sample(&mut self.rng) {
|
|
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);
|
|
}
|
|
}
|
|
}
|