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.

550 lines
20 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. use std::collections::{HashMap, BTreeMap};
  2. use serde::{Serialize, Deserialize};
  3. use rand::Rng;
  4. use rand::distributions::{WeightedIndex, Distribution};
  5. use rand::seq::SliceRandom;
  6. use entity::character::SectionID;
  7. use entity::item::weapon::{Weapon, WeaponType, Attribute, WeaponAttribute, WeaponSpecial};
  8. use maps::room::{Difficulty, Episode};
  9. use maps::area::MapArea;
  10. use crate::{ItemDropType, load_data_file};
  11. #[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Copy, Clone, Ord, PartialOrd)]
  12. pub enum WeaponDropType {
  13. Saber,
  14. Sword,
  15. Dagger,
  16. Partisan,
  17. Slicer,
  18. Handgun,
  19. Rifle,
  20. Mechgun,
  21. Shot,
  22. Cane,
  23. Rod,
  24. Wand
  25. }
  26. #[derive(Debug, Serialize, Deserialize, Default, Copy, Clone)]
  27. pub struct WeaponRatio {
  28. rate: u32,
  29. rank: i32,
  30. inc: u32,
  31. }
  32. #[derive(Debug, Serialize, Deserialize, Default, Clone)]
  33. pub struct WeaponRatios {
  34. saber: WeaponRatio,
  35. sword: WeaponRatio,
  36. dagger: WeaponRatio,
  37. partisan: WeaponRatio,
  38. slicer: WeaponRatio,
  39. handgun: WeaponRatio,
  40. rifle: WeaponRatio,
  41. mechgun: WeaponRatio,
  42. shot: WeaponRatio,
  43. cane: WeaponRatio,
  44. rod: WeaponRatio,
  45. wand: WeaponRatio,
  46. }
  47. impl WeaponRatios {
  48. fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> WeaponRatios {
  49. load_data_file(episode, difficulty, section_id, "weapon_rate.toml")
  50. }
  51. }
  52. #[derive(Debug, Serialize, Deserialize)]
  53. struct GrindRates {
  54. grind_rate: [[u32; 9]; 4]
  55. }
  56. impl GrindRates {
  57. fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> GrindRates {
  58. load_data_file(episode, difficulty, section_id, "grind_rate.toml")
  59. }
  60. }
  61. #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
  62. struct AttributeRate {
  63. none: u32,
  64. native: u32,
  65. abeast: u32,
  66. machine: u32,
  67. dark: u32,
  68. hit: u32,
  69. }
  70. #[derive(Debug, Serialize, Deserialize)]
  71. struct AttributeRates {
  72. area1: AttributeRate,
  73. area2: AttributeRate,
  74. area3: AttributeRate,
  75. area4: AttributeRate,
  76. area5: AttributeRate,
  77. area6: AttributeRate,
  78. area7: AttributeRate,
  79. area8: AttributeRate,
  80. area9: AttributeRate,
  81. area10: AttributeRate,
  82. }
  83. impl AttributeRates {
  84. fn get_by_area(&self, map_area: &MapArea) -> AttributeRate {
  85. match map_area.drop_area_value().unwrap() {
  86. 0 => self.area1,
  87. 1 => self.area2,
  88. 2 => self.area3,
  89. 3 => self.area4,
  90. 4 => self.area5,
  91. 5 => self.area6,
  92. 6 => self.area7,
  93. 7 => self.area8,
  94. 8 => self.area9,
  95. _ => self.area10,
  96. }
  97. }
  98. }
  99. #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
  100. struct PercentRate {
  101. p5: u32,
  102. p10: u32,
  103. p15: u32,
  104. p20: u32,
  105. p25: u32,
  106. p30: u32,
  107. p35: u32,
  108. p40: u32,
  109. p45: u32,
  110. p50: u32,
  111. p55: u32,
  112. p60: u32,
  113. p65: u32,
  114. p70: u32,
  115. p75: u32,
  116. p80: u32,
  117. p85: u32,
  118. p90: u32,
  119. }
  120. impl PercentRate {
  121. fn as_array(&self) -> [u32; 18] {
  122. [self.p5, self.p10, self.p15, self.p20, self.p25, self.p30, self.p35, self.p40, self.p45,
  123. self.p50, self.p55, self.p60, self.p65, self.p70, self.p75, self.p80, self.p85, self.p90]
  124. }
  125. }
  126. #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
  127. enum PercentPatternType {
  128. #[serde(rename = "pattern1")]
  129. Pattern1,
  130. #[serde(rename = "pattern2")]
  131. Pattern2,
  132. #[serde(rename = "pattern3")]
  133. Pattern3,
  134. #[serde(rename = "pattern4")]
  135. Pattern4,
  136. #[serde(rename = "pattern5")]
  137. Pattern5,
  138. #[serde(rename = "pattern6")]
  139. Pattern6,
  140. }
  141. #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
  142. struct AttributePercentPattern {
  143. attribute1: Option<PercentPatternType>,
  144. attribute2: Option<PercentPatternType>,
  145. attribute3: Option<PercentPatternType>,
  146. }
  147. #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
  148. struct AreaPercentPatterns {
  149. area1: AttributePercentPattern,
  150. area2: AttributePercentPattern,
  151. area3: AttributePercentPattern,
  152. area4: AttributePercentPattern,
  153. area5: AttributePercentPattern,
  154. area6: AttributePercentPattern,
  155. area7: AttributePercentPattern,
  156. area8: AttributePercentPattern,
  157. area9: AttributePercentPattern,
  158. area10: AttributePercentPattern,
  159. }
  160. impl AreaPercentPatterns {
  161. fn get_by_area(&self, map_area: &MapArea) -> AttributePercentPattern {
  162. match map_area.drop_area_value().unwrap() {
  163. 0 => self.area1,
  164. 1 => self.area2,
  165. 2 => self.area3,
  166. 3 => self.area4,
  167. 4 => self.area5,
  168. 5 => self.area6,
  169. 6 => self.area7,
  170. 7 => self.area8,
  171. 8 => self.area9,
  172. _ => self.area10,
  173. }
  174. }
  175. }
  176. #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
  177. struct PercentRatePatterns {
  178. pattern1: PercentRate,
  179. pattern2: PercentRate,
  180. pattern3: PercentRate,
  181. pattern4: PercentRate,
  182. pattern5: PercentRate,
  183. pattern6: PercentRate,
  184. }
  185. impl PercentRatePatterns {
  186. fn get_by_pattern(&self, pattern: &PercentPatternType) -> PercentRate {
  187. match pattern {
  188. PercentPatternType::Pattern1 => self.pattern1,
  189. PercentPatternType::Pattern2 => self.pattern2,
  190. PercentPatternType::Pattern3 => self.pattern3,
  191. PercentPatternType::Pattern4 => self.pattern4,
  192. PercentPatternType::Pattern5 => self.pattern5,
  193. PercentPatternType::Pattern6 => self.pattern6
  194. }
  195. }
  196. }
  197. pub struct AttributeTable {
  198. attribute_rates: AttributeRates,
  199. percent_rates: PercentRatePatterns,
  200. area_percent_patterns: AreaPercentPatterns,
  201. }
  202. impl AttributeTable {
  203. pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> AttributeTable {
  204. // TODO: new these
  205. let attribute_rates: AttributeRates = load_data_file(episode, difficulty, section_id, "attribute_rate.toml");
  206. let percent_rates: PercentRatePatterns = load_data_file(episode, difficulty, section_id, "percent_rate.toml");
  207. let area_percent_patterns: AreaPercentPatterns = load_data_file(episode, difficulty, section_id, "area_percent_pattern.toml");
  208. AttributeTable {
  209. attribute_rates,
  210. percent_rates,
  211. area_percent_patterns,
  212. }
  213. }
  214. fn generate_attribute<R: Rng>(&self, pattern: &PercentPatternType, rates: &AttributeRate, rng: &mut R) -> Option<WeaponAttribute> {
  215. let attribute_weights = WeightedIndex::new([rates.none, rates.native, rates.abeast, rates.machine, rates.dark, rates.hit]).unwrap();
  216. let attr = match attribute_weights.sample(rng) {
  217. 0 => return None,
  218. 1 => Attribute::Native,
  219. 2 => Attribute::ABeast,
  220. 3 => Attribute::Machine,
  221. 4 => Attribute::Dark,
  222. 5 => Attribute::Hit,
  223. _ => panic!()
  224. };
  225. let percents = self.percent_rates.get_by_pattern(pattern);
  226. let value_weights = WeightedIndex::new(percents.as_array()).unwrap();
  227. let value = value_weights.sample(rng);
  228. let percent = ((value + 1) * 5) as i8;
  229. Some(WeaponAttribute {
  230. attr,
  231. value: percent
  232. })
  233. }
  234. fn attributes<R: Rng>(&self, percent_pattern: &AttributePercentPattern, attribute_rate: &AttributeRate, rng: &mut R) -> [Option<WeaponAttribute>; 3] {
  235. let mut percents = vec![
  236. percent_pattern.attribute1.and_then(|pattern_type| {
  237. self.generate_attribute(&pattern_type, attribute_rate, rng)
  238. }),
  239. percent_pattern.attribute2.and_then(|pattern_type| {
  240. self.generate_attribute(&pattern_type, attribute_rate, rng)
  241. }),
  242. percent_pattern.attribute3.and_then(|pattern_type| {
  243. self.generate_attribute(&pattern_type, attribute_rate, rng)
  244. }),
  245. ];
  246. percents.sort_by_key(|p| {
  247. p.as_ref().map(|a| a.attr)
  248. });
  249. percents.dedup_by_key(|p| {
  250. p.as_ref().map(|a| a.attr)
  251. });
  252. percents.iter()
  253. .fold(([None; 3], 0), |(mut acc, index), p| { // one day I'll be able to collece into an array
  254. if p.is_some() {
  255. acc[index] = *p;
  256. (acc, index + 1)
  257. }
  258. else {
  259. (acc, index)
  260. }
  261. }).0
  262. }
  263. fn generate_attributes<R: Rng>(&self, map_area: &MapArea, rng: &mut R) -> [Option<WeaponAttribute>; 3] {
  264. let percent_pattern = self.area_percent_patterns.get_by_area(map_area);
  265. let attribute_rate = self.attribute_rates.get_by_area(map_area);
  266. self.attributes(&percent_pattern, &attribute_rate, rng)
  267. }
  268. pub fn generate_rare_attributes<R: Rng>(&self, map_area: &MapArea, rng: &mut R) -> [Option<WeaponAttribute>; 3] {
  269. let percent_pattern = AttributePercentPattern {
  270. attribute1: Some(PercentPatternType::Pattern6),
  271. attribute2: Some(PercentPatternType::Pattern6),
  272. attribute3: Some(PercentPatternType::Pattern6),
  273. };
  274. let attribute_rate = self.attribute_rates.get_by_area(map_area);
  275. self.attributes(&percent_pattern, &attribute_rate, rng)
  276. }
  277. }
  278. #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
  279. struct SpecialRate {
  280. rank: u32,
  281. rate: u32,
  282. }
  283. #[derive(Debug, Serialize, Deserialize, Copy, Clone)]
  284. struct SpecialRates {
  285. area1: SpecialRate,
  286. area2: SpecialRate,
  287. area3: SpecialRate,
  288. area4: SpecialRate,
  289. area5: SpecialRate,
  290. area6: SpecialRate,
  291. area7: SpecialRate,
  292. area8: SpecialRate,
  293. area9: SpecialRate,
  294. area10: SpecialRate,
  295. }
  296. impl SpecialRates {
  297. fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> SpecialRates {
  298. load_data_file(episode, difficulty, section_id, "weapon_special_rate.toml")
  299. }
  300. fn rate_by_area(&self, map_area: &MapArea) -> SpecialRate {
  301. match map_area.drop_area_value().unwrap() {
  302. 0 => self.area1,
  303. 1 => self.area2,
  304. 2 => self.area3,
  305. 3 => self.area4,
  306. 4 => self.area5,
  307. 5 => self.area6,
  308. 6 => self.area7,
  309. 7=> self.area8,
  310. 8 => self.area9,
  311. _ => self.area10,
  312. }
  313. }
  314. fn random_special_by_rank<R: Rng>(&self, rank: u32, rng: &mut R) -> WeaponSpecial {
  315. let specials = match rank {
  316. 1 => vec![WeaponSpecial::Draw, WeaponSpecial::Heart, WeaponSpecial::Ice, WeaponSpecial::Bind, WeaponSpecial::Heat, WeaponSpecial::Shock,
  317. WeaponSpecial::Dim, WeaponSpecial::Panic],
  318. 2 => vec![WeaponSpecial::Drain, WeaponSpecial::Mind, WeaponSpecial::Frost, WeaponSpecial::Hold, WeaponSpecial::Fire, WeaponSpecial::Thunder,
  319. WeaponSpecial::Shadow, WeaponSpecial::Riot, WeaponSpecial::Masters, WeaponSpecial::Charge],
  320. 3 => vec![WeaponSpecial::Fill, WeaponSpecial::Soul, WeaponSpecial::Freeze, WeaponSpecial::Seize, WeaponSpecial::Flame, WeaponSpecial::Storm,
  321. WeaponSpecial::Dark, WeaponSpecial::Havoc, WeaponSpecial::Lords, WeaponSpecial::Charge, WeaponSpecial::Spirit, WeaponSpecial::Devils],
  322. 4 => vec![WeaponSpecial::Gush, WeaponSpecial::Geist, WeaponSpecial::Blizzard, WeaponSpecial::Arrest, WeaponSpecial::Burning, WeaponSpecial::Tempest,
  323. WeaponSpecial::Hell, WeaponSpecial::Chaos, WeaponSpecial::Kings, WeaponSpecial::Charge, WeaponSpecial::Berserk, WeaponSpecial::Demons],
  324. _ => panic!(),
  325. };
  326. *specials.choose(rng).unwrap()
  327. }
  328. fn get_special<R: Rng>(&self, map_area: &MapArea, rng: &mut R) -> Option<WeaponSpecial> {
  329. let rate = self.rate_by_area(map_area);
  330. if rng.gen_range(0, 100) < rate.rate {
  331. Some(self.random_special_by_rank(rate.rank, rng))
  332. }
  333. else {
  334. None
  335. }
  336. }
  337. }
  338. pub struct GenericWeaponTable {
  339. rank_table: HashMap<WeaponDropType, Vec<WeaponType>>,
  340. weapon_ratio: WeaponRatios,
  341. grind_rates: GrindRates,
  342. attribute_table: AttributeTable,
  343. special_table: SpecialRates,
  344. }
  345. impl GenericWeaponTable {
  346. pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> GenericWeaponTable {
  347. let mut rank_table = HashMap::new();
  348. rank_table.insert(WeaponDropType::Saber, vec![WeaponType::Saber, WeaponType::Brand, WeaponType::Buster, WeaponType::Pallasch, WeaponType::Gladius]);
  349. rank_table.insert(WeaponDropType::Sword, vec![WeaponType::Sword, WeaponType::Gigush, WeaponType::Breaker, WeaponType::Claymore, WeaponType::Calibur]);
  350. rank_table.insert(WeaponDropType::Dagger, vec![WeaponType::Dagger, WeaponType::Knife, WeaponType::Blade, WeaponType::Edge, WeaponType::Ripper]);
  351. rank_table.insert(WeaponDropType::Partisan, vec![WeaponType::Partisan, WeaponType::Halbert, WeaponType::Glaive, WeaponType::Berdys, WeaponType::Gungnir]);
  352. rank_table.insert(WeaponDropType::Slicer, vec![WeaponType::Slicer, WeaponType::Spinner, WeaponType::Cutter, WeaponType::Sawcer, WeaponType::Diska]);
  353. rank_table.insert(WeaponDropType::Handgun, vec![WeaponType::Handgun, WeaponType::Autogun, WeaponType::Lockgun, WeaponType::Railgun, WeaponType::Raygun]);
  354. rank_table.insert(WeaponDropType::Rifle, vec![WeaponType::Rifle, WeaponType::Sniper, WeaponType::Blaster, WeaponType::Beam, WeaponType::Laser]);
  355. rank_table.insert(WeaponDropType::Mechgun, vec![WeaponType::Mechgun, WeaponType::Assault, WeaponType::Repeater, WeaponType::Gatling, WeaponType::Vulcan]);
  356. rank_table.insert(WeaponDropType::Shot, vec![WeaponType::Shot, WeaponType::Spread, WeaponType::Cannon, WeaponType::Launcher, WeaponType::Arms]);
  357. rank_table.insert(WeaponDropType::Cane, vec![WeaponType::Cane, WeaponType::Stick, WeaponType::Mace, WeaponType::Club]);
  358. rank_table.insert(WeaponDropType::Rod, vec![WeaponType::Rod, WeaponType::Pole, WeaponType::Pillar, WeaponType::Striker]);
  359. rank_table.insert(WeaponDropType::Wand, vec![WeaponType::Wand, WeaponType::Staff, WeaponType::Baton, WeaponType::Scepter]);
  360. GenericWeaponTable {
  361. rank_table,
  362. weapon_ratio: WeaponRatios::new(episode, difficulty, section_id),
  363. grind_rates: GrindRates::new(episode, difficulty, section_id),
  364. attribute_table: AttributeTable::new(episode, difficulty, section_id),
  365. special_table: SpecialRates::new(episode, difficulty, section_id),
  366. }
  367. }
  368. fn area_rank(&self, ratio: &WeaponRatio, map_area: &MapArea) -> (u32, u32) {
  369. if ratio.rank >= 0 {
  370. (map_area.drop_area_value().unwrap_or(0), ratio.rank as u32)
  371. }
  372. else {
  373. ((map_area.drop_area_value().unwrap_or(0) as i32 + ratio.rank) as u32, 0)
  374. }
  375. }
  376. fn get_possible_weapon_types(&self, map_area: &MapArea) -> BTreeMap<WeaponDropType, WeaponRatio> {
  377. let mut valid_weapons = BTreeMap::new();
  378. let item_rate_pairs = vec![
  379. (WeaponDropType::Saber, self.weapon_ratio.saber),
  380. (WeaponDropType::Sword, self.weapon_ratio.sword),
  381. (WeaponDropType::Dagger, self.weapon_ratio.dagger),
  382. (WeaponDropType::Partisan, self.weapon_ratio.partisan),
  383. (WeaponDropType::Slicer, self.weapon_ratio.slicer),
  384. (WeaponDropType::Handgun, self.weapon_ratio.handgun),
  385. (WeaponDropType::Rifle, self.weapon_ratio.rifle),
  386. (WeaponDropType::Mechgun, self.weapon_ratio.mechgun),
  387. (WeaponDropType::Shot, self.weapon_ratio.shot),
  388. (WeaponDropType::Cane, self.weapon_ratio.cane),
  389. (WeaponDropType::Rod, self.weapon_ratio.rod),
  390. (WeaponDropType::Wand, self.weapon_ratio.wand),
  391. ];
  392. for (item_type, ratio) in item_rate_pairs.into_iter() {
  393. if ratio.rate > 0 && map_area.drop_area_value().map(|k| k as i32 + ratio.rank).unwrap_or(-1) >= 0 {
  394. valid_weapons.insert(item_type, ratio);
  395. }
  396. }
  397. valid_weapons
  398. }
  399. fn weapon_type<R: Rng>(&self, possible_weapon_types: &BTreeMap<WeaponDropType, WeaponRatio>, _map_area: &MapArea, rng: &mut R) -> WeaponDropType {
  400. let mut weapon_rates = possible_weapon_types.iter()
  401. .map(|(weapon, stat)| {
  402. (weapon, stat.rate)
  403. });
  404. let weapon_choice = WeightedIndex::new(weapon_rates.clone().map(|wr| wr.1)).unwrap();
  405. *weapon_rates.nth(weapon_choice.sample(rng)).unwrap().0
  406. }
  407. fn weapon_rank(&self, ratio: &WeaponRatio, map_area: &MapArea) -> u32 {
  408. let (area, rank) = self.area_rank(ratio, map_area);
  409. let weapon_rank = rank + area / ratio.inc;
  410. std::cmp::max(weapon_rank, 0)
  411. }
  412. fn actual_weapon(&self, weapon_type: &WeaponDropType, weapon_rank: u32) -> WeaponType {
  413. let weapons = self.rank_table.get(weapon_type).unwrap();
  414. weapons[std::cmp::min(weapons.len() - 1, weapon_rank as usize)]
  415. }
  416. fn get_grind<R: Rng>(&self, ratio: &WeaponRatio, map_area: &MapArea, rng: &mut R) -> usize {
  417. let (area, _) = self.area_rank(ratio, map_area);
  418. let pattern = std::cmp::min(area % ratio.inc, 3);
  419. let weights = self.grind_rates.grind_rate[pattern as usize];
  420. let grind_choice = WeightedIndex::new(weights).unwrap();
  421. grind_choice.sample(rng)
  422. }
  423. pub fn get_drop<R: Rng>(&self, map_area: &MapArea, rng: &mut R) -> Option<ItemDropType> {
  424. let possible_weapon_types = self.get_possible_weapon_types(map_area);
  425. let weapon_type = self.weapon_type(&possible_weapon_types, map_area, rng);
  426. let ratio = possible_weapon_types.get(&weapon_type).unwrap();
  427. let weapon_rank = self.weapon_rank(ratio, map_area);
  428. let weapon_grind = self.get_grind(ratio, map_area, rng);
  429. let weapon_attributes = self.attribute_table.generate_attributes(map_area, rng);
  430. let weapon_special = self.special_table.get_special(map_area, rng);
  431. let actual_weapon = self.actual_weapon(&weapon_type, weapon_rank);
  432. Some(ItemDropType::Weapon(Weapon {
  433. weapon: actual_weapon,
  434. special: weapon_special,
  435. grind: weapon_grind as u8,
  436. attrs: weapon_attributes,
  437. tekked: weapon_special.is_none(),
  438. }))
  439. }
  440. }
  441. #[cfg(test)]
  442. mod test {
  443. use super::*;
  444. use rand::{SeedableRng};
  445. #[test]
  446. fn test_weapon_generation() {
  447. let mut rng = rand_chacha::ChaCha20Rng::from_seed([23;32]);
  448. let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Normal, SectionID::Skyly);
  449. assert!(gwt.get_drop(&MapArea::Forest1, &mut rng) == Some(ItemDropType::Weapon(Weapon {
  450. weapon: WeaponType::Cane,
  451. special: None,
  452. grind: 0,
  453. attrs: [None, None, None],
  454. tekked: true,
  455. })));
  456. let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Hard, SectionID::Skyly);
  457. assert!(gwt.get_drop(&MapArea::Caves2, &mut rng) == Some(ItemDropType::Weapon(Weapon {
  458. weapon: WeaponType::Sniper,
  459. special: None,
  460. grind: 2,
  461. attrs: [None, None, None],
  462. tekked: true,
  463. })));
  464. let gwt = GenericWeaponTable::new(Episode::One, Difficulty::VeryHard, SectionID::Skyly);
  465. assert!(gwt.get_drop(&MapArea::Mines1, &mut rng) == Some(ItemDropType::Weapon(Weapon {
  466. weapon: WeaponType::Club,
  467. special: Some(WeaponSpecial::Berserk),
  468. grind: 0,
  469. attrs: [None, None, None],
  470. tekked: false,
  471. })));
  472. let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Ultimate, SectionID::Skyly);
  473. assert!(gwt.get_drop(&MapArea::DarkFalz, &mut rng) == Some(ItemDropType::Weapon(Weapon {
  474. weapon: WeaponType::Vulcan,
  475. special: None,
  476. grind: 0,
  477. attrs: [Some(WeaponAttribute {attr: Attribute::ABeast, value: 30}), Some(WeaponAttribute {attr: Attribute::Dark, value: 30}), None],
  478. tekked: true,
  479. })));
  480. }
  481. }