use serde::Serialize;
use super::{MoveId, Type};
use crate::Result;
use crate::formats::narc::Narc;
use crate::io::Reader;
const RECORD_LEN: usize = 36;
const NEVER_MISSES: u8 = 101;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Move {
pub id: MoveId,
pub name: Option<String>,
pub description: Option<String>,
pub r#type: Type,
pub class: MoveClass,
pub kind: MoveKind,
pub power: u8,
pub accuracy: Option<u8>,
pub pp: u8,
pub priority: i8,
pub target: MoveTarget,
pub flags: MoveFlags,
pub hits: Option<Span>,
pub ailment: Ailment,
pub ailment_chance: u8,
pub ailment_turns: Option<Span>,
pub critical_stage: u8,
pub flinch_chance: u8,
pub drain: i8,
pub healing: i8,
pub stat_changes: Vec<StatChange>,
pub effect: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Span {
pub min: u8,
pub max: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct StatChange {
pub stat: BattleStat,
pub stages: i8,
pub chance: u8,
}
pub(super) fn parse(table: &[u8], names: &[String], descriptions: &[String]) -> Result<Vec<Move>> {
let narc = Narc::parse(table)?;
(1..narc.len())
.map(|index| {
let bytes = narc.member(table, index)?;
let mut record = Reader::new("Gen 5 move", &bytes);
if bytes.len() != RECORD_LEN {
return Err(record.error_at(0, "record is not 36 bytes"));
}
let span = |min: u8, max: u8| (min | max != 0).then_some(Span { min, max });
let r#type = Type::from_raw(record.u8()?);
let kind = MoveKind::from_raw(record.u8()?);
let class = MoveClass::from_raw(record.u8()?);
let power = record.u8()?;
let accuracy = Some(record.u8()?).filter(|&accuracy| accuracy != NEVER_MISSES);
let pp = record.u8()?;
let priority = record.i8()?;
let hits = record.u8()?;
let ailment = Ailment::from_raw(record.u16()?);
let ailment_chance = record.u8()?;
record.skip(1)?;
let ailment_turns = span(record.u8()?, record.u8()?);
let critical_stage = record.u8()?;
let flinch_chance = record.u8()?;
let effect = record.u16()?;
let drain = record.i8()?;
let healing = record.i8()?;
let target = MoveTarget::from_raw(record.u8()?);
let stats = record.array::<3>()?;
let stages = record.array::<3>()?;
let chances = record.array::<3>()?;
record.skip(2)?;
Ok(Move {
id: MoveId(index as u16),
name: names.get(index).cloned(),
description: descriptions.get(index).cloned(),
r#type,
class,
kind,
power,
accuracy,
pp,
priority,
target,
flags: MoveFlags(record.u32()?),
hits: span(hits & 0xF, hits >> 4),
ailment,
ailment_chance,
ailment_turns,
critical_stage,
flinch_chance,
drain,
healing,
stat_changes: (0..3)
.filter(|&slot| stats[slot] != 0)
.map(|slot| StatChange {
stat: BattleStat::from_raw(stats[slot]),
stages: stages[slot] as i8,
chance: chances[slot],
})
.collect(),
effect,
})
})
.collect()
}
raw_enum! {
pub enum MoveClass: u8 {
Status = 0,
Physical = 1,
Special = 2,
}
}
raw_enum! {
pub enum MoveKind: u8 {
Damage = 0,
Ailment = 1,
StatChange = 2,
Heal = 3,
DamageAilment = 4,
AilmentRaiseStat = 5,
DamageLowerStat = 6,
DamageRaiseStat = 7,
DamageDrain = 8,
OneHitKo = 9,
FieldEffect = 10,
SideEffect = 11,
ForceSwitch = 12,
Unique = 13,
}
}
raw_enum! {
pub enum MoveTarget: u8 {
Selected = 0,
UserOrAlly = 1,
Ally = 2,
SelectedOpponent = 3,
AllAdjacent = 4,
AllAdjacentOpponents = 5,
UserParty = 6,
User = 7,
All = 8,
RandomOpponent = 9,
Field = 10,
OpponentSide = 11,
UserSide = 12,
Automatic = 13,
}
}
raw_enum! {
pub enum Ailment: u16 {
None = 0,
Paralysis = 1,
Sleep = 2,
Freeze = 3,
Burn = 4,
Poison = 5,
Confusion = 6,
Infatuation = 7,
Trap = 8,
Nightmare = 9,
Torment = 12,
Disable = 13,
Yawn = 14,
HealBlock = 15,
Identified = 17,
LeechSeed = 18,
Embargo = 19,
PerishSong = 20,
Ingrain = 21,
Special = 0xFFFF,
}
}
raw_enum! {
pub enum BattleStat: u8 {
Attack = 1,
Defense = 2,
SpecialAttack = 3,
SpecialDefense = 4,
Speed = 5,
Accuracy = 6,
Evasion = 7,
All = 8,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct MoveFlags(pub u32);
impl MoveFlags {
pub const NAMES: [&str; 14] = [
"makes_contact",
"charges_first",
"recharges_after",
"blocked_by_protect",
"reflected_by_magic_coat",
"stolen_by_snatch",
"copied_by_mirror_move",
"punches",
"makes_sound",
"fails_in_gravity",
"thaws_user",
"reaches_far",
"heals",
"bypasses_substitute",
];
pub fn names(self) -> impl Iterator<Item = String> {
(0..32)
.filter(move |bit| self.0 & 1 << bit != 0)
.map(|bit| match Self::NAMES.get(bit) {
Some(name) => (*name).to_owned(),
None => format!("bit_{bit}"),
})
}
}
impl Serialize for MoveFlags {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_seq(self.names())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::narc;
#[test]
fn parses_records() {
let thunderbolt = [
0x0C, 4, 2, 95, 100, 15, 0, 0, 1, 0, 10, 1, 0, 0, 0, 0, 6, 0, 0, 0, 0, 4, 0, 0, 0xFF,
0, 0, 10, 0, 0, 0x53, 0x53, 0x48, 0, 0, 0,
];
let comet_punch = [
&[0, 0, 1, 18, 85, 15, 0, 0x52][..],
&[0; 24],
&[0x89, 0, 0, 0x80],
]
.concat();
let table = narc(&[&[0; 36], &thunderbolt, &comet_punch]);
let names = ["", "Thunderbolt"].map(str::to_owned);
let moves = parse(&table, &names, &[]).unwrap();
let bolt = &moves[0];
assert_eq!(bolt.name.as_deref(), Some("Thunderbolt"));
assert_eq!(
(bolt.r#type, bolt.class, bolt.power),
(Type::Electric, MoveClass::Special, 95)
);
assert_eq!(
(bolt.kind, bolt.accuracy),
(MoveKind::DamageAilment, Some(100))
);
assert_eq!(
(bolt.ailment, bolt.ailment_chance),
(Ailment::Paralysis, 10)
);
assert_eq!(
bolt.stat_changes,
[StatChange {
stat: BattleStat::SpecialDefense,
stages: -1,
chance: 10
}]
);
assert_eq!(
serde_json::to_value(bolt.flags).unwrap(),
serde_json::json!(["blocked_by_protect", "copied_by_mirror_move"])
);
let punch = &moves[1];
assert_eq!(punch.hits, Some(Span { min: 2, max: 5 }));
assert_eq!(punch.ailment_turns, None);
assert_eq!(
punch.flags.names().collect::<Vec<_>>(),
["makes_contact", "blocked_by_protect", "punches", "bit_31"]
);
assert!(parse(&narc(&[&[0; 36], &[0; 35]]), &[], &[]).is_err());
}
}