use serde::Serialize;
use super::Type;
use super::{EggGroup, GenderRatio, GrowthRate, ItemId, LevelUpMove, MoveId, SpeciesId, Stats};
use crate::Result;
use crate::games::pokemon::gen1::Bank;
use crate::games::pokemon::gen1::{bits, parse_learnset};
use crate::io::Reader;
const RECORD_LEN: usize = 32;
const EGG_MOVES_END: u8 = 0xFF;
const NO_ITEM: u8 = 0xFF;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Species {
pub id: SpeciesId,
pub name: String,
pub types: [Type; 2],
pub base_stats: Stats,
pub catch_rate: u8,
pub base_experience: u8,
pub held_items: [Option<ItemId>; 2],
pub gender_ratio: GenderRatio,
pub hatch_cycles: u8,
pub growth_rate: GrowthRate,
pub egg_groups: [EggGroup; 2],
pub machines: Vec<u8>,
pub evolutions: Vec<Evolution>,
pub level_up_moves: Vec<LevelUpMove>,
pub egg_moves: Vec<MoveId>,
#[serde(skip)]
pub(super) pic_size: (u8, 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,
TradeHolding {
item: ItemId,
},
Friendship {
time: TimeOfDay,
},
LevelAttackGreater {
level: u8,
},
LevelAttackEqual {
level: u8,
},
LevelAttackLess {
level: u8,
},
}
raw_enum! {
pub enum TimeOfDay: u8 {
Any = 1,
Day = 2,
Night = 3,
}
}
pub(super) fn parse(
records: &[u8],
names: &[String],
(evolutions, evolution_table): (&Bank, u16),
(egg_moves, egg_move_table): (&Bank, u16),
) -> Result<Vec<Species>> {
let evolution_table = evolutions.reader("Gen 2 evolution table", evolution_table)?;
let egg_move_table = egg_moves.reader("Gen 2 egg move table", egg_move_table)?;
records
.as_chunks::<RECORD_LEN>()
.0
.iter()
.enumerate()
.map(|(index, record)| {
let mut species = parse_record(record)?;
if usize::from(species.id.0) != index + 1 {
let records = Reader::new("Gen 2 base stats", records);
return Err(records.error_at(index * RECORD_LEN, "out of Pokédex order"));
}
species.name = names.get(index).cloned().unwrap_or_default();
let address = evolution_table.u16_at(index * 2)?;
let mut list = evolutions.reader("Gen 2 evolutions and learnset", address)?;
species.evolutions = parse_evolutions(&mut list)?;
species.level_up_moves = parse_learnset(&mut list)?;
let address = egg_move_table.u16_at(index * 2)?;
let list = egg_moves.from(address)?;
let end = list
.iter()
.position(|&id| id == EGG_MOVES_END)
.ok_or_else(|| {
Reader::new("Gen 2 egg moves", list).error_at(0, "list without an end")
})?;
species.egg_moves = list[..end].iter().map(|&id| MoveId(id.into())).collect();
Ok(species)
})
.collect()
}
fn parse_record(bytes: &[u8]) -> Result<Species> {
let mut record = Reader::new("Gen 2 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_attack: record.u8()?,
special_defense: record.u8()?,
};
let types = [Type::from_raw(record.u8()?), Type::from_raw(record.u8()?)];
let catch_rate = record.u8()?;
let base_experience = record.u8()?;
let item = |raw: u8| (raw != 0).then_some(ItemId(raw.into()));
let held_items = [item(record.u8()?), item(record.u8()?)];
let gender_ratio = GenderRatio::from_raw(record.u8()?);
record.skip(1)?; let hatch_cycles = record.u8()?;
record.skip(1)?; let size = record.u8()?;
record.skip(4)?; let growth_rate = GrowthRate::from_raw(record.u8()?);
let groups = record.u8()?;
let machines = bits(record.take(8)?, 64);
Ok(Species {
id,
name: String::new(),
types,
base_stats,
catch_rate,
base_experience,
held_items,
gender_ratio,
hatch_cycles,
growth_rate,
egg_groups: [
EggGroup::from_raw(groups >> 4),
EggGroup::from_raw(groups & 0xF),
],
machines,
evolutions: Vec::new(),
level_up_moves: Vec::new(),
egg_moves: Vec::new(),
pic_size: (size >> 4, size & 0xF),
})
}
fn parse_evolutions(list: &mut Reader) -> Result<Vec<Evolution>> {
let mut evolutions = Vec::new();
loop {
let at = list.position();
let method = list.u8()?;
if method == 0 {
return Ok(evolutions);
}
let parameter = list.u8()?;
let item = ItemId(parameter.into());
let method = match (method, parameter) {
(1, level) => EvolutionMethod::Level { level },
(2, _) => EvolutionMethod::UseItem { item },
(3, NO_ITEM) => EvolutionMethod::Trade,
(3, _) => EvolutionMethod::TradeHolding { item },
(4, time) => EvolutionMethod::Friendship {
time: TimeOfDay::from_raw(time),
},
(5, level) => match list.u8()? {
1 => EvolutionMethod::LevelAttackLess { level },
2 => EvolutionMethod::LevelAttackGreater { level },
3 => EvolutionMethod::LevelAttackEqual { level },
_ => return Err(list.error_at(at, "unknown stat comparison")),
},
_ => return Err(list.error_at(at, "unknown evolution method")),
};
evolutions.push(Evolution {
method,
into: SpeciesId(list.u8()?.into()),
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_records_evolutions_and_egg_moves() {
let mut record = [0; RECORD_LEN];
record[..17].copy_from_slice(&[
1, 45, 49, 49, 45, 65, 65, 0x16, 0x03, 45, 64, 0, 0, 0x1F, 100, 20, 5,
]);
record[17] = 0x55;
record[22..25].copy_from_slice(&[3, 0x17, 0b0000_0001]);
let mut bank = vec![0; 0x4000];
bank[..2].copy_from_slice(&0x4100u16.to_le_bytes());
bank[2..4].copy_from_slice(&0x4200u16.to_le_bytes());
bank[0x100..0x10A].copy_from_slice(&[5, 20, 2, 2, 1, 16, 2, 0, 1, 33]);
bank[0x10A] = 0;
bank[0x200..0x203].copy_from_slice(&[0x71, 0x82, EGG_MOVES_END]);
let bank = Bank {
number: 1,
bytes: bank.into(),
};
let species = parse(
&record,
&["BULBASAUR".to_owned()],
(&bank, 0x4000),
(&bank, 0x4002),
);
let bulbasaur = &species.unwrap()[0];
assert_eq!(bulbasaur.types, [Type::Grass, Type::Poison]);
assert_eq!(bulbasaur.base_stats.special_defense, 65);
assert_eq!(bulbasaur.egg_groups, [EggGroup::Monster, EggGroup::Grass]);
assert_eq!(bulbasaur.machines, [0]);
assert_eq!(
bulbasaur.evolutions[0],
Evolution {
method: EvolutionMethod::LevelAttackGreater { level: 20 },
into: SpeciesId(2)
}
);
assert_eq!(
bulbasaur.evolutions[1].method,
EvolutionMethod::Level { level: 16 }
);
assert_eq!(bulbasaur.level_up_moves.len(), 1);
assert_eq!(bulbasaur.egg_moves, [MoveId(0x71), MoveId(0x82)]);
assert_eq!(bulbasaur.pic_size, (5, 5));
assert!(parse_evolutions(&mut Reader::new("test", &[9, 1, 1])).is_err());
}
}