use crate::compare::STAT_ORDER;
use crate::models::{PokemonDetail, StatKind};
pub const MAX_LEVEL: u8 = 100;
pub const MAX_IV: u8 = 31;
pub const STAT_EV_CAP: u16 = 252;
pub const MAX_EV: u16 = 255;
pub const TOTAL_EV_CAP: u16 = 510;
const NATURE_STATS: [StatKind; 5] = [
StatKind::Attack,
StatKind::Defense,
StatKind::Speed,
StatKind::SpecialAttack,
StatKind::SpecialDefense,
];
const NATURES: [&str; 25] = [
"Hardy", "Lonely", "Brave", "Adamant", "Naughty", "Bold", "Docile", "Relaxed", "Impish", "Lax", "Timid", "Hasty", "Serious", "Jolly", "Naive", "Modest", "Mild", "Quiet", "Bashful", "Rash", "Calm", "Gentle", "Sassy", "Careful", "Quirky",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Nature(u8);
impl Nature {
#[cfg(test)]
pub fn named(name: &str) -> Option<Self> {
NATURES
.iter()
.position(|n| n.eq_ignore_ascii_case(name))
.map(|idx| Nature(idx as u8))
}
pub fn name(self) -> &'static str {
NATURES[self.0 as usize]
}
pub fn raised(self) -> Option<StatKind> {
let (up, down) = (self.0 / 5, self.0 % 5);
(up != down).then_some(NATURE_STATS[up as usize])
}
pub fn lowered(self) -> Option<StatKind> {
let (up, down) = (self.0 / 5, self.0 % 5);
(up != down).then_some(NATURE_STATS[down as usize])
}
fn cycled(self, step: i32) -> Self {
Nature((i32::from(self.0) + step).rem_euclid(NATURES.len() as i32) as u8)
}
}
pub fn final_stat(kind: StatKind, base: u16, iv: u8, ev: u16, level: u8, nature: Nature) -> u16 {
let level = u32::from(level.clamp(1, MAX_LEVEL));
let core = (2 * u32::from(base) + u32::from(iv) + u32::from(ev) / 4) * level / 100;
let value = match kind {
StatKind::Hp if base == 1 => 1,
StatKind::Hp => core + level + 10,
_ => {
let percent = if nature.raised() == Some(kind) {
110
} else if nature.lowered() == Some(kind) {
90
} else {
100
};
(core + 5) * percent / 100
}
};
value as u16
}
pub fn bases(detail: &PokemonDetail) -> Option<[u16; 6]> {
let mut out = [0; 6];
for (slot, kind) in out.iter_mut().zip(STAT_ORDER) {
*slot = detail.stats.iter().find(|s| s.kind == kind)?.base;
}
Some(out)
}
pub const ROWS: usize = 2 + STAT_ORDER.len();
const LEVEL_ROW: usize = 0;
const NATURE_ROW: usize = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Column {
#[default]
Iv,
Ev,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Field {
Level,
Nature,
Iv(usize),
Ev(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Calculator {
pub level: u8,
pub nature: Nature,
pub ivs: [u8; 6],
pub evs: [u16; 6],
pub row: usize,
pub column: Column,
fresh: bool,
}
impl Default for Calculator {
fn default() -> Self {
Calculator {
level: 50,
nature: Nature::default(),
ivs: [MAX_IV; 6],
evs: [0; 6],
row: LEVEL_ROW,
column: Column::Iv,
fresh: true,
}
}
}
impl Calculator {
pub fn field(&self) -> Field {
match (self.row, self.column) {
(LEVEL_ROW, _) => Field::Level,
(NATURE_ROW, _) => Field::Nature,
(row, Column::Iv) => Field::Iv(row - 2),
(row, Column::Ev) => Field::Ev(row - 2),
}
}
pub fn move_row(&mut self, step: i32) {
self.row = (self.row as i32 + step).rem_euclid(ROWS as i32) as usize;
self.fresh = true;
}
pub fn move_column(&mut self, step: i32) {
match self.field() {
Field::Level | Field::Nature => self.step(step),
Field::Iv(_) | Field::Ev(_) => {
self.column = match self.column {
Column::Iv => Column::Ev,
Column::Ev => Column::Iv,
};
self.fresh = true;
}
}
}
pub fn step(&mut self, step: i32) {
match self.field() {
Field::Level => {
self.level = (i32::from(self.level) + step).clamp(1, i32::from(MAX_LEVEL)) as u8;
}
Field::Nature => self.nature = self.nature.cycled(step),
Field::Iv(i) => {
self.ivs[i] = (i32::from(self.ivs[i]) + step).clamp(0, i32::from(MAX_IV)) as u8;
}
Field::Ev(i) => {
let ev = i32::from(self.evs[i]);
let next = if step > 0 {
(ev / 4 + step) * 4
} else {
(ev + 3) / 4 * 4 + step * 4
};
let cap = i32::from(STAT_EV_CAP).max(ev);
self.evs[i] = next.clamp(0, cap) as u16;
}
}
self.fresh = true;
}
pub fn type_digit(&mut self, digit: u8) {
let digit = u32::from(digit);
let extend = |value: u32, max: u32, fresh: bool| match fresh {
true => digit.min(max),
false => (value * 10 + digit).min(max),
};
match self.field() {
Field::Level => {
self.level = extend(self.level.into(), MAX_LEVEL.into(), self.fresh) as u8;
}
Field::Nature => return,
Field::Iv(i) => {
self.ivs[i] = extend(self.ivs[i].into(), MAX_IV.into(), self.fresh) as u8
}
Field::Ev(i) => {
self.evs[i] = extend(self.evs[i].into(), MAX_EV.into(), self.fresh) as u16
}
}
self.fresh = false;
}
pub fn backspace(&mut self) {
match self.field() {
Field::Level => self.level /= 10,
Field::Nature => return,
Field::Iv(i) => self.ivs[i] /= 10,
Field::Ev(i) => self.evs[i] /= 10,
}
self.fresh = false;
}
pub fn reset(&mut self) {
*self = Calculator {
row: self.row,
column: self.column,
..Calculator::default()
};
}
pub fn finals(&self, bases: [u16; 6]) -> [u16; 6] {
std::array::from_fn(|i| {
final_stat(
STAT_ORDER[i],
bases[i],
self.ivs[i],
self.evs[i],
self.level,
self.nature,
)
})
}
pub fn ev_total(&self) -> u16 {
self.evs.iter().sum()
}
pub fn over_total(&self) -> bool {
self.ev_total() > TOTAL_EV_CAP
}
pub fn over_stat(&self, i: usize) -> bool {
self.evs[i] > STAT_EV_CAP
}
}
#[cfg(test)]
mod tests {
use super::*;
const GARCHOMP: [u16; 6] = [108, 130, 95, 80, 85, 102];
fn spread(level: u8, nature: &str, evs: [u16; 6]) -> Calculator {
Calculator {
level,
nature: Nature::named(nature).expect("a nature"),
evs,
..Calculator::default()
}
}
#[test]
fn a_level_100_jolly_garchomp_has_the_stats_the_games_show() {
let calc = spread(100, "Jolly", [0, 252, 4, 0, 0, 252]);
assert_eq!(calc.finals(GARCHOMP), [357, 359, 227, 176, 206, 333]);
}
#[test]
fn level_50_is_what_competitive_play_reads() {
let calc = spread(50, "Jolly", [0, 252, 4, 0, 0, 252]);
assert_eq!(calc.finals(GARCHOMP), [183, 182, 116, 90, 105, 169]);
}
#[test]
fn shedinja_has_one_hp_whatever_it_is_trained_in() {
assert_eq!(
final_stat(StatKind::Hp, 1, 31, 252, 100, Nature::default()),
1
);
}
#[test]
fn natures_raise_and_lower_by_their_place_in_the_grid() {
let effect = |name| {
let n = Nature::named(name).unwrap();
(n.raised(), n.lowered())
};
use StatKind::*;
assert_eq!(effect("Adamant"), (Some(Attack), Some(SpecialAttack)));
assert_eq!(effect("Jolly"), (Some(Speed), Some(SpecialAttack)));
assert_eq!(effect("Timid"), (Some(Speed), Some(Attack)));
assert_eq!(effect("Modest"), (Some(SpecialAttack), Some(Attack)));
assert_eq!(effect("Bold"), (Some(Defense), Some(Attack)));
assert_eq!(
effect("Careful"),
(Some(SpecialDefense), Some(SpecialAttack))
);
assert_eq!(effect("Brave"), (Some(Attack), Some(Speed)));
for neutral in ["Hardy", "Docile", "Serious", "Bashful", "Quirky"] {
assert_eq!(effect(neutral), (None, None), "{neutral}");
}
}
#[test]
fn hp_ignores_the_nature() {
let plain = final_stat(StatKind::Hp, 108, 31, 0, 50, Nature::default());
for i in 0..25 {
assert_eq!(final_stat(StatKind::Hp, 108, 31, 0, 50, Nature(i)), plain);
}
}
#[test]
fn a_calculator_opens_on_level_50_perfect_ivs_and_no_evs() {
let calc = Calculator::default();
assert_eq!((calc.level, calc.ivs, calc.evs), (50, [31; 6], [0; 6]));
assert_eq!(calc.field(), Field::Level);
}
#[test]
fn digits_replace_the_value_on_arrival_and_extend_it_after() {
let mut calc = Calculator::default();
calc.type_digit(1);
calc.type_digit(0);
calc.type_digit(0);
assert_eq!(calc.level, 100);
calc.type_digit(5);
assert_eq!(calc.level, 100);
calc.backspace();
assert_eq!(calc.level, 10);
calc.move_row(1);
calc.move_row(-1);
calc.type_digit(7);
assert_eq!(calc.level, 7);
}
#[test]
fn arrows_cross_between_iv_and_ev_and_keep_the_column_down_the_rows() {
let mut calc = Calculator::default();
calc.move_row(2);
assert_eq!(calc.field(), Field::Iv(0));
calc.move_column(1);
assert_eq!(calc.field(), Field::Ev(0));
calc.move_row(5);
assert_eq!(calc.field(), Field::Ev(5));
calc.move_row(1);
assert_eq!(calc.field(), Field::Level);
}
#[test]
fn arrows_on_the_nature_row_pick_the_nature() {
let mut calc = Calculator::default();
calc.move_row(1);
calc.move_column(1);
assert_eq!(calc.nature.name(), "Lonely");
calc.move_column(-2);
assert_eq!(calc.nature.name(), "Quirky", "wraps");
calc.type_digit(3);
assert_eq!(calc.nature.name(), "Quirky");
}
#[test]
fn evs_step_in_fours_and_stop_at_the_stat_cap() {
let mut calc = Calculator::default();
calc.move_row(2);
calc.move_column(1);
calc.step(1);
assert_eq!(calc.evs[0], 4);
calc.evs[0] = 250;
calc.step(1);
assert_eq!(calc.evs[0], 252);
calc.step(1);
assert_eq!(calc.evs[0], 252);
calc.evs[0] = 6;
calc.step(-1);
assert_eq!(calc.evs[0], 4, "an off-grid value steps onto the grid");
calc.step(-1);
calc.step(-1);
assert_eq!(calc.evs[0], 0);
}
#[test]
fn spreads_past_the_caps_are_flagged_rather_than_refused() {
let mut calc = Calculator::default();
calc.move_row(2);
calc.move_column(1);
for digit in [2, 5, 5] {
calc.type_digit(digit);
}
assert_eq!(calc.evs[0], 255);
assert!(calc.over_stat(0));
assert!(!calc.over_total());
calc.evs = [252, 252, 8, 0, 0, 0];
assert!(!calc.over_stat(2));
assert!(calc.over_total());
assert_eq!(calc.ev_total(), 512);
}
#[test]
fn reset_restores_the_defaults_but_not_the_cursor() {
let mut calc = spread(100, "Adamant", [252; 6]);
calc.move_row(3);
calc.reset();
assert_eq!(calc.level, 50);
assert_eq!(calc.evs, [0; 6]);
assert_eq!(calc.row, 3);
}
}