Battle formulas
Damage, MP-cost, accuracy, and RNG arithmetic kernels used by the battle action state machine. The central damage-application primitive is FUN_800402F4; the engine-side mirror lives in crates/engine-vm/src/battle_formulas.rs.
Damage application primitive
FUN_800402F4 (~7,904 bytes / 1,976 instructions, dumped as ghidra/scripts/funcs/800402f4.txt) is a selector dispatch: switch(selector) { case 0..0x83 ... }. Each case is one “damage / status / stat-modify kind.” The function fills four local 8-pointer arrays (one per actor slot) over +0x14C (HP), +0x14E, +0x150 (MP), +0x152 before the selector switch.
The most common selectors:
- Selector 0 — basic damage. Subtracts attacker stat from defender stat, capped at the per-party-slot ceiling table at
DAT_8007655C. - Selector 9 — accuracy / evasion roll.
roll = rand() % (caster_acc + target_eva); hit whentarget_eva < roll. Standard JRPG-flat-roll model. - Selectors 1..7 — stat-buff ramps, multiplying
actor_record + 0x158..+0x16Aby6/5with a0xFFFFclamp.
Spirit damage formula
Hard-coded per Spirit super-art: damage = ((target_HP * 7) / 5) + 8, capped at 0x120 (288) for the larger arts and 100 for the smaller ones. This is the one place the engine reproduces a non-obvious arithmetic path; everything else is selector-dispatch driven.
MP cost & ability-bit modifiers
base_mp_cost = spell_table[spell_id].mp_cost;
if (character_record.ability_bits & 0x20) // "MP-half"
cost = base / 2;
else if (character_record.ability_bits & 0x10) // "MP-quarter"
cost = base / 4;
else
cost = base;
The character record's ability bitfield is the 4-byte field at +0xF4 (record stride 0x414, base 0x80084708). The engine port resolves the modifier via battle_formulas::MpCostModifier::from_ability_flags.
RNG primitive
FUN_80056798 is the in-game RNG — standard PsyQ rand() pattern: seed = seed * 1103515245 + 12345; return (seed >> 16) & 0x7FFF. Range 0..32767. The engine seeds it from the boot timer; for deterministic playback, the engine port must seed from the same source.
Engine port
crates/engine-vm/src/battle_formulas.rs ports the formulas above as pure functions:
spirit_damage(target_hp, cap)mp_cost_after_ability_bits(base, modifier)accuracy_roll(caster_acc, target_eva, rng_seed)psyq_rand_step(seed)buff_ramp(value)damage_cap_for_party_slot(caps, slot)
Unit tests pin the documented formulas as fixtures — a future runtime trace can add comparison cases without touching the formula bodies.
What's still open
- Selectors
0x10..=0x83. Beyond the status / buff / damage cases, these handle stat-up animations, status-clear, queue-end markers, and multi-target item slots. Mostly read-only ramps that don't affect game balance. - Stat-field semantics inside
+0x158..+0x16A. The buff order in selectors 1..7 implies an ATK / DEF / MAG / SPR / AGL / LUCK ordering, but mapping each halfword precisely needs a save-state diff before / after a known buff. - Ability-bit catalogue. The bitfield at
+0xF4has the documented MP-half / MP-quarter / HP-cap / MP-cap bits plus the0x10/0x20impact-step modifiers. The full per-character map comes out of save-data inspection.
Full reference
The full doc with provenance citations is at docs/subsystems/battle-formulas.md.