use dotzuki_engine::battle::stack::Event;
use serde::{Deserialize, Deserializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rational {
pub num: u32,
pub den: u32,
}
impl<'de> Deserialize<'de> for Rational {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let v: Vec<u32> = Vec::deserialize(d)?;
if v.len() != 2 {
return Err(serde::de::Error::invalid_length(
v.len(),
&"a 2-element rational [num, den]",
));
}
Ok(Rational {
num: v[0],
den: v[1],
})
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum LoadError {
#[error("RON parse error: {0}")]
Ron(String),
#[error("unknown event name in `on:`: {0:?}")]
UnknownEvent(String),
#[error("unknown type name: {0:?}")]
UnknownType(String),
#[error("invalid chance fraction {0}/{1} (denominator must be > 0)")]
BadChance(u32, u32),
#[error("unknown status name: {0:?}")]
UnknownStatus(String),
#[error("unknown stat name: {0:?}")]
UnknownStat(String),
#[error("unknown resource name: {0:?}")]
UnknownResource(String),
}
#[derive(Debug, Clone, Deserialize)]
pub struct Ruleset {
#[serde(default)]
pub stats: Vec<String>,
#[serde(default)]
pub types: Vec<TypeName>,
#[serde(default)]
pub resources: Vec<String>,
#[serde(default)]
pub type_chart: Vec<TypeChartEntry>,
#[serde(default)]
pub effects: Vec<EffectRecord>,
}
pub type TypeName = String;
#[derive(Debug, Clone, Deserialize)]
pub struct TypeChartEntry {
pub atk: String,
pub def: String,
pub mult: Rational,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename = "Effect")]
pub struct EffectRecord {
pub id: String,
pub kind: EffectKind,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub power: Option<u32>,
#[serde(rename = "type", default)]
pub mtype: Option<String>,
#[serde(default)]
pub accuracy: Option<u32>,
#[serde(default)]
pub cost: Vec<ResourceCost>,
#[serde(default)]
pub hooks: Vec<HookRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename = "Cost")]
pub struct ResourceCost {
pub resource: String,
pub amount: u16,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename = "Hook")]
pub struct HookRecord {
pub on: String,
#[serde(default = "default_order")]
pub order: u32,
#[serde(default)]
pub priority: i32,
#[serde(default)]
pub chance: Option<Rational>,
#[serde(rename = "do", default)]
pub ops: Vec<Op>,
}
fn default_order() -> u32 {
u32::MAX
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum EffectKind {
Move,
Status,
Ability,
Item,
Weather,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum Selector {
Target,
Foe,
Host,
Source,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum FractionOf {
MaxHp,
CurHp,
LastDamage,
}
impl Default for FractionOf {
fn default() -> Self {
FractionOf::MaxHp
}
}
pub type StatRef = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum DamageValue {
Const(u16),
UserLevel,
RngScaledLevel {
num: u32,
den: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum AmountSpec {
Const(u16),
RngMask {
mask: u8,
plus: u8,
},
RngRange {
lo: u16,
hi: u16,
},
}
impl Default for AmountSpec {
fn default() -> Self {
AmountSpec::Const(0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub enum Predicate {
HasType(String),
StatIs(String),
RelayIntLt(i64),
HasVolatile(String),
MoveTypeIsDefenderType,
TargetHasStatus(String),
Not(Box<Predicate>),
TargetHasAnyStatus,
LevelGE,
SelfHpBelow {
num: u32,
den: u32,
},
SourceHasStatus(String),
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub enum Op {
DealMoveDamage,
DamageFraction {
num: u32,
den: u32,
#[serde(default)]
of: FractionOf,
target: Selector,
#[serde(default)]
unless: Option<Predicate>,
},
HealFraction {
num: u32,
den: u32,
#[serde(default)]
of: FractionOf,
target: Selector,
#[serde(default)]
unless: Option<Predicate>,
},
InflictStatus {
status: String,
target: Selector,
#[serde(default)]
amount: AmountSpec,
},
InflictVolatile {
kind: String,
target: Selector,
#[serde(default)]
amount: AmountSpec,
},
Boost {
stat: String,
stages: i8,
target: Selector,
},
ScaleRelay {
num: u32,
den: u32,
#[serde(default)]
when: Vec<Predicate>,
},
SetRelay(i64),
AddRelay(i64),
ClampRelay {
lo: i64,
hi: i64,
},
VetoIf {
cond: Predicate,
#[serde(default)]
silent: bool,
},
ApplyTypeChart,
PayResource {
resource: String,
amount: u16,
target: Selector,
},
SetHp {
target: Selector,
value: u16,
#[serde(default)]
when: Vec<Predicate>,
},
SetDamage {
value: DamageValue,
#[serde(default = "default_source_selector")]
of: Selector,
},
DamageCurrentHpFraction {
num: u32,
den: u32,
target: Selector,
},
RepeatHits {
count: HitCount,
target: Selector,
#[serde(default)]
final_hit: FinalHitRider,
},
RemoveStatus {
target: Selector,
},
}
fn default_source_selector() -> Selector {
Selector::Source
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum HitCount {
Fixed(u8),
TwoToFive,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub enum FinalHitRider {
None,
OnFinal {
chance: Rational,
ops: Vec<Op>,
},
}
impl Default for FinalHitRider {
fn default() -> Self {
FinalHitRider::None
}
}
pub fn parse_event(name: &str) -> Result<Event, LoadError> {
let ev = match name {
"BeforeTurn" => Event::BeforeTurn,
"ResidualOrder" => Event::ResidualOrder,
"AfterTurn" => Event::AfterTurn,
"BeforeMove" => Event::BeforeMove,
"ModifyMove" => Event::ModifyMove,
"ModifyType" => Event::ModifyType,
"ModifyCritRatio" => Event::ModifyCritRatio,
"Accuracy" => Event::Accuracy,
"Invulnerability" => Event::Invulnerability,
"ModifyDamage" => Event::ModifyDamage,
"Effectiveness" => Event::Effectiveness,
"AfterMove" => Event::AfterMove,
"TryHit" => Event::TryHit,
"Damage" => Event::Damage,
"DamagingHit" => Event::DamagingHit,
"Heal" => Event::Heal,
"AfterFaint" => Event::AfterFaint,
"TrySetStatus" => Event::TrySetStatus,
"AfterSetStatus" => Event::AfterSetStatus,
"TryBoost" => Event::TryBoost,
"AfterBoost" => Event::AfterBoost,
"ModifyStat" => Event::ModifyStat,
"WeatherModifyStat" => Event::WeatherModifyStat,
"Start" => Event::Start,
"End" => Event::End,
"Faint" => Event::Faint,
"SwitchIn" => Event::SwitchIn,
"SwitchOut" => Event::SwitchOut,
"SetWeather" => Event::SetWeather,
"FieldResidual" => Event::FieldResidual,
"SideResidual" => Event::SideResidual,
"Residual" => Event::Residual,
other => {
if let Some(rest) = other
.strip_prefix("Custom(")
.and_then(|s| s.strip_suffix(')'))
{
rest.trim()
.parse::<u16>()
.map(Event::Custom)
.map_err(|_| LoadError::UnknownEvent(name.to_string()))?
} else {
return Err(LoadError::UnknownEvent(name.to_string()));
}
}
};
Ok(ev)
}
pub fn parse_kind(kind: EffectKind) -> dotzuki_engine::battle::stack::EffectType {
use dotzuki_engine::battle::stack::EffectType;
match kind {
EffectKind::Move => EffectType::Move,
EffectKind::Status => EffectType::Status,
EffectKind::Ability | EffectKind::Item | EffectKind::Weather => EffectType::Condition,
}
}
impl Ruleset {
pub fn from_ron(text: &str) -> Result<Self, LoadError> {
let opts = ron::Options::default()
.with_default_extension(ron::extensions::Extensions::IMPLICIT_SOME);
opts.from_str(text)
.map_err(|e| LoadError::Ron(e.to_string()))
}
pub fn type_index(&self, name: &str) -> Option<usize> {
self.types.iter().position(|t| t == name)
}
pub fn stat_index(&self, name: &str) -> Option<usize> {
self.stats.iter().position(|s| s == name)
}
pub fn resource_index(&self, name: &str) -> Option<usize> {
self.resources.iter().position(|r| r == name)
}
}