721 lines
19 KiB
Rust
721 lines
19 KiB
Rust
use crate::constants;
|
||
use crate::error::DomainError;
|
||
use crate::spells;
|
||
use sb_shared::{Character, CharacterStats, Class, Race};
|
||
|
||
pub struct TrainingGround {
|
||
pub stat: &'static str,
|
||
pub name: &'static str,
|
||
pub description: &'static str,
|
||
}
|
||
|
||
pub const TRAINING_GROUNDS: &[TrainingGround] = &[
|
||
TrainingGround {
|
||
stat: "strength",
|
||
name: "The Anvil",
|
||
description: "Pound steel and lift stone under the guidance of master smiths.",
|
||
},
|
||
TrainingGround {
|
||
stat: "dexterity",
|
||
name: "The Archery Range",
|
||
description: "Sharpen reflexes with bow, blade, and balance drills.",
|
||
},
|
||
TrainingGround {
|
||
stat: "constitution",
|
||
name: "The Endurance Pits",
|
||
description: "Push your body past its limits in grueling trials of resilience.",
|
||
},
|
||
TrainingGround {
|
||
stat: "intelligence",
|
||
name: "The Sage's Study",
|
||
description: "Study ancient tomes and practice arcane disciplines.",
|
||
},
|
||
TrainingGround {
|
||
stat: "luck",
|
||
name: "Fortune's Shrine",
|
||
description: "Consult oracles and tempt fate in games of chance.",
|
||
},
|
||
];
|
||
|
||
pub struct RaceInfo {
|
||
pub race: Race,
|
||
pub name: &'static str,
|
||
pub str_mod: i64,
|
||
pub int_mod: i64,
|
||
pub dex_mod: i64,
|
||
pub con_mod: i64,
|
||
pub luck_mod: i64,
|
||
}
|
||
|
||
pub const RACES: &[RaceInfo] = &[
|
||
RaceInfo {
|
||
race: Race::Human,
|
||
name: "Human",
|
||
str_mod: 0,
|
||
int_mod: 0,
|
||
dex_mod: 0,
|
||
con_mod: 0,
|
||
luck_mod: 0,
|
||
},
|
||
RaceInfo {
|
||
race: Race::Elf,
|
||
name: "Elf",
|
||
str_mod: -2,
|
||
int_mod: 4,
|
||
dex_mod: 2,
|
||
con_mod: -2,
|
||
luck_mod: 0,
|
||
},
|
||
RaceInfo {
|
||
race: Race::Dwarf,
|
||
name: "Dwarf",
|
||
str_mod: 2,
|
||
int_mod: -2,
|
||
dex_mod: -2,
|
||
con_mod: 4,
|
||
luck_mod: 0,
|
||
},
|
||
RaceInfo {
|
||
race: Race::Hobbit,
|
||
name: "Hobbit",
|
||
str_mod: -4,
|
||
int_mod: 0,
|
||
dex_mod: 2,
|
||
con_mod: -2,
|
||
luck_mod: 4,
|
||
},
|
||
RaceInfo {
|
||
race: Race::HalfElf,
|
||
name: "Half-Elf",
|
||
str_mod: 0,
|
||
int_mod: 2,
|
||
dex_mod: 2,
|
||
con_mod: 0,
|
||
luck_mod: 0,
|
||
},
|
||
];
|
||
|
||
pub struct ClassInfo {
|
||
pub class: Class,
|
||
pub name: &'static str,
|
||
pub primary_stat: &'static str,
|
||
pub secondary_stat: &'static str,
|
||
pub base_hp: i32,
|
||
pub hp_per_con: f64,
|
||
}
|
||
|
||
pub const CLASSES: &[ClassInfo] = &[
|
||
ClassInfo {
|
||
class: Class::Warrior,
|
||
name: "Warrior",
|
||
primary_stat: "strength",
|
||
secondary_stat: "constitution",
|
||
base_hp: 20,
|
||
hp_per_con: 3.0,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Paladin,
|
||
name: "Paladin",
|
||
primary_stat: "strength",
|
||
secondary_stat: "luck",
|
||
base_hp: 18,
|
||
hp_per_con: 2.5,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Rogue,
|
||
name: "Rogue",
|
||
primary_stat: "dexterity",
|
||
secondary_stat: "luck",
|
||
base_hp: 14,
|
||
hp_per_con: 2.0,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Bard,
|
||
name: "Bard",
|
||
primary_stat: "dexterity",
|
||
secondary_stat: "intelligence",
|
||
base_hp: 14,
|
||
hp_per_con: 2.0,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Hunter,
|
||
name: "Hunter",
|
||
primary_stat: "dexterity",
|
||
secondary_stat: "strength",
|
||
base_hp: 16,
|
||
hp_per_con: 2.5,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Monk,
|
||
name: "Monk",
|
||
primary_stat: "dexterity",
|
||
secondary_stat: "constitution",
|
||
base_hp: 15,
|
||
hp_per_con: 2.5,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Mage,
|
||
name: "Mage",
|
||
primary_stat: "intelligence",
|
||
secondary_stat: "dexterity",
|
||
base_hp: 11,
|
||
hp_per_con: 1.8,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Wizard,
|
||
name: "Wizard",
|
||
primary_stat: "intelligence",
|
||
secondary_stat: "constitution",
|
||
base_hp: 10,
|
||
hp_per_con: 1.5,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Conjurer,
|
||
name: "Conjurer",
|
||
primary_stat: "intelligence",
|
||
secondary_stat: "luck",
|
||
base_hp: 10,
|
||
hp_per_con: 1.5,
|
||
},
|
||
ClassInfo {
|
||
class: Class::Sorcerer,
|
||
name: "Sorcerer",
|
||
primary_stat: "intelligence",
|
||
secondary_stat: "strength",
|
||
base_hp: 12,
|
||
hp_per_con: 2.0,
|
||
},
|
||
];
|
||
|
||
pub fn stat_bonus(stat: i64) -> i64 {
|
||
(stat - constants::STAT_BONUS_NEUTRAL) / constants::STAT_BONUS_STEP
|
||
}
|
||
|
||
fn roll_stat(rng: &mut impl rand::Rng, modifier: i64) -> i64 {
|
||
let base: i64 = rng.random_range(constants::STAT_ROLL_MIN..=constants::STAT_ROLL_MAX);
|
||
base + modifier
|
||
}
|
||
|
||
pub fn roll_stats(rng: &mut impl rand::Rng, race: &Race) -> CharacterStats {
|
||
let info = RACES
|
||
.iter()
|
||
.find(|r| &r.race == race)
|
||
.expect("invalid race");
|
||
CharacterStats {
|
||
strength: roll_stat(rng, info.str_mod),
|
||
intelligence: roll_stat(rng, info.int_mod),
|
||
dexterity: roll_stat(rng, info.dex_mod),
|
||
constitution: roll_stat(rng, info.con_mod),
|
||
luck: roll_stat(rng, info.luck_mod),
|
||
}
|
||
}
|
||
|
||
pub fn calc_max_hp(class: &Class, stats: &CharacterStats) -> i32 {
|
||
let info = CLASSES
|
||
.iter()
|
||
.find(|c| &c.class == class)
|
||
.expect("invalid class");
|
||
(info.base_hp as f64 + stats.constitution as f64 * info.hp_per_con) as i32
|
||
}
|
||
|
||
pub fn calc_max_energy(class: &Class) -> i32 {
|
||
let info = CLASSES
|
||
.iter()
|
||
.find(|c| &c.class == class)
|
||
.expect("invalid class");
|
||
constants::BASE_MAX_ENERGY + info.base_hp / constants::MAX_ENERGY_PER_BASE_HP_DIVISOR
|
||
}
|
||
|
||
pub fn calc_max_nerve(class: &Class) -> i32 {
|
||
let info = CLASSES
|
||
.iter()
|
||
.find(|c| &c.class == class)
|
||
.expect("invalid class");
|
||
constants::BASE_MAX_NERVE + info.base_hp / constants::MAX_NERVE_PER_BASE_HP_DIVISOR
|
||
}
|
||
|
||
pub fn energy_regen_rate() -> i64 {
|
||
constants::ENERGY_REGEN_INTERVAL_MS
|
||
}
|
||
|
||
pub fn nerve_regen_rate() -> i64 {
|
||
constants::NERVE_REGEN_INTERVAL_MS
|
||
}
|
||
|
||
pub fn hp_regen_rate() -> i64 {
|
||
constants::HP_REGEN_INTERVAL_MS
|
||
}
|
||
|
||
pub fn apply_regen(char: &mut Character, now_ms: i64) {
|
||
let elapsed = now_ms - char.last_regen;
|
||
if elapsed <= 0 {
|
||
return;
|
||
}
|
||
let energy_ticks = (elapsed / energy_regen_rate()) as i32;
|
||
let nerve_ticks = (elapsed / nerve_regen_rate()) as i32;
|
||
let hp_ticks = (elapsed / hp_regen_rate()) as i32;
|
||
if energy_ticks > 0 {
|
||
char.energy = (char.energy + energy_ticks).min(char.max_energy);
|
||
}
|
||
if nerve_ticks > 0 {
|
||
char.nerve = (char.nerve + nerve_ticks).min(char.max_nerve);
|
||
}
|
||
if hp_ticks > 0 && char.hp > 0 && char.hp < char.max_hp {
|
||
let regen_per_tick =
|
||
(char.max_hp as f64 * constants::HP_REGEN_FRACTION_PER_TICK).ceil() as i32;
|
||
char.hp = (char.hp + regen_per_tick * hp_ticks).min(char.max_hp);
|
||
}
|
||
char.last_regen = now_ms;
|
||
}
|
||
|
||
pub fn training_cost(stat: i64) -> i32 {
|
||
let scaled = (stat as f64 / constants::TRAINING_STAT_SCALER).floor() as i32;
|
||
(constants::TRAINING_BASE_COST * constants::TRAINING_GROWTH_FACTOR.powi(scaled)) as i32
|
||
}
|
||
|
||
pub fn create_character(
|
||
rng: &mut impl rand::Rng,
|
||
user_id: u32,
|
||
name: String,
|
||
race: Race,
|
||
class: Class,
|
||
) -> Character {
|
||
let class_str = class.as_str().to_string();
|
||
let stats = roll_stats(rng, &race);
|
||
let max_hp = calc_max_hp(&class, &stats);
|
||
let max_energy = calc_max_energy(&class);
|
||
let max_nerve = calc_max_nerve(&class);
|
||
|
||
let known_spells: Vec<String> = if spells::is_caster_class(&class_str) {
|
||
spells::spells_for_class(&class_str, 1)
|
||
.iter()
|
||
.filter(|s| s.spell_level <= 1)
|
||
.map(|s| s.id.clone())
|
||
.collect()
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
|
||
Character {
|
||
id: 0,
|
||
user_id,
|
||
name,
|
||
race,
|
||
class,
|
||
level: constants::STARTING_LEVEL,
|
||
xp: 0,
|
||
stats,
|
||
hp: max_hp,
|
||
max_hp,
|
||
energy: max_energy,
|
||
max_energy,
|
||
nerve: max_nerve,
|
||
max_nerve,
|
||
gold: constants::STARTING_GOLD,
|
||
last_regen: 0,
|
||
equipment: [
|
||
("head".into(), None),
|
||
("body".into(), None),
|
||
("off_hand".into(), None),
|
||
("main_hand".into(), None),
|
||
("hands".into(), None),
|
||
("feet".into(), None),
|
||
("back".into(), None),
|
||
("neck".into(), None),
|
||
("ring".into(), None),
|
||
("trinket".into(), None),
|
||
("instrument".into(), None),
|
||
]
|
||
.into_iter()
|
||
.collect(),
|
||
inventory: Vec::new(),
|
||
active_song: None,
|
||
known_spells,
|
||
active_spell: None,
|
||
spell_points: spells::max_spell_points(&class_str, 1),
|
||
max_spell_points: spells::max_spell_points(&class_str, 1),
|
||
}
|
||
}
|
||
|
||
pub const MAX_LEVEL: i32 = 100;
|
||
pub const XP_FACTOR: i64 = 18;
|
||
|
||
pub fn xp_for_level(level: i32) -> i64 {
|
||
if level <= 1 {
|
||
return 0;
|
||
}
|
||
let n = (level - 1) as f64;
|
||
XP_FACTOR * (constants::XP_CURVE_COEFFICIENT * n * n) as i64
|
||
}
|
||
|
||
pub fn level_up_cost(level: i32) -> i32 {
|
||
constants::LEVEL_UP_GOLD_PER_LEVEL * level
|
||
}
|
||
|
||
pub fn level_up(char: &mut Character) -> Result<i32, DomainError> {
|
||
if char.level >= MAX_LEVEL {
|
||
return Err(DomainError::AlreadyAtMaxLevel);
|
||
}
|
||
let next_level = char.level + 1;
|
||
if char.xp < xp_for_level(next_level) {
|
||
return Err(DomainError::NotEnoughXp);
|
||
}
|
||
let cost = level_up_cost(next_level);
|
||
if char.gold < cost {
|
||
return Err(DomainError::NotEnoughGold);
|
||
}
|
||
char.gold -= cost;
|
||
char.level = next_level;
|
||
let class_str = char.class.as_str().to_string();
|
||
char.max_hp = calc_max_hp(&char.class, &char.stats);
|
||
char.hp = char.max_hp;
|
||
char.max_energy = calc_max_energy(&char.class);
|
||
char.energy = char.max_energy;
|
||
char.max_nerve = calc_max_nerve(&char.class);
|
||
char.nerve = char.max_nerve;
|
||
char.max_spell_points = spells::max_spell_points(&class_str, char.level);
|
||
char.spell_points = char.max_spell_points;
|
||
Ok(char.level)
|
||
}
|
||
|
||
pub fn find_race_info(race: &Race) -> &'static RaceInfo {
|
||
RACES
|
||
.iter()
|
||
.find(|r| &r.race == race)
|
||
.expect("invalid race")
|
||
}
|
||
|
||
pub fn find_class_info(class: &Class) -> &'static ClassInfo {
|
||
CLASSES
|
||
.iter()
|
||
.find(|c| &c.class == class)
|
||
.expect("invalid class")
|
||
}
|
||
|
||
/// Gym stat display order — pinned to the get_gym handler's fixed array
|
||
/// (routes/mod.rs:588–594), deliberately differs from TRAINING_GROUNDS order.
|
||
pub const GYM_STAT_DISPLAY_ORDER: &[&str] = &[
|
||
"strength",
|
||
"intelligence",
|
||
"dexterity",
|
||
"constitution",
|
||
"luck",
|
||
];
|
||
|
||
pub fn stat_value(stats: &CharacterStats, stat: &str) -> Option<i64> {
|
||
match stat {
|
||
"strength" => Some(stats.strength),
|
||
"intelligence" => Some(stats.intelligence),
|
||
"dexterity" => Some(stats.dexterity),
|
||
"constitution" => Some(stats.constitution),
|
||
"luck" => Some(stats.luck),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use sb_shared::{Character, CharacterStats, Class, Race};
|
||
use std::collections::HashMap;
|
||
|
||
fn base_char() -> Character {
|
||
Character {
|
||
id: 1,
|
||
user_id: 1,
|
||
name: "Test".into(),
|
||
race: Race::Human,
|
||
class: Class::Warrior,
|
||
level: 1,
|
||
xp: 0,
|
||
stats: CharacterStats {
|
||
strength: 20,
|
||
intelligence: 20,
|
||
dexterity: 20,
|
||
constitution: 20,
|
||
luck: 20,
|
||
},
|
||
hp: 80,
|
||
max_hp: 80,
|
||
energy: 36,
|
||
max_energy: 36,
|
||
nerve: 110,
|
||
max_nerve: 110,
|
||
gold: 0,
|
||
last_regen: 0,
|
||
equipment: HashMap::new(),
|
||
inventory: Vec::new(),
|
||
active_song: None,
|
||
known_spells: Vec::new(),
|
||
active_spell: None,
|
||
spell_points: 0,
|
||
max_spell_points: 0,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn stat_bonus_at_20_is_zero() {
|
||
assert_eq!(stat_bonus(20), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn stat_bonus_below_20_is_negative() {
|
||
assert_eq!(stat_bonus(10), -5);
|
||
assert_eq!(stat_bonus(1), -9);
|
||
}
|
||
|
||
#[test]
|
||
fn stat_bonus_above_20_is_positive() {
|
||
assert_eq!(stat_bonus(30), 5);
|
||
assert_eq!(stat_bonus(100), 40);
|
||
}
|
||
|
||
#[test]
|
||
fn stat_bonus_handles_large_values() {
|
||
assert_eq!(stat_bonus(1_000_000_000), 499_999_990);
|
||
}
|
||
|
||
#[test]
|
||
fn xp_for_level_1_is_zero() {
|
||
assert_eq!(xp_for_level(1), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn xp_for_level_2_is_1800() {
|
||
assert_eq!(xp_for_level(2), 1800);
|
||
}
|
||
|
||
#[test]
|
||
fn xp_for_level_scales_subexponentially() {
|
||
assert!(xp_for_level(3) > xp_for_level(2));
|
||
assert!(xp_for_level(5) > xp_for_level(4));
|
||
assert!(xp_for_level(10) > xp_for_level(5));
|
||
assert!(xp_for_level(50) > xp_for_level(25));
|
||
}
|
||
|
||
#[test]
|
||
fn xp_for_level_does_not_overflow() {
|
||
assert!(xp_for_level(50) > 0);
|
||
assert!(xp_for_level(50) < i64::MAX / 2);
|
||
}
|
||
|
||
#[test]
|
||
fn level_up_fails_at_max_level() {
|
||
let mut c = base_char();
|
||
c.level = MAX_LEVEL;
|
||
c.xp = i64::MAX;
|
||
c.gold = i32::MAX;
|
||
assert_eq!(level_up(&mut c), Err(DomainError::AlreadyAtMaxLevel));
|
||
}
|
||
|
||
#[test]
|
||
fn level_up_cost_scales() {
|
||
assert_eq!(level_up_cost(2), 1000);
|
||
assert_eq!(level_up_cost(3), 1500);
|
||
assert_eq!(level_up_cost(10), 5000);
|
||
}
|
||
|
||
#[test]
|
||
fn level_up_succeeds_with_enough_xp_and_gold() {
|
||
let mut c = base_char();
|
||
c.xp = 1800;
|
||
c.gold = 1000;
|
||
let result = level_up(&mut c);
|
||
assert_eq!(result, Ok(2));
|
||
assert_eq!(c.level, 2);
|
||
assert_eq!(c.gold, 0);
|
||
assert_eq!(c.hp, c.max_hp);
|
||
assert_eq!(c.energy, c.max_energy);
|
||
assert_eq!(c.nerve, c.max_nerve);
|
||
}
|
||
|
||
#[test]
|
||
fn level_up_fails_without_enough_xp() {
|
||
let mut c = base_char();
|
||
c.xp = 100;
|
||
c.gold = 10000;
|
||
assert_eq!(level_up(&mut c), Err(DomainError::NotEnoughXp));
|
||
assert_eq!(c.level, 1);
|
||
assert_eq!(c.gold, 10000);
|
||
}
|
||
|
||
#[test]
|
||
fn level_up_fails_without_enough_gold() {
|
||
let mut c = base_char();
|
||
c.xp = 1800;
|
||
c.gold = 0;
|
||
assert_eq!(level_up(&mut c), Err(DomainError::NotEnoughGold));
|
||
assert_eq!(c.level, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn calc_max_hp_depends_on_class_and_con() {
|
||
let stats_high = CharacterStats {
|
||
strength: 10,
|
||
intelligence: 10,
|
||
dexterity: 10,
|
||
constitution: 30,
|
||
luck: 10,
|
||
};
|
||
let stats_low = CharacterStats {
|
||
strength: 10,
|
||
intelligence: 10,
|
||
dexterity: 10,
|
||
constitution: 10,
|
||
luck: 10,
|
||
};
|
||
let hp_high = calc_max_hp(&Class::Warrior, &stats_high);
|
||
let hp_low = calc_max_hp(&Class::Warrior, &stats_low);
|
||
assert!(hp_high > hp_low);
|
||
}
|
||
|
||
#[test]
|
||
fn calc_max_energy_and_nerve_vary_by_class() {
|
||
let warrior_energy = calc_max_energy(&Class::Warrior);
|
||
let mage_energy = calc_max_energy(&Class::Mage);
|
||
assert!(warrior_energy > mage_energy);
|
||
}
|
||
|
||
#[test]
|
||
fn training_cost_scales_with_stat() {
|
||
assert!(training_cost(20) < training_cost(30));
|
||
assert!(training_cost(30) < training_cost(40));
|
||
}
|
||
|
||
#[test]
|
||
fn apply_regen_restores_resources() {
|
||
let mut c = base_char();
|
||
c.hp = 40;
|
||
c.energy = 10;
|
||
c.nerve = 50;
|
||
c.last_regen = 0;
|
||
apply_regen(&mut c, 1_800_000);
|
||
assert!(c.energy > 10);
|
||
assert!(c.nerve > 50);
|
||
assert!(c.hp > 40);
|
||
}
|
||
|
||
#[test]
|
||
fn apply_regen_no_change_if_no_time_passed() {
|
||
let mut c = base_char();
|
||
c.hp = 40;
|
||
c.last_regen = 1_800_000;
|
||
let before = c.hp;
|
||
apply_regen(&mut c, 1_800_000);
|
||
assert_eq!(c.hp, before);
|
||
}
|
||
|
||
#[test]
|
||
fn roll_stats_returns_all_five() {
|
||
use rand::SeedableRng;
|
||
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||
let stats = roll_stats(&mut rng, &Race::Human);
|
||
assert!(stats.strength >= 6 && stats.strength <= 16);
|
||
assert!(stats.intelligence >= 6 && stats.intelligence <= 16);
|
||
assert!(stats.dexterity >= 6 && stats.dexterity <= 16);
|
||
assert!(stats.constitution >= 6 && stats.constitution <= 16);
|
||
assert!(stats.luck >= 6 && stats.luck <= 16);
|
||
}
|
||
|
||
#[test]
|
||
fn race_modifiers_apply() {
|
||
use rand::SeedableRng;
|
||
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||
let human = roll_stats(&mut rng, &Race::Human);
|
||
let mut rng2 = rand::rngs::StdRng::seed_from_u64(42);
|
||
let dwarf = roll_stats(&mut rng2, &Race::Dwarf);
|
||
assert!(dwarf.constitution > human.constitution);
|
||
assert!(dwarf.strength > human.strength);
|
||
assert!(dwarf.intelligence < human.intelligence);
|
||
}
|
||
|
||
#[test]
|
||
fn create_character_is_deterministic_under_seeded_rng() {
|
||
use rand::SeedableRng;
|
||
let first = create_character(
|
||
&mut rand::rngs::StdRng::seed_from_u64(42),
|
||
7,
|
||
"Twin".into(),
|
||
Race::Human,
|
||
Class::Warrior,
|
||
);
|
||
let second = create_character(
|
||
&mut rand::rngs::StdRng::seed_from_u64(42),
|
||
7,
|
||
"Twin".into(),
|
||
Race::Human,
|
||
Class::Warrior,
|
||
);
|
||
assert_eq!(format!("{:?}", first.stats), format!("{:?}", second.stats));
|
||
assert_eq!(first.hp, second.hp);
|
||
assert_eq!(first.max_hp, second.max_hp);
|
||
assert_eq!(first.energy, second.energy);
|
||
assert_eq!(first.nerve, second.nerve);
|
||
}
|
||
|
||
#[test]
|
||
fn gym_display_order_covers_all_training_grounds() {
|
||
let stats = CharacterStats {
|
||
strength: 10,
|
||
intelligence: 11,
|
||
dexterity: 12,
|
||
constitution: 13,
|
||
luck: 14,
|
||
};
|
||
assert_eq!(GYM_STAT_DISPLAY_ORDER.len(), TRAINING_GROUNDS.len());
|
||
for name in GYM_STAT_DISPLAY_ORDER {
|
||
assert!(
|
||
TRAINING_GROUNDS.iter().any(|g| g.stat == *name),
|
||
"no training ground for stat {}",
|
||
name
|
||
);
|
||
assert!(
|
||
stat_value(&stats, name).is_some(),
|
||
"stat_value cannot resolve {}",
|
||
name
|
||
);
|
||
}
|
||
for ground in TRAINING_GROUNDS {
|
||
assert!(
|
||
GYM_STAT_DISPLAY_ORDER.contains(&ground.stat),
|
||
"gym display order missing stat {}",
|
||
ground.stat
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn race_table_covers_all_variants() {
|
||
let all = [
|
||
Race::Human,
|
||
Race::Elf,
|
||
Race::Dwarf,
|
||
Race::Hobbit,
|
||
Race::HalfElf,
|
||
];
|
||
for race in &all {
|
||
assert!(RACES.iter().any(|r| &r.race == race));
|
||
let _ = find_race_info(race);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn class_table_covers_all_variants() {
|
||
let all = [
|
||
Class::Warrior,
|
||
Class::Paladin,
|
||
Class::Rogue,
|
||
Class::Bard,
|
||
Class::Hunter,
|
||
Class::Monk,
|
||
Class::Mage,
|
||
Class::Wizard,
|
||
Class::Conjurer,
|
||
Class::Sorcerer,
|
||
];
|
||
for class in &all {
|
||
assert!(CLASSES.iter().any(|c| &c.class == class));
|
||
let _ = find_class_info(class);
|
||
}
|
||
}
|
||
}
|