use dotzuki_engine::battle::stack::{BattleCtx, EffectId, HandlerResult, RelayVar};
use dotzuki_engine::battle::BattlerRef;
use crate::bindings::RuleBindings;
use crate::model::{AmountSpec, DamageValue, FractionOf, Op, Predicate, Selector};
use crate::registry::{CompiledHook, RulesProvider};
use crate::trace;
pub fn interpret<P: RulesProvider>(
ctx: &mut BattleCtx<'_, P>,
relay: RelayVar,
target: BattlerRef,
source: BattlerRef,
source_effect: EffectId,
) -> HandlerResult {
let Some(host) = P::rules_host() else {
return HandlerResult::Unchanged;
};
let Some(hook) = host.hook(source_effect) else {
return HandlerResult::Unchanged;
};
if let Some((num, den)) = hook.chance {
let pass = ctx.rng.chance(num, den);
if !pass {
return HandlerResult::Unchanged;
}
}
run_ops(ctx, relay, target, source, &host.bindings, hook)
}
pub fn run_ops<P: RulesProvider>(
ctx: &mut BattleCtx<'_, P>,
mut relay: RelayVar,
target: BattlerRef,
source: BattlerRef,
bindings: &P::Bindings,
hook: &CompiledHook,
) -> HandlerResult {
let mut result = HandlerResult::Unchanged;
for op in &hook.ops {
let before = relay;
match apply_op(ctx, relay, target, source, bindings, hook, op) {
OpOutcome::Unchanged => {
trace::record(hook.id, hook.event, op, before, before);
}
OpOutcome::Set(v) => {
relay = v;
result = HandlerResult::Set(v);
trace::record(hook.id, hook.event, op, before, v);
}
OpOutcome::Fail => {
trace::record(hook.id, hook.event, op, before, RelayVar::Bool(false));
return HandlerResult::Fail;
}
OpOutcome::FailSilent => {
trace::record(hook.id, hook.event, op, before, RelayVar::Unit);
return HandlerResult::FailSilent;
}
}
}
result
}
enum OpOutcome {
Unchanged,
Set(RelayVar),
Fail,
FailSilent,
}
fn apply_op<P: RulesProvider>(
ctx: &mut BattleCtx<'_, P>,
relay: RelayVar,
target: BattlerRef,
source: BattlerRef,
bindings: &P::Bindings,
hook: &CompiledHook,
op: &Op,
) -> OpOutcome {
match op {
Op::DealMoveDamage => OpOutcome::Unchanged,
Op::DamageFraction {
num,
den,
of,
target: sel,
unless,
} => {
if pred_holds(ctx, bindings, relay, target, source, hook, unless.as_ref()) {
return OpOutcome::Unchanged; }
let who = resolve(*sel, target, source);
let amt = fraction_amount(ctx, who, *of, *num, *den);
if !bindings.redirect_hp_loss(ctx, who, source, amt) {
ctx.battler_mut(who).take_damage(amt);
}
OpOutcome::Unchanged
}
Op::HealFraction {
num,
den,
of,
target: sel,
unless,
} => {
if pred_holds(ctx, bindings, relay, target, source, hook, unless.as_ref()) {
return OpOutcome::Unchanged;
}
let who = resolve(*sel, target, source);
let amt = fraction_amount(ctx, who, *of, *num, *den);
ctx.battler_mut(who).heal(amt);
OpOutcome::Unchanged
}
Op::InflictStatus {
status,
target: sel,
amount,
} => {
let who = resolve(*sel, target, source);
let amt = resolve_amount(ctx, *amount);
if let Some(idx) = status_index::<P>(status) {
let b = ctx.battler_mut(who);
bindings.set_status_with_amount(b, idx, amt);
}
OpOutcome::Unchanged
}
Op::InflictVolatile {
kind,
target: sel,
amount,
} => {
let who = resolve(*sel, target, source);
let amt = resolve_amount(ctx, *amount);
if let Some(kind) = bindings.make_volatile(kind, amt) {
ctx.install_effect(who, kind);
}
OpOutcome::Unchanged
}
Op::Boost {
stat,
stages,
target: sel,
} => {
let who = resolve(*sel, target, source);
if let Some(idx) = host_stat_index::<P>(stat) {
let b = ctx.battler_mut(who);
bindings.apply_boost(b, idx, *stages);
}
OpOutcome::Unchanged
}
Op::ScaleRelay { num, den, when } => {
if when
.iter()
.all(|p| pred_holds(ctx, bindings, relay, target, source, hook, Some(p)))
{
OpOutcome::Set(relay.scale(*num, *den))
} else {
OpOutcome::Unchanged
}
}
Op::SetRelay(v) => OpOutcome::Set(RelayVar::Int(*v)),
Op::AddRelay(k) => OpOutcome::Set(RelayVar::Int(relay.as_int() + *k)),
Op::ClampRelay { lo, hi } => {
let v = relay.as_int().clamp(*lo, *hi);
OpOutcome::Set(RelayVar::Int(v))
}
Op::VetoIf { cond, silent } => {
if pred_holds(ctx, bindings, relay, target, source, hook, Some(cond)) {
if *silent {
OpOutcome::FailSilent
} else {
OpOutcome::Fail
}
} else {
OpOutcome::Unchanged
}
}
Op::ApplyTypeChart => {
let Some(mti) = hook.move_type_index else {
return OpOutcome::Unchanged; };
let (num, den) = bindings.type_chart_mult(ctx, mti, target);
OpOutcome::Set(relay.scale(num, den))
}
Op::PayResource {
resource,
amount,
target: sel,
} => {
let who = resolve(*sel, target, source);
let Some(idx) = host_resource_index::<P>(resource) else {
return OpOutcome::Unchanged;
};
if !bindings.can_pay_resource(ctx.battler(who), idx, *amount) {
return OpOutcome::Fail; }
bindings.pay_resource(ctx.battler_mut(who), idx, *amount);
OpOutcome::Unchanged
}
Op::SetHp {
target: sel,
value,
when,
} => {
if !when
.iter()
.all(|p| pred_holds(ctx, bindings, relay, target, source, hook, Some(p)))
{
return OpOutcome::Unchanged; }
let who = resolve(*sel, target, source);
let cur = ctx.battler(who).hp;
if cur > *value {
let sink_amount = if *value == 0 { u16::MAX } else { cur - *value };
if bindings.redirect_hp_loss(ctx, who, source, sink_amount) {
return OpOutcome::Unchanged; }
}
ctx.battler_mut(who).hp = *value;
OpOutcome::Unchanged
}
Op::SetDamage { value, of } => {
let who = resolve(*of, target, source);
let dmg = match *value {
DamageValue::Const(c) => c,
DamageValue::UserLevel => bindings.battler_level(ctx.battler(who)),
DamageValue::RngScaledLevel { num, den } => {
let byte = ctx.rng.next_u8() as u32;
let level = bindings.battler_level(ctx.battler(who)) as u32;
let den = den.max(1);
let raw = (byte * num / den * level) / 256;
raw.min(u16::MAX as u32) as u16
}
};
ctx.mv.damage = dmg;
OpOutcome::Unchanged
}
Op::DamageCurrentHpFraction {
num,
den,
target: sel,
} => {
let who = resolve(*sel, target, source);
let cur = ctx.battler(who).hp as u64;
let den = (*den).max(1) as u64;
let amt = ((cur * *num as u64) / den).min(u16::MAX as u64) as u16;
let amt = if cur > 0 { amt.max(1) } else { 0 };
if !bindings.redirect_hp_loss(ctx, who, source, amt) {
ctx.battler_mut(who).take_damage(amt);
}
ctx.mv.damage = amt; OpOutcome::Unchanged
}
Op::RepeatHits {
count,
target: sel,
final_hit,
} => {
let who = resolve(*sel, target, source);
let per_hit = ctx.mv.damage;
let n = match count {
crate::model::HitCount::Fixed(k) => *k,
crate::model::HitCount::TwoToFive => determine_hit_count(ctx.rng.next_u8()),
};
for _ in 1..n {
if !bindings.redirect_hp_loss(ctx, who, source, per_hit) {
ctx.battler_mut(who).take_damage(per_hit);
}
}
if let crate::model::FinalHitRider::OnFinal { chance, ops } = final_hit {
let pass = ctx.rng.chance(chance.num, chance.den);
if pass {
for op in ops {
match apply_op(ctx, relay, target, source, bindings, hook, op) {
OpOutcome::Fail | OpOutcome::FailSilent => break,
OpOutcome::Unchanged | OpOutcome::Set(_) => {}
}
}
}
}
OpOutcome::Unchanged
}
Op::RemoveStatus { target: sel } => {
let who = resolve(*sel, target, source);
ctx.battler_mut(who).status = None;
OpOutcome::Unchanged
}
}
}
fn determine_hit_count(roll: u8) -> u8 {
if roll < 96 {
2
} else if roll < 192 {
3
} else if roll < 224 {
4
} else {
5
}
}
fn resolve_amount<P: RulesProvider>(ctx: &mut BattleCtx<'_, P>, spec: AmountSpec) -> u16 {
match spec {
AmountSpec::Const(c) => c,
AmountSpec::RngMask { mask, plus } => {
let byte = ctx.rng.next_u8();
(byte & mask) as u16 + plus as u16
}
AmountSpec::RngRange { lo, hi } => {
let span = hi.saturating_sub(lo).saturating_add(1).max(1);
if span >= 256 {
let byte = ctx.rng.next_u8() as u16;
return lo + (byte % span);
}
let limit = 256 - (256 % span);
let mut byte = ctx.rng.next_u8() as u16;
let mut tries = 0;
while byte >= limit && tries < 64 {
byte = ctx.rng.next_u8() as u16;
tries += 1;
}
lo + (byte % span)
}
}
}
fn resolve(sel: Selector, target: BattlerRef, source: BattlerRef) -> BattlerRef {
match sel {
Selector::Target | Selector::Host => target,
Selector::Source => source,
Selector::Foe => BattlerRef::new(if target.side == 0 { 1 } else { 0 }, target.slot),
}
}
fn fraction_amount<P: RulesProvider>(
ctx: &BattleCtx<'_, P>,
who: BattlerRef,
of: FractionOf,
num: u32,
den: u32,
) -> u16 {
let den = den.max(1) as u64;
match of {
FractionOf::MaxHp => {
let base = ctx.battler(who).max_hp as u64;
((base * num as u64) / den).min(u16::MAX as u64) as u16
}
FractionOf::CurHp => {
let base = ctx.battler(who).hp as u64;
((base * num as u64) / den).min(u16::MAX as u64) as u16
}
FractionOf::LastDamage => {
let base = ctx.mv.last_damage as u64;
let amt = ((base * num as u64) / den).min(u16::MAX as u64) as u16;
if base > 0 {
amt.max(1)
} else {
0
}
}
}
}
fn pred_holds<P: RulesProvider>(
ctx: &BattleCtx<'_, P>,
bindings: &P::Bindings,
relay: RelayVar,
target: BattlerRef,
source: BattlerRef,
hook: &CompiledHook,
pred: Option<&Predicate>,
) -> bool {
let Some(pred) = pred else { return false };
match pred {
Predicate::HasType(name) => match host_type_index::<P>(name) {
Some(idx) => bindings.has_type(ctx.battler(target), idx),
None => false,
},
Predicate::StatIs(name) => {
match (host_stat_index::<P>(name), bindings.current_stat_index(ctx)) {
(Some(want), Some(cur)) => want == cur,
_ => false,
}
}
Predicate::RelayIntLt(n) => relay.as_int() < *n,
Predicate::HasVolatile(name) => bindings.has_volatile(ctx, target, name),
Predicate::MoveTypeIsDefenderType => match hook.move_type_index {
Some(mti) => bindings.move_type_is_defender_type(ctx, mti, target),
None => false,
},
Predicate::TargetHasStatus(name) => match status_index::<P>(name) {
Some(idx) => bindings.has_status(ctx.battler(target), idx),
None => false,
},
Predicate::Not(inner) => {
!pred_holds(ctx, bindings, relay, target, source, hook, Some(inner))
}
Predicate::TargetHasAnyStatus => bindings.has_any_status(ctx.battler(target)),
Predicate::LevelGE => {
bindings.battler_level(ctx.battler(source))
>= bindings.battler_level(ctx.battler(target))
}
Predicate::SelfHpBelow { num, den } => {
let b = ctx.battler(source);
let den = (*den).max(1) as u64;
(b.hp as u64) * den < (b.max_hp as u64) * (*num as u64)
}
Predicate::SourceHasStatus(name) => match status_index::<P>(name) {
Some(idx) => bindings.has_status(ctx.battler(source), idx),
None => false,
},
}
}
fn host_type_index<P: RulesProvider>(name: &str) -> Option<usize> {
P::rules_host().and_then(|h| h.compiled.types.iter().position(|t| t == name))
}
fn host_stat_index<P: RulesProvider>(name: &str) -> Option<usize> {
P::rules_host().and_then(|h| h.compiled.stats.iter().position(|s| s == name))
}
fn host_resource_index<P: RulesProvider>(name: &str) -> Option<usize> {
P::rules_host().and_then(|h| h.compiled.resources.iter().position(|r| r == name))
}
fn status_index<P: RulesProvider>(name: &str) -> Option<usize> {
P::rules_host().and_then(|h| h.compiled.status_index(name))
}