use serde::Serialize;
use super::{Bank, GrowthRate, ItemId, LevelUpMove, MoveId, SpeciesId, Type};
use crate::Result;
use crate::io::Reader;
const RECORD_LEN: usize = 28;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Species {
pub id: SpeciesId,
pub index: u8,
pub name: String,
pub types: [Type; 2],
pub base_stats: Stats,
pub catch_rate: u8,
pub base_experience: u8,
pub growth_rate: GrowthRate,
pub starting_moves: Vec<MoveId>,
pub machines: Vec<u8>,
pub evolutions: Vec<Evolution>,
pub level_up_moves: Vec<LevelUpMove>,
#[serde(skip)]
pub(super) pics: Pics,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(super) struct Pics {
pub front: u16,
pub back: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Stats {
pub hp: u8,
pub attack: u8,
pub defense: u8,
pub speed: u8,
pub special: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Evolution {
#[serde(flatten)]
pub method: EvolutionMethod,
pub into: SpeciesId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(tag = "method", rename_all = "snake_case")]
pub enum EvolutionMethod {
Level { level: u8 },
UseItem { item: ItemId },
Trade,
}
pub(super) fn parse(
records: &[u8],
pokedex: &[u8],
names: &[String],
evolutions: &Bank,
table: u16,
) -> Result<Vec<Species>> {
let table = evolutions.reader("Gen 1 evolution table", table)?;
records
.as_chunks::<RECORD_LEN>()
.0
.iter()
.enumerate()
.map(|(position, record)| {
let dex = position as u8 + 1;
let mut species = parse_record(record)?;
if species.id.0 != u16::from(dex) {
let reader = Reader::new("Gen 1 base stats", records);
return Err(reader.error_at(position * RECORD_LEN, "out of Pokédex order"));
}
let index = pokedex
.iter()
.position(|&number| number == dex)
.ok_or_else(|| {
Reader::new("Gen 1 Pokédex order", pokedex)
.error_at(0, "species without an index")
})?;
species.index = index as u8 + 1;
species.name = names.get(index).cloned().unwrap_or_default();
let address = table.u16_at(index * 2)?;
let mut list = evolutions.reader("Gen 1 evolutions and learnset", address)?;
species.evolutions = parse_evolutions(&mut list, pokedex)?;
species.level_up_moves = parse_learnset(&mut list)?;
Ok(species)
})
.collect()
}
fn parse_record(bytes: &[u8]) -> Result<Species> {
let mut record = Reader::new("Gen 1 base stats", bytes);
let id = SpeciesId(record.u8()?.into());
let base_stats = Stats {
hp: record.u8()?,
attack: record.u8()?,
defense: record.u8()?,
speed: record.u8()?,
special: record.u8()?,
};
let types = [Type::from_raw(record.u8()?), Type::from_raw(record.u8()?)];
let catch_rate = record.u8()?;
let base_experience = record.u8()?;
record.skip(1)?; let pics = Pics {
front: record.u16()?,
back: record.u16()?,
};
let starting_moves = record
.take(4)?
.iter()
.filter(|&&id| id != 0)
.map(|&id| MoveId(id.into()))
.collect();
let growth_rate = GrowthRate::from_raw(record.u8()?);
let machines = bits(record.take(7)?, super::MACHINES);
Ok(Species {
id,
index: 0,
name: String::new(),
types,
base_stats,
catch_rate,
base_experience,
growth_rate,
starting_moves,
machines,
evolutions: Vec::new(),
level_up_moves: Vec::new(),
pics,
})
}
pub(crate) fn bits(bytes: &[u8], count: usize) -> Vec<u8> {
(0..count)
.filter(|&bit| {
bytes
.get(bit / 8)
.is_some_and(|byte| byte >> (bit % 8) & 1 != 0)
})
.map(|bit| bit as u8)
.collect()
}
fn parse_evolutions(list: &mut Reader, pokedex: &[u8]) -> Result<Vec<Evolution>> {
let mut evolutions = Vec::new();
loop {
let at = list.position();
let method = match list.u8()? {
0 => return Ok(evolutions),
1 => EvolutionMethod::Level { level: list.u8()? },
2 => {
let item = ItemId(list.u8()?.into());
list.skip(1)?;
EvolutionMethod::UseItem { item }
}
3 => {
list.skip(1)?;
EvolutionMethod::Trade
}
_ => return Err(list.error_at(at, "unknown evolution method")),
};
let index = list.u8()?;
let into = usize::from(index)
.checked_sub(1)
.and_then(|index| pokedex.get(index))
.filter(|&&dex| dex != 0)
.ok_or_else(|| list.error_at(at, "evolves into no species"))?;
evolutions.push(Evolution {
method,
into: SpeciesId((*into).into()),
});
}
}
pub(crate) fn parse_learnset(list: &mut Reader) -> Result<Vec<LevelUpMove>> {
let mut moves = Vec::new();
loop {
match list.u8()? {
0 => return Ok(moves),
level => moves.push(LevelUpMove {
level,
r#move: MoveId(list.u8()?.into()),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_records_evolutions_and_learnsets() {
let record = [
25, 35, 55, 30, 90, 50, 0x17, 0x17, 190, 82, 0x55, 0x7D, 0x4D, 0x8B, 0x4E, 84, 45, 0,
0, 0, 0xC2, 0x43, 0, 0, 0, 0, 0, 0,
];
let mut records: Vec<u8> = (1..=24).flat_map(|dex| [dex, 0, 0, 0].repeat(7)).collect();
records.extend(record);
let mut pokedex = vec![0; 190];
pokedex[..24].copy_from_slice(&(1..=24).collect::<Vec<u8>>());
pokedex[0x53] = 25;
pokedex[0x54] = 26;
let mut bank = 0x4200u16.to_le_bytes().repeat(190);
bank.resize(0x4000, 0);
bank[0x200..0x20A].copy_from_slice(&[2, 0x21, 1, 0x55, 0, 9, 86, 16, 98, 0]);
let bank = Bank {
number: 1,
bytes: bank.into(),
};
let species = parse(&records, &pokedex, &[], &bank, 0x4000).unwrap();
let pikachu = &species[24];
assert_eq!((pikachu.id, pikachu.index), (SpeciesId(25), 0x54));
assert_eq!(pikachu.types, [Type::Electric; 2]);
assert_eq!(pikachu.base_stats.special, 50);
assert_eq!(pikachu.starting_moves, [MoveId(84), MoveId(45)]);
assert_eq!(pikachu.machines, [1, 6, 7, 8, 9, 14]);
assert_eq!(
pikachu.evolutions,
[Evolution {
method: EvolutionMethod::UseItem { item: ItemId(0x21) },
into: SpeciesId(26)
}]
);
assert_eq!(pikachu.level_up_moves[1].level, 16);
records[24 * RECORD_LEN] = 7;
assert!(parse(&records, &pokedex, &[], &bank, 0x4000).is_err());
let mut list = Reader::new("test", &[9, 1, 2, 0]);
assert!(parse_evolutions(&mut list, &pokedex).is_err());
assert!(parse_learnset(&mut Reader::new("test", &[5, 1])).is_err());
}
}