use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PokemonEntry {
pub name: String,
pub id: u32,
}
impl PokemonEntry {
pub fn dex_number(&self) -> Option<u32> {
(self.id <= MAX_DEX_NUMBER).then_some(self.id)
}
pub fn generation(&self) -> Option<u8> {
let dex = self.dex_number()?;
GENERATION_RANGES
.iter()
.position(|&last| dex <= last)
.map(|idx| idx as u8 + 1)
}
}
const MAX_DEX_NUMBER: u32 = 1025;
const GENERATION_RANGES: [u32; 9] = [151, 251, 386, 493, 649, 721, 809, 905, MAX_DEX_NUMBER];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StatKind {
Hp,
Attack,
Defense,
SpecialAttack,
SpecialDefense,
Speed,
}
impl StatKind {
pub fn from_api(slug: &str) -> Option<Self> {
match slug {
"hp" => Some(Self::Hp),
"attack" => Some(Self::Attack),
"defense" => Some(Self::Defense),
"special-attack" => Some(Self::SpecialAttack),
"special-defense" => Some(Self::SpecialDefense),
"speed" => Some(Self::Speed),
_ => None,
}
}
pub fn order(&self) -> u8 {
match self {
Self::Hp => 0,
Self::Attack => 1,
Self::Defense => 2,
Self::SpecialAttack => 3,
Self::SpecialDefense => 4,
Self::Speed => 5,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Stat {
pub kind: StatKind,
pub base: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ability {
pub name: String,
pub is_hidden: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AbilityInfo {
pub name: String,
pub names: HashMap<String, String>,
pub flavors: HashMap<String, String>,
}
impl AbilityInfo {
pub fn name_for(&self, code: &str) -> String {
self.names
.get(code)
.or_else(|| self.names.get("en"))
.cloned()
.unwrap_or_else(|| title_case(&self.name))
}
pub fn flavor_for(&self, code: &str) -> Option<&str> {
self.flavors
.get(code)
.or_else(|| self.flavors.get("en"))
.map(String::as_str)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum SpriteVariant {
#[default]
Normal,
Shiny,
}
impl SpriteVariant {
pub fn toggled(self) -> Self {
match self {
Self::Normal => Self::Shiny,
Self::Shiny => Self::Normal,
}
}
pub fn is_shiny(self) -> bool {
matches!(self, Self::Shiny)
}
pub fn file_suffix(self) -> &'static str {
match self {
Self::Normal => "",
Self::Shiny => ".shiny",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PokemonDetail {
pub name: String,
pub species: String,
pub dex_number: u32,
pub is_legendary: bool,
pub is_mythical: bool,
pub is_baby: bool,
pub types: Vec<String>,
pub abilities: Vec<Ability>,
pub stats: Vec<Stat>,
pub height: u32,
pub weight: u32,
pub sprite_url: Option<String>,
pub shiny_sprite_url: Option<String>,
pub genera: HashMap<String, String>,
pub flavors: HashMap<String, String>,
}
impl PokemonDetail {
pub fn stat_total(&self) -> u32 {
self.stats.iter().map(|s| s.base as u32).sum()
}
pub fn sprite_url_for(&self, variant: SpriteVariant) -> Option<&str> {
match variant {
SpriteVariant::Normal => self.sprite_url.as_deref(),
SpriteVariant::Shiny => self
.shiny_sprite_url
.as_deref()
.or(self.sprite_url.as_deref()),
}
}
pub fn genus_for(&self, code: &str) -> Option<&str> {
self.genera
.get(code)
.or_else(|| self.genera.get("en"))
.map(String::as_str)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EvolutionTrigger {
LevelUp,
Trade,
UseItem,
Shed,
Other(String),
}
impl EvolutionTrigger {
pub fn from_api(slug: &str) -> Self {
match slug {
"level-up" => Self::LevelUp,
"trade" => Self::Trade,
"use-item" => Self::UseItem,
"shed" => Self::Shed,
other => Self::Other(other.to_string()),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvolutionCondition {
pub trigger: Option<EvolutionTrigger>,
pub min_level: Option<u32>,
pub item: Option<String>,
pub held_item: Option<String>,
pub known_move: Option<String>,
pub known_move_type: Option<String>,
pub min_happiness: Option<u32>,
pub min_affection: Option<u32>,
pub min_beauty: Option<u32>,
pub time_of_day: Option<String>,
pub location: Option<String>,
pub gender: Option<u8>,
pub needs_overworld_rain: bool,
pub turn_upside_down: bool,
pub trade_species: Option<String>,
pub party_species: Option<String>,
pub party_type: Option<String>,
pub relative_physical_stats: Option<i8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvolutionTree {
pub name: String,
pub condition: Option<EvolutionCondition>,
pub children: Vec<EvolutionTree>,
}
impl EvolutionTree {
pub fn collect_names(&self, out: &mut Vec<String>) {
out.push(self.name.clone());
for child in &self.children {
child.collect_names(out);
}
}
pub fn leaf_count(&self) -> usize {
if self.children.is_empty() {
1
} else {
self.children.iter().map(EvolutionTree::leaf_count).sum()
}
}
pub fn find(&self, name: &str) -> Option<&EvolutionTree> {
if self.name == name {
return Some(self);
}
self.children.iter().find_map(|child| child.find(name))
}
pub fn depth(&self) -> usize {
1 + self
.children
.iter()
.map(EvolutionTree::depth)
.max()
.unwrap_or(0)
}
}
#[derive(Debug, Clone)]
pub struct Sprite {
pub width: u32,
pub height: u32,
pub pixels: Vec<[u8; 4]>,
}
impl Sprite {
pub fn box_average(&self, x0: u32, y0: u32, x1: u32, y1: u32) -> [u8; 4] {
let x1 = x1.min(self.width.saturating_sub(1)).max(x0);
let y1 = y1.min(self.height.saturating_sub(1)).max(y0);
let (mut r, mut g, mut b, mut a, mut n) = (0u32, 0u32, 0u32, 0u32, 0u32);
for y in y0..=y1 {
for x in x0..=x1 {
let p = self.pixels[(y * self.width + x) as usize];
let pa = p[3] as u32;
r += p[0] as u32 * pa;
g += p[1] as u32 * pa;
b += p[2] as u32 * pa;
a += pa;
n += 1;
}
}
if a == 0 || n == 0 {
return [0, 0, 0, 0];
}
[(r / a) as u8, (g / a) as u8, (b / a) as u8, (a / n) as u8]
}
pub fn content_bounds(&self) -> (u32, u32, u32, u32) {
let (mut x0, mut y0, mut x1, mut y1) = (self.width, self.height, 0u32, 0u32);
let mut found = false;
for y in 0..self.height {
for x in 0..self.width {
if self.pixels[(y * self.width + x) as usize][3] >= 128 {
found = true;
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
}
}
if found {
(x0, y0, x1, y1)
} else {
(
0,
0,
self.width.saturating_sub(1),
self.height.saturating_sub(1),
)
}
}
}
pub fn title_case(raw: &str) -> String {
raw.split(['-', ' '])
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(id: u32) -> PokemonEntry {
PokemonEntry {
name: String::new(),
id,
}
}
#[test]
fn generation_boundaries_land_on_the_right_side() {
for (id, gen) in [
(1, 1),
(151, 1),
(152, 2),
(251, 2),
(252, 3),
(386, 3),
(387, 4),
(493, 4),
(494, 5),
(649, 5),
(650, 6),
(721, 6),
(722, 7),
(809, 7),
(810, 8),
(905, 8),
(906, 9),
(1025, 9),
] {
assert_eq!(entry(id).generation(), Some(gen), "dex #{id}");
}
}
fn detail_with_sprites(normal: Option<&str>, shiny: Option<&str>) -> PokemonDetail {
PokemonDetail {
name: "pikachu".into(),
species: "pikachu".into(),
dex_number: 25,
is_legendary: false,
is_mythical: false,
is_baby: false,
types: Vec::new(),
abilities: Vec::new(),
stats: Vec::new(),
height: 0,
weight: 0,
sprite_url: normal.map(str::to_string),
shiny_sprite_url: shiny.map(str::to_string),
genera: HashMap::new(),
flavors: HashMap::new(),
}
}
#[test]
fn shiny_artwork_falls_back_to_the_normal_palette() {
let both = detail_with_sprites(Some("front.png"), Some("shiny.png"));
assert_eq!(both.sprite_url_for(SpriteVariant::Shiny), Some("shiny.png"));
assert_eq!(
both.sprite_url_for(SpriteVariant::Normal),
Some("front.png")
);
let normal_only = detail_with_sprites(Some("front.png"), None);
assert_eq!(
normal_only.sprite_url_for(SpriteVariant::Shiny),
Some("front.png")
);
let neither = detail_with_sprites(None, None);
assert_eq!(neither.sprite_url_for(SpriteVariant::Shiny), None);
}
#[test]
fn alternate_forms_have_no_dex_number_or_generation() {
let alolan_raichu = entry(10100);
assert_eq!(alolan_raichu.dex_number(), None);
assert_eq!(alolan_raichu.generation(), None);
}
}