use std::cell::RefCell;
use std::collections::HashMap;
use dotzuki_engine::battle::rng::BattleRng as EngineRng;
use dotzuki_engine::battle::stack::{
BattleCtx, Effect, EffectProvider, EffectState, Event, MoveContext,
};
use dotzuki_engine::battle::{
BattleProvider, BattleState, BattlerRef, BattlerState, DamageResult, EffectResult, EnumMap,
MoveEffect,
};
use dotzuki_rules::{
CompiledRuleset, EffectKind, LoadError, RuleBindings, RulesHost, RulesProvider, Ruleset,
};
use super::{basic_attack, normalize_stat_key, stage_multiplier, Combatant, Skill, MAX_STAGE};
pub const DATA_ID_BASE: u32 = 0x10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StatId(pub u16);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusId(pub u16);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeId(pub u16);
#[derive(Debug, Clone)]
pub struct VolatileKind {
pub name: String,
pub amount: u16,
}
#[derive(Debug, Clone, Default)]
pub struct SpeciesData {
pub element: Option<String>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct GenericProvider;
fn eff_stat(b: &BattlerState<GenericProvider>, key: &str) -> u32 {
let Some(host) = GenericProvider::rules_host() else {
return 1;
};
let Some(idx) = host
.compiled
.stats
.iter()
.position(|s| normalize_stat_key(s) == key)
else {
return 1;
};
let id = StatId(idx as u16);
let raw = u32::from(b.stats.get(id).copied().unwrap_or(1));
let stage = b.stat_stages.get(id).copied().unwrap_or(0);
stage_multiplier(raw, stage)
}
impl BattleProvider for GenericProvider {
type Monster = ();
type Move = Skill;
type Ability = ();
type Status = StatusId;
type Stat = StatId;
type Species = SpeciesData;
type Type = TypeId;
type Item = ();
fn calculate_damage(
&self,
move_: &Skill,
attacker: &BattlerState<Self>,
defender: &BattlerState<Self>,
random: u8,
is_critical: bool,
) -> DamageResult {
let base = move_.power as u64 * eff_stat(attacker, "attack") as u64
/ eff_stat(defender, "defense").max(1) as u64;
let varied = base * (85 + u64::from(random % 16)) / 100;
let after_crit = if is_critical { varied * 3 / 2 } else { varied };
DamageResult {
damage: after_crit.max(1).min(u64::from(u16::MAX)) as u16,
effectiveness: 1.0,
is_miss: false,
}
}
fn select_move(
&self,
battler: &BattlerState<Self>,
_state: &BattleState<Self>,
) -> Self::Move {
battler.moves.first().cloned().unwrap_or_else(basic_attack)
}
fn apply_move_effect(
&self,
_effect: MoveEffect,
_user: &mut BattlerState<Self>,
_target: &mut BattlerState<Self>,
) -> EffectResult {
EffectResult::NoEffect
}
fn create_monster(&self, species: Self::Species, level: u8) -> BattlerState<Self> {
BattlerState::new(species, 1, 1, EnumMap::default(), Vec::new()).with_level(level)
}
}
impl EffectProvider for GenericProvider {
type EffectStateKind = VolatileKind;
fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
None
}
fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
None
}
fn turn_order_rank(
&self,
state: &BattleState<Self>,
who: BattlerRef,
_action: &Self::Move,
) -> (i32, i32) {
let b = if who.side == 0 {
&state.player_battlers[who.slot as usize]
} else {
&state.opponent_battlers[who.slot as usize]
};
(0, -(eff_stat(b, "speed") as i32))
}
}
thread_local! {
static HOST: RefCell<Option<&'static RulesHost<GenericProvider>>> =
const { RefCell::new(None) };
}
pub fn install_compiled(compiled: CompiledRuleset) {
let host = RulesHost::new(compiled, GenericBindings);
let leaked: &'static RulesHost<GenericProvider> = Box::leak(Box::new(host));
HOST.with(|h| *h.borrow_mut() = Some(leaked));
}
impl RulesProvider for GenericProvider {
type Bindings = GenericBindings;
fn compiled(&self) -> &CompiledRuleset {
&Self::rules_host().expect("rules host installed").compiled
}
fn bindings(&self) -> &Self::Bindings {
&Self::rules_host().expect("rules host installed").bindings
}
fn rules_host() -> Option<&'static RulesHost<GenericProvider>> {
HOST.with(|h| *h.borrow())
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct GenericBindings;
impl GenericBindings {
fn stat_key(stat_index: usize) -> Option<String> {
let host = GenericProvider::rules_host()?;
let name = host.compiled.stats.get(stat_index)?;
Some(normalize_stat_key(name))
}
fn type_name(type_index: usize) -> Option<String> {
let host = GenericProvider::rules_host()?;
host.compiled.types.get(type_index).cloned()
}
}
impl RuleBindings<GenericProvider> for GenericBindings {
fn apply_boost(&self, b: &mut BattlerState<GenericProvider>, stat_index: usize, stages: i8) -> bool {
if Self::stat_key(stat_index).is_none() {
return false;
}
let id = StatId(stat_index as u16);
let cur = b.stat_stages.get(id).copied().unwrap_or(0);
b.stat_stages.set(id, (cur + stages).clamp(-MAX_STAGE, MAX_STAGE));
true
}
fn set_status(&self, b: &mut BattlerState<GenericProvider>, status_index: usize) -> bool {
b.status = Some(StatusId(status_index as u16));
true
}
fn has_type(&self, b: &BattlerState<GenericProvider>, type_index: usize) -> bool {
match (Self::type_name(type_index), &b.species.element) {
(Some(name), Some(element)) => name.eq_ignore_ascii_case(element),
_ => false,
}
}
fn type_chart_mult(
&self,
ctx: &BattleCtx<'_, GenericProvider>,
move_type_index: usize,
defender: BattlerRef,
) -> (u32, u32) {
let Some(host) = GenericProvider::rules_host() else {
return (1, 1);
};
let Some(element) = &ctx.battler(defender).species.element else {
return (1, 1);
};
let Some(def_index) = host
.compiled
.types
.iter()
.position(|t| t.eq_ignore_ascii_case(element))
else {
return (1, 1);
};
host.compiled.chart_mult(move_type_index, def_index)
}
fn make_volatile(&self, name: &str, amount: u16) -> Option<VolatileKind> {
Some(VolatileKind {
name: name.to_string(),
amount,
})
}
fn has_volatile(&self, ctx: &BattleCtx<'_, GenericProvider>, who: BattlerRef, name: &str) -> bool {
ctx.effects
.iter()
.any(|e| e.host == who && e.kind.name == name)
}
fn battler_level(&self, b: &BattlerState<GenericProvider>) -> u16 {
u16::from(b.level)
}
fn has_status(&self, b: &BattlerState<GenericProvider>, status_index: usize) -> bool {
b.status == Some(StatusId(status_index as u16))
}
fn has_any_status(&self, b: &BattlerState<GenericProvider>) -> bool {
b.status.is_some()
}
}
pub fn status_index_of(ruleset: &Ruleset, name: &str) -> Option<usize> {
ruleset
.effects
.iter()
.filter(|r| r.kind == EffectKind::Status)
.position(|r| r.id == name)
}
pub fn status_names(ruleset: &Ruleset) -> Vec<String> {
ruleset
.effects
.iter()
.filter(|r| r.kind == EffectKind::Status)
.map(|r| r.id.clone())
.collect()
}
pub fn compile_ruleset(ruleset: &Ruleset) -> Result<CompiledRuleset, LoadError> {
CompiledRuleset::compile::<GenericProvider, GenericBindings>(
ruleset,
DATA_ID_BASE,
&GenericBindings,
|name| status_index_of(ruleset, name),
)
}
pub fn ron_moves(ruleset: &Ruleset, resource: Option<&str>) -> HashMap<String, RonMove> {
ruleset
.effects
.iter()
.filter(|r| r.kind == EffectKind::Move)
.map(|rec| {
let cost = if rec.cost.is_empty() {
None
} else {
resource.map(|res| {
rec.cost
.iter()
.filter(|c| c.resource == res)
.map(|c| u32::from(c.amount))
.sum()
})
};
(
rec.id.clone(),
RonMove {
power: rec.power,
accuracy: rec.accuracy,
mtype: rec.mtype.clone(),
cost,
},
)
})
.collect()
}
pub fn validate_ruleset(rules_text: &str) -> Vec<String> {
match Ruleset::from_ron(rules_text) {
Err(e) => vec![e.to_string()],
Ok(ruleset) => match compile_ruleset(&ruleset) {
Ok(_) => Vec::new(),
Err(e) => vec![e.to_string()],
},
}
}
#[derive(Debug, Clone, Default)]
pub struct RonMove {
pub power: Option<u32>,
pub accuracy: Option<u32>,
pub mtype: Option<String>,
pub cost: Option<u32>,
}
pub struct HookState {
pub state: BattleState<GenericProvider>,
pub effects: Vec<EffectState<GenericProvider>>,
pub mv: MoveContext,
pub registry: Vec<&'static Effect<GenericProvider>>,
pub move_records: HashMap<String, RonMove>,
pub status_names: Vec<String>,
pub stat_names: Vec<String>,
pub has_resource: bool,
}
impl HookState {
pub fn battler_ref(side: super::Side) -> BattlerRef {
match side {
super::Side::Player => BattlerRef::PLAYER,
super::Side::Enemy => BattlerRef::OPPONENT,
}
}
pub fn battler(&self, side: super::Side) -> &BattlerState<GenericProvider> {
let r = Self::battler_ref(side);
if r.side == 0 {
&self.state.player_battlers[r.slot as usize]
} else {
&self.state.opponent_battlers[r.slot as usize]
}
}
pub fn subscribes(&self, skill_id: &str, event: Event) -> bool {
let Some(host) = GenericProvider::rules_host() else {
return false;
};
host.compiled
.hooks
.values()
.any(|h| h.source_id == skill_id && h.event == event)
}
}
pub struct RngAdapter<'a>(pub &'a mut dyn super::BattleRng);
impl EngineRng for RngAdapter<'_> {
fn next_u8(&mut self) -> u8 {
self.0.byte()
}
}
fn raw_stat(c: &Combatant, name: &str) -> u32 {
match normalize_stat_key(name).as_str() {
"hp" => c.max_hp,
"defense" => c.defense,
"speed" => c.speed,
_ => c.attack,
}
}
fn status_id_of(status: &Option<String>, status_names: &[String]) -> Option<StatusId> {
status
.as_ref()
.and_then(|name| status_names.iter().position(|n| n == name))
.map(|idx| StatusId(idx as u16))
}
pub fn mirror_of(
c: &Combatant,
stat_names: &[String],
status_names: &[String],
has_resource: bool,
) -> BattlerState<GenericProvider> {
let mut b = BattlerState::new(
SpeciesData {
element: c.element.clone(),
},
c.hp.min(u32::from(u16::MAX)) as u16,
c.max_hp.min(u32::from(u16::MAX)) as u16,
EnumMap::default(),
c.skills.clone(),
)
.with_level(c.level);
b.status = status_id_of(&c.status, status_names);
sync_to_mirror(c, &mut b, stat_names, status_names, has_resource);
b
}
pub fn sync_to_mirror(
c: &Combatant,
b: &mut BattlerState<GenericProvider>,
stat_names: &[String],
status_names: &[String],
has_resource: bool,
) {
b.hp = c.hp.min(u32::from(u16::MAX)) as u16;
b.max_hp = c.max_hp.min(u32::from(u16::MAX)) as u16;
b.level = c.level;
b.status = status_id_of(&c.status, status_names);
for (i, name) in stat_names.iter().enumerate() {
let id = StatId(i as u16);
b.stats.set(id, raw_stat(c, name).min(u32::from(u16::MAX)) as u16);
b.stat_stages.set(id, c.stages.get(name));
}
if has_resource {
b.resources.set(
0,
c.mp.min(u32::from(u16::MAX)) as u16,
c.max_mp.min(u32::from(u16::MAX)) as u16,
);
}
}
pub fn sync_from_mirror(
b: &BattlerState<GenericProvider>,
c: &mut Combatant,
stat_names: &[String],
status_names: &[String],
has_resource: bool,
) {
c.hp = u32::from(b.hp);
c.max_hp = u32::from(b.max_hp);
c.status = b
.status
.as_ref()
.and_then(|id| status_names.get(id.0 as usize).cloned());
for (i, name) in stat_names.iter().enumerate() {
let id = StatId(i as u16);
if let Some(stage) = b.stat_stages.get(id) {
c.stages.set(name, *stage);
}
}
if has_resource {
c.mp = u32::from(b.resources.current(0).unwrap_or(0));
c.max_mp = u32::from(b.resources.max(0).unwrap_or(0));
}
}