At a glance

Physical hits
FUN_801EC3E4 in the battle overlay (PROT 0898) - every direction command and Art strike
Magic
FUN_801DD0AC + finisher FUN_801DDB30 - Seru magic, monster specials, boss casts; INT-driven
Applicator
FUN_800402F4 in SCUS_942.54 - the selector dispatch most HP / status / buff changes funnel through
RNG
Standard PsyQ rand(): seed × 1103515245 + 12345, top 15 bits
In the port
engine-battle-vm::battle_formulas - pure functions, drawing rand() in retail order
Confidence
Confirmed against the disassembly, term by term (how we know)
  • Your attack is base ATK (the unequipped number) plus half the gear the command uses, folded at swing time - never at battle load.
  • An Art multiplies by 1.3 (1.4 with War Soul) and takes the element factor twice.
  • Damage = Offense − Defense, capped at 9999 - with an underdog rewrite instead of a floor-at-1 when the defense would win outright.
  • Magic ignores ATK and DEF. A Seru spell is the summoned creature's HP and INT plus twice the caster's INT, against the target's INT and a sliver of its HP and defences.

A physical hit

One routine, two rolls, subtracted. Integer arithmetic throughout; Rnd(1..1.125) is shorthand for x + rand() % (x/8 + 1) - a random 0-12.5% nudge, drawn separately for each side.

Base Offense = Base ATK + Equipment ATK / 2

Offense = [ Base Offense × Rnd(1..1.125) × Power / 16
          + Current HP / 256
          + Base Offense × Juggle / 64
          + Base Offense × Angle / 65536 ]
          × Arts Modifier × Element  (× Element again for an Art)

Defense = Base DEF × Rnd(1..1.125) + Base DEF × Distance / 1024

Damage  = Offense − Defense                 (cap 9999; underdog rewrite below)
Base Offense base ATK + one slot / 2 Offense roll x Rnd x Power/16 + HP/256 + juggle + angle Multipliers Art x1.3 (War Soul 1.4) element x1.04 / x0.96 Defense roll UDF or LDF x Rnd + distance; Spirit x3 Damage Offense - Defense underdog rewrite Finisher guard halve, cap 9999 spirit gauge fill
The attacker's side folds one equipment slot at half strength at swing time, rolls, scales by the move's power, then takes the Art and element multipliers; the defender's side rolls too; the difference is the number that pops.

Worked example - Vahn vs Evil Fly

ZetaPhoenix's playtest: Vahn at base ATK 188, weapon ATK 98, footwear ATK 86, Ra-Seru ATK 100, HP 2747; an Evil Fly with UDF 42 / LDF 49, neutral element. The combo is Left arm, Right arm, then Hyper Elbow (a 20-power, LDF-targeting Art); the random draws are the observed ones.

HitBase OffenseRndOffense termsOffenseDefenseDamage
1 - Left arm188 + 98/2 = 237+7 → 244244×20/16 = 305, HP 10, juggle 1 → 3, angle 0318UDF 42 +1, distance +1 = 44274
2 - Right arm188 + 100/2 = 238+10 → 248248×20/16 = 310, HP 10, juggle 2 → 7327UDF 42 +2 = 44283
3 - Hyper Elbow188 + (98+86+100)/2 = 330+20 → 350350×20/16 = 437, HP 10, juggle 1 → 5; sum 452, ×13/10587LDF 49 +2 = 51536

Total 274 + 283 + 536 = 1086, matching the in-game figure. The second hit's juggle 2 is why it out-damages the first from a near-identical base, and the Art takes all three gear ATKs at half and then the ×1.3 - which is where most of an Art's advantage comes from.

Which gear a command reads

Base ATK is the character's ATK with nothing equipped - the battle loader copies it into the actor with no equipment folded in (it folds UDF / LDF / SPD, and skips ATK). The menu's ATK figure is base plus all gear, so the two never match. The equipment half is added at swing time, per command:

CommandSlot readFold
Left armslot 2atk[2] / 2
Right armslot 3atk[3] / 2
High / Lowslot 4 (footwear)atk[4] / 2
Art hitslots 0-4(sum of all five) / 2
Monstersnoneno fold

The arms are keyed by slot, not item type: Vahn and Gala carry the weapon in slot 2 and the Ra-Seru in slot 3; Noa carries Terra in slot 2 and her claws in slot 3, so hers are swapped. The Art arm sums body and head too - those normally carry no ATK, so in practice it is weapon + Ra-Seru + footwear.

A Seru spell

Seru magic does not use ATK, DEF or a per-spell power number. The summoned creature is a real battle actor (slot 7), and the roll is built from live stats - its own, the caster's and the target's - in three stages:

Attack  = rand(0..summon INT) + summon HP + 2 × caster INT
Defense = rand(0..target INT/2) + target HP/256 + UDF/16 + LDF/16 + 2 × target INT

Attack  = Attack × affinity[summon element][target element] / 100
Attack  = Attack + Attack × (magic level − 1) / 8          (level 1..9)
if Defense + summon HP > Attack:  Attack = Defense + rand(0..summon INT/2) + summon HP

Damage  = (Attack − Defense) × caster power % / 100           (cap 9999)

Venom / Toxic scale either side by 9/10 / 7/10, and a defender in the Spirit stance doubles its Defense. The finisher then halves the damage for a party defender wearing the matching elemental guard (the melee kernel carries the same ladder, keyed on the attacking monster's element, so a plain monster swing is elemental too), halves it again for a guarding target, and replaces a fully-mitigated hit with rand() % 9 + 8.

The melee resolver is its own routine. The guard ladder has none of the shared finisher’s other stages: no zeroed-hit floor, no Mystic Shield halve and no guard halve, because the Spirit stance is already the Defense triple. Past the cap:

  • Stone. A Stoned defender takes no accumulate, no strip, no Spirit and no rumble.
  • The Mystic Shield. While it is up, an attacker on a clip below 0x10 is blocked whatever the defender’s poses, and an Art passes.
  • The block breaker. A hit it turned lands a quarter of its excess and plays the block pose on a defender left standing.
  • The drain clip. A monster on a clip whose +0x77 is 0x3B drains: the raw roll becomes raw × 7/4, the hit is that minus Defense, and the attacker heals by it.
  • The status arm. The clip’s +0x7A selects a roll on every swing: 3 Venom and 4 Toxic at 1/8, 5 a Rot limb always on a party target unless a guard passive stops it, 6 Curse at 1/4. A monster special reads the same ladder off its move record and stops at 5, so it cannot inflict Curse.
  • The Spirit percent. It is masked to a byte at every use here, where the finisher’s is not, so a hit of 256% of max HP adds nothing.

The whole body runs against the retail bytes in the kernel differential.

A spell the caster has not learned

The magic-level search walks the caster's 32-entry spell list and falls out with its index at 0x20 when the spell is absent. Slot 0x20 is still inside the record (the lists hold 0x24 entries), so an unlearned cast reads a level byte of 0: 0 − 1 negates the roll, the logical shift turns it into a huge positive number, and the finisher caps it at 9999. Play never reaches this, because a learned spell's level is at least 1, but an injected cast of an unlearned spell does.

The spell XP lands in the same slot. The finisher's accrual and the level check walk the same ids, so slot 0x20 is the list's own XP word and level byte; level 0 reads a zero threshold, and the first XP such a cast banks levels the slot to 1 and raises the "magic level increased" banner. The port's magic_xp::accrue_and_level takes the same slot.

Worked example (illustrative numbers)

Round numbers chosen to show the shape, not a specific spell: a summon with INT 60 and HP 200, cast by a character with INT 80 at magic level 5, against a monster with INT 40, HP 1000, UDF 120, LDF 100, neutral element.

  1. Attack rolldraw 30 + 200 + 2×80 = 390
  2. Defense rolldraw 10 + 3 + 7 + 6 + 2×40 = 106
  3. Elementneutral, ×100/100 = 390
  4. Magic level 5390 + 390×4/8 = 585
  5. Re-roll check106 + 200 = 306, not above 585 - kept
  6. Damage585 − 106 = 479 at 100% caster power

The target's INT counts twice, so a high-INT monster shrugs off magic that would flatten a brute of the same HP. Magic level is the big lever: level 9 doubles the attack roll.

Caster power. Each caster summons their own element at 100% and their opposed element weakest - Vahn fire 100 / water 40, Noa wind 100 / earth 40, Gala thunder 100 / dark 60, everything else 70-95. The same Seru in Vahn's hands and Noa's hands hits differently. Table at 0x801F5468 (PROT 0898 file 0x26C50).

Recovery summons skip the roll. Vera heals 32 × level + 224 (256 at level 1, 512 at level 9), capped at the missing HP; Orb heals 64 × level + 448 across the party row. A levelled Seru spell also stages an element-keyed stat debuff - see Seru-magic side-effects.

Running away

A Run is one draw for each side, compared. Every party slot counts (downed members included), and missing HP raises both sides' scores - a hurt party slips away more easily, a hurt pack pursues harder.

party score = Σ party   SPD × 3/2 + missing HP / 16
enemy score = Σ enemies SPD       + missing HP / 32

roll_p = rand() % party score        roll_e = rand() % enemy score
Chicken Heart (passive 0x34):         roll_p = roll_p × 3/2
Chicken King (0x37) or pre-emptive:   roll_p = roll_e        (forced tie)

caught if roll_p < roll_e, or the fight is a scripted (boss) fight

Worked example

  1. Partythree members, SPD 40 each, unhurt: 3 × 60 = 180
  2. Enemiestwo, SPD 50 each, unhurt: 100
  3. Drawsroll_p in 0..179, roll_e in 0..99
  4. Caughtwhen roll_p < roll_e: 27.5% of the time
  5. Escape72.5%; about 81.5% with Chicken Heart

The tie arms guarantee only that the compare cannot fail: both run before the scripted-fight test, so neither Chicken King nor a pre-emptive strike gets you out of a boss fight. A back attack is not the mirror image - it never touches this roll and only costs the party its first-round turns. On success the battle ends with no loot and downed members floored at 1 HP (which is how fleeing cures Stone); on failure the turn is spent and the battle continues.

The physical hit, term by term

TermWhat it is
Rnd(1..1.125)x + rand() % (x/8 + 1) - between x and x + x/8. Drawn separately for attack and defense.
PowerThe move's power tier: 20 for a normal direction hit; each Art strike carries its own from [12, 18, 20, 22, 28]. Applied as × Power / 16.
Current HP / 256The attacker's current HP, shifted down.
Juggle1 on a chain's first hit, +1 per further hit that lands while the defender is still reeling; back to 1 otherwise, and always 1 against a block. Applied as × ATK / 64. See the juggle window.
AngleThe folded facing difference between attacker and defender, 0 face-on; only a chain's first hit carries it, bounded at ATK / 32. Applied as × ATK / 65536.
Arts Modifier×13/10 for an Art; ×14/10 with the War Soul accessory (Arts Power passive); ×1 for a plain hit.
ElementFrom the 8×8 matchup matrix: same element 96/100, opposed pairs (fire-water, earth-wind, light-dark) 104/100, else 100/100. Applied once per hit and a second time inside the Art arm.
Base DEFDefender UDF for a high-class strike, LDF for a low-class one. Party defenders do get their equipment's defence bytes here.
DistanceAccumulated per step of the attacker's walk-in and zeroed once the first hit lands; × DEF / 1024. Measured ~30 for a straight walk-in from the starting line.
Spirit stanceDefense ×3 when the defender chose Spirit, or is fleeing while a party member wears Safe Escape.
Venom / ToxicScales a side ×9/10 (Venom) or ×7/10 (Toxic) - the attacker's status on Offense, the defender's on Defense.

A zero-damage enemy strike is a block

A monster's ordinary swing that leaves a party member untouched is not a to-hit miss: it is a block, rolled per swing on that swing's own power byte. It only runs when the defender has a block clip, and both sides build a roll from SPD, four-fifths of the unfolded ATK and the angle and distance words:

s0 = max(attacker roll, defender roll)
s0 += (rand() % s0) × BLOCK[(power byte − 0x0C) % 5] / 2     BLOCK = [6, 4, 4, 4, 2]
s1 += rand() % s1
Spirit stance ×3/2 (defender), Art slot 0x11 ×3/2 (attacker), Curse ×8/10 either side,
block passives ×3/2 or forced ties, guard-disable status: s0 = s1
blocked = s0 < s1

A defender with no block clip draws nothing. A blocked hit forces the juggle counter back to 1.

Measured. A breakpoint probe on the roll reads both sides, the seed and the verdict on every hit; fed the same seeds, the port's verdicts match retail's on every hit of three recorded fights (Nova's five swings on Gala, Gaza's four Astral Slash beats on Vahn, Vahn's two rolls on the Ropeway monster). Astral Slash shows the juggle arm overturning the roll: beats two to four each roll a block against Vahn's Spirit stance and block passive, and the juggle arm clears each because Vahn is mid-flinch rather than in his block pose, so all four land. On a stream whose first beat blocks, the defender holds the block clip and every later beat is blocked too: the all-or-nothing outcome is the rule, and the stream decides which way it goes.

The underdog floor

When Offense does not clear the defense, the routine does not floor the hit at 1 - it rebuilds the attack on top of the defense: Defense + (3/4 × Offense + rand() % (Offense/4 + 1)) × Power / 64 plus the juggle terms, re-applies a reduced Art multiplier (×11/10, or ×12/10 with War Soul) and the element pass, and finally a chip floor guarantees a few points (rand()%3 + 3 plain, rand()%4 + 5 for an Art). The "minimum damage" against a heavily-armoured enemy is therefore a fraction of your own roll, not a constant. A petrified defender takes nothing; the whole hit caps at 9999.

The juggle window - what makes a monster "juggleable"

Every hit adds BaseOffense × juggle / 64. The counter climbs by one for each hit that lands while the defender is still reeling from the last, and snaps back to 1 if you miss the window. The bonus is modest - the fourth hit of a juggle carries an extra ~6% - but it is the term that rewards a tight string. Three facts define it:

  • The window is a property of the reaction clip, not the monster's stats: it is open while the defender's current clip is before its first authored event frame. Two bytes of the light-flinch animation entry - a beat and a rate - set its length: window = 4 × beat / rate ticks at 60 Hz in normal play.
  • It restarts on every hit that lands inside it, so what matters is the gap between consecutive hits, never the length of the string.
  • An Art's slow-motion halves or quarters everyone's play speed, which doubles or quadruples every window while it runs - part of why Arts chain so well.
DefenderFlinch beat/rateWindowRead as
Rogue1/22 tickseffectively un-jugglable
Bee family5/210 ticks
Gobu Gobu8/216 ticks
Zeto16/232 ticks
Caruban10/140 tickshit again within ~0.67 s and the counter climbs
Vahn (party)3/112 tickswhy enemy swings, 24+ ticks apart, never juggle him

Party members have the same mechanic - the kernel reads the defender's reeling byte whichever side it is on. legaia-patcher monster-stats prints the juggle column for every monster.

Deep dive: how the reeling byte works

Two writers on the whole disc. Scanning SCUS_942.54 and every overlay image for a store to actor +0x1F7 finds exactly two, four bytes apart, both in the per-frame anim tick FUN_80047430 (0x80047E50 stores 0, 0x80047E54 stores 1); the battle overlay only reads it. A live write-watch across three battle states sees no other writer.

What it stores. Every tick, after advancing the actor's clip cursor: frame = cursor >> 4; byte = (frame < record[+0x10]) ? 1 : 0 - where record[+0x10] is the clip's first event frame, the same list that marks an attack's contact frame. The byte is re-derived each tick from whichever clip the actor is in, not a loaded timer.

On a hit the defender's light flinch (tag 2) is staged with the cursor at 0 and the byte goes up. Each tick the cursor gains speed × rate / 2 sixteenths of a frame (speed = actor +0x21D, 8 in normal play, dropped to 4 / 2 on everyone by Art slow-motion; rate = the entry's +0x78). Hence window = 32 × beat / (speed × rate) anim passes. The commit zeroes the cursor (sh zero,0x68(s5) at 0x8004B068), so no fraction carries into the new clip; the per-pass advance is also multiplied by the frame step (DAT_1F800393, vsyncs since the last pass), so in vsyncs the window ends on the first pass boundary at or past the formula and a long pass overshoots it by a vsync or two. A hit inside the window grows the counter and restarts the flinch; a blocking defender is never juggled. Set sites: 0x801ECA20..0x801ECA80. The anim commit reads the same byte (0x8004AFD4) for one thing only: a flinch re-staged while it is up restarts with its sound-cue cursor at 1 (0x8004B05C), so the first cue is not replayed over the one still ringing.

Live checks. Against Gilium (f8/2) the counter reads 2, 3, 4 on hits 8-12 ticks apart during an Art (window doubled to 32 by slow-motion), 1 on every hit after the drop; Vahn's own f3/1 = 12-tick window measured three times against Gobu Gobu's three hits 24-26 ticks apart, counter 1 each time. Side effects of "whichever clip the actor is in": an actor struck during its own wind-up counts as juggled, and one Cort's beat lies past its 7-frame flinch, so its byte holds for the whole clip.

The smaller formulas

FormulaExpressionWorked example
Accuracy / evasionrand() % (acc + eva); hit when eva < roll - hit chance acc / (acc + eva)acc 120 vs eva 40: hits 75% of the time
Turn orderinitiative key = SPD + rand(0..SPD/2) + 1 per round; highest living key actsSPD 60 draws 61..91 - a SPD 40 actor (41..61) can still edge a bad roll
Back attack / pre-emptivecompare the two sides' mean SPD, each blurred by a random spread; then a 1-in-16 gate (1-in-2 for pre-emptive with the pre-emptive passive, 1-in-64 for a back attack with the guard passive)the side caught out faces away and loses its round-one turns; skipped entirely in a scripted fight
MP costMP-half passive: base - (base >> 1); MP-quarter: base - (base >> 2); half wins when both are worna 5-MP spell costs 3 with MP-half (rounds up, not down); 40 → 30 with MP-quarter
Spirit turnnext turn's arts bar = min(base AGL × 7/5 + 8, 288); Spirit gauge +32 (every other action pays +8), cap 100; the guard stance triples Defense against a physical hit and halves magic damageAGL 150: the bar grows from 150 to 218 AP for the next Arts input
Escape (Run)see running awaya fast, wounded party out-rolls a slow healthy pack most rounds
Stat buffworking stat × 6/5, clamp 0xFFFF; each stat is a working/base halfword pairATK 300 → 360 for the fight
Seru-magic stat debuffstat - stat × pct / 100 per hit, pct = 5 / 10 / 15 / 20 by magic-level band 3-4 / 5-6 / 7-8 / 9; the stat is picked by the summon's element (below)two level-5 Gimard hits: ATK 300 → 270 → 243
Victory goldΣ gold/2 over seats that died and were not captured, ×1.25 with the gold passive, then halved; lone enemy floor((gold/2)/2)Gimard (record gold 60) credits exactly +15
Victory EXPΣ exp over the same seats × 3/4, ceiling-split over members standing and not Stonedtwo survivors of a 400-EXP fight get 150 each
RNGseed = seed × 1103515245 + 12345; return (seed >> 16) & 0x7FFFrange 0..32767 - the BIOS rand(), its one seed the kernel word 0x80009010; the engine's world stream steps the same recurrence

The frame driver burns one draw a pass

The battle frame driver FUN_80046A20 calls rand() on every pass (jal 0x80056798 at 0x80046D2C) and overwrites the result at 0x80046D34 without reading it. So where the stream stands at a hit, damage, AI or status roll depends on how many driver passes ran before it, not only on which decisions were made. A breakpoint count from a state parked at the Begin prompt sees exactly one draw per pass and no other site: 150 draws over 300 idle vsyncs.

Three things therefore move a round’s later rolls once its turn order is fixed: how long the player waits before pressing Begin, the button presses that cut a wait short (the victory banner, the Seru-absorb banner, the failed-run message), and anything that changes the frame step or how many passes a CD load takes. The port reproduces it: World::tick_battle_pass_draw burns one draw on the first tick of every battle frame, and a world toggle turns it off for an A/B.

Retail has one rand() stream; the overlays' own generators are separate words, not reseeds. The battle intro's particle seeders are on that stream: the world steps past their draws on the transition's first frame and the emitter replays them from the word it left (cutscenes). The port's remaining private-seed stand-ins (the Nova whirlwind ring yaws, the scripted-motion channels, the opt-in level-up jitter) decide nothing a battle channel reads.

How the AP gauge fills

The 0..100 AP gauge that Super and Miracle Arts spend fills three ways, and nothing else in the battle code adds to it:

  • End of every action: the actor's art cost comes off first, then +8 goes on - or +32 for a Spirit action, in place of the 8, not on top of it. The AP Boost accessories add a quarter or a tenth of that. So a Miracle Art (99 AP) from a full gauge leaves 9.
  • Taking a hit: the defender gains max(1, damage × 100 / max HP), plus a quarter or a tenth of that same base percent for a party defender wearing AP Boost. The attacker's own hit adds nothing to its gauge. The gauge is a halfword and each add is stored back before the cap of 100 is tested, so a percent past 0xFFFF (a huge hit on a tiny max HP) wraps the gauge, and a wrap that lands at or under 100 stays.
  • A level-9 Vera, Orb or Spoon doubles each healed ally's AP, capped at 100. Order matters: healed after their turn, 17 AP becomes (17 + 8) × 2 = 50; healed before it, 17 × 2 + 8 = 42.

Maximum AP is not a fill: the per-frame passive sweep pins a living wearer's gauge at 100.

First reported by the_rabidsquirel from save-state testing on retail, and confirmed against the battle overlay's code.

Actor stats and the monster record

The per-actor stat block runs +0x14C..+0x16A, each stat a pair of halfwords (working value, base). The loader copies each monster-record halfword into it; the names come from the formulas that consume each slot:

RecordActor pairStatConsumed by
+0x0C+0x14CHPthe applicator's HP delta
+0x10+0x150MPcast costs
+0x0E+0x154AGLthe per-turn action budget (the party's arts bar, the AI's move queue), restored each round; "Power Up" raises it
+0x12+0x158ATKattacker's offense
+0x14+0x15CUDFdefense vs high-class strikes
+0x16+0x160LDFdefense vs low-class strikes
+0x18+0x168INTmagical damage and magic defense; seeds accuracy / evasion
+0x1A+0x164SPDthe initiative key and the escape roll

The single "Defense Up" buff raising UDF and LDF together confirms they are two facets of one defense. The parser exposes all six via legaia_actor_formats::monster_archive::MonsterRecord; the enemy table renders them per enemy, along with each monster's castable spell list and AGL costs (decoded from the record's spell-offset array - entry head byte is the spell id, +0x74 the AGL cost the AI checks and spends).

Seru-magic side-effects

A levelled Seru spell does more than damage: the stager FUN_801F3D3C, called once per cast from inside the spell's own summon module, stages a secondary effect keyed on the summoned creature's element (the slot-7 record's +0x1D, the byte the affinity scale reads) and scaled by the caster's magic level for the spell (record +0x161). It writes a percent to 0x801F6960 and a banner string to 0x800775B4; the damage finisher FUN_801DDB30 then shaves stat × pct / 100 off the target on every hit. The table is 0x801F6870 (PROT 0898 file 0x28058), parsed by legaia_game_tables::seru_side_effect.

Summon elementEffectlv 3-4lv 5-6lv 7-8lv 9
0 earthDEF down - all four defence halfwords5%10%15%20%
1 waterAGL down - the gauge base +0x156 only5%10%15%20%
2 fireATK down5%10%15%20%
3 windSPD down5%10%15%20%
4 thunderINT down5%10%15%20%
5 lightcure class (party targets)1234
6 darkMP down - current MP +0x150 only5%10%15%20%

Why bosses shrug most of it off. Level below 3 stages nothing. In a scripted fight (a formation row's non-zero header byte - every boss but Lapis), four casts in five are suppressed unless the spell is light or affinity[summon][the formation’s first monster] ≥ 101. Then, for earth / fire / wind / thunder / dark in a scripted fight and for water always, the target's base stat halfword is compared with the raw record and the effect is dropped when they differ. Because the scripted boost profile has already raised ATK, UDF/LDF and INT, those three debuffs can never land on a boss; SPD and AGL land once per battle - the first cast that passes moves the value the compare reads, and every later cast of that element prints "No effect."; MP-down compares a value the hit never moves and lands every time.

A random encounter runs neither the roll nor the compare (water aside), so everything lands and stacks. There is no per-monster immunity field - the record's +0x24..+0x43 is zero across the roster. The enemy table renders the verdict per enemy; the "No effect." banner is the return-from-fade pass FUN_801F3C34 announcing a suppressed or already-landed effect.

Victory spoils

EXP / gold / drop are inline in each monster record; the spoils routine walks the seats that died and were not captured, so a Seru the Ra-Seru took pays nothing and neither does a monster that ran, and applies the scaling in the table above. The level-ups run ahead of the drop walk, and a fallen member’s Items Up counts because the EXP loop has already stood them up:

RecordFieldNote
+0x44 u16base goldscaled, then committed to the wallet (clamp 99,999,999)
+0x46 u16base EXP×3/4, ceiling-split over members standing and not Stoned; a fallen member stands at 1 HP and earns nothing
+0x48 u8drop item id0 = no drop
+0x49 u8drop chance %one rand()%100 < chance per enemy (+30 with Items Up); at most one item, then a 1-in-4 gate

The drop is one item per battle, not one per enemy. Every enemy costs one rand() % 100 against its chance (plus 30 when a living member carries Items Up), and the last enemy whose roll wins supplies the item - a captured one cannot. Then, unless some enemy has a 100% chance or the bonus applies, a further rand() & 3 must come up zero or the drop is lost: a lone 10% enemy drops 2.5% of the time. The item is not granted when 99 are already held.

Runtime-confirmed by write-watchpoint (Gimard credits exactly +15 gold) and cross-checked against the curated game-data tables (Gimard drops Healing Leaf at 10%).

Other regions pay more. The record bytes are identical on every disc, but the JP original and the three PAL executables skip the final gold halving and the EXP cut: a lone enemy pays half its record gold there against a quarter on NTSC-U, and the whole record EXP is split instead of three quarters. Zeto pays 2000 G on NTSC-U and 4000 G on PAL - the source of community tables that show "double" gold.

The special-battle word _DAT_8007BAC0 is not the scripted-fight flag - a boss row leaves it alone. It is non-zero in an arena leg and in the two Ra-Seru-forbidden fights (the Rim Elm ambush, first monster 0xAF), and any bit of it zeroes the gold and each member's EXP, skips the drop walk and the steal, makes the Seru absorb answer "known", skips spell XP and drops a monster flee the roll granted. The same word opens no result window at the end, ends the battle as a party wipe once the leader has all three limbs rotted, and in an arena leg hands a Run round's first turn to the leader and records the leg as run; its 0x100 bit forces the party's flee.

In the port

engine-battle-vm::battle_formulas carries every formula on this page as a pure function, drawing rand() in retail call order so the RNG stream matches: physical_predamage for the melee kernel, the summon kernels for Seru magic, damage_finish and spirit_gauge_fill for the finisher, escape_roll, the spoils scaling and the drop roll. Element affinity applies in both directions and by slot, not spell: a player Seru cast attacks as the summoned creature.

Deep dive: the applicator, and what stays open

The applicator FUN_800402F4 (~1,976 instructions) is a selector dispatch, switch(selector) { case 0..0x83 }, over four 8-pointer arrays covering each actor's HP / MP pairs. Selector 0 applies the pending HP delta against the per-party-slot damage-cap table; selector 9 is the accuracy roll; selectors 1..7 are the ×6/5 stat-buff ramps (each touching a working/base halfword pair). The table has 132 slots but only 15 distinct arms, and 116 of them - every selector from 0x0F to 0x83 bar one - are the function's own epilogue, the same place an out-of-range selector lands. The only body above 0x0E is a single call that ramps screen brightness. The arms that do work are all at or below 0x0E: status clear, an opposed-INT status roll, learning a Tactical Art, and discharging the Point Card counter as damage.

The spirit-gauge fill exists twice in overlay 0898 - one inlined copy in the magic finisher, one in the physical resolver. A mod that edits only the first leaves ordinary enemy swings running stock.

What stays open: no live hunt is indexed for this page on open RE threads. The juggle window's former residuals are settled: the four-slot read is fixed by the fight's heap placement (monster animation), and the overrun is the pass quantisation described above. The ability bitfield at record +0xF4 / +0xF8 is fully catalogued as the 64 passive indices (accessory passive table). The anim commit's own read of the reeling byte is not a parry window: it starts a juggled re-hit's flinch past its first sound cue. Not open: retail's only stat debuffs are the Seru side-effects, which run on their own path, and the strike-angle word cannot go negative because its writer folds before it biases. In the port, the Slow halving of the turn order and the scripted-fight one-tick DoT clear are modelled, and Jedo's non-scripted effect on the monster seats is carried by its summon director (cast module).

The guard passives work through one sweep. Each battle frame the template tick calls a sweep that strips a party actor's status bits when its record carries the matching guard passive (Poison, Rot, Curse, Stone, Master Guard and the guard-disable status), with no HP test. Most appliers (the melee status arms, an enemy special's Venom and Toxic, a cast's raw bits) have no guard test of their own, so the sweep is what makes those guards work. The port runs it at the head of each battle frame (World::tick_battle_passive_sweep).

Xain's Bloody Horns pins the guard-bypass wrapper to the unit: a retail recording lands 595 on Gala, and the port's kernel on the recording's rand() words and inputs lands 595. The attacker's HP / 256 term is live: on Xain's full HP the same draws read 605.

How we know

Function / dataAddressWhat it provesDump
Melee kernelFUN_801EC3E4The whole physical pipeline: roll pair, power tiers at 0x801F64EC, juggle / angle terms, Art ×13/10, element matrix at 0x801F53E8, underdog rewrite, chip floor.overlay_0898_801ec3e4.txt
Equipment fold armsPTR_801CF4B4 table, arms 0x801ECBC4..0x801ECDE4Which slot each command reads, halved; the arms live in a listing gap and are read straight from overlay bytes.overlay 0898 bytes
Battle loaderFUN_80053CB8ATK is seeded base, equipment folded only at swing time.80053cb8.txt
Reeling byteFUN_80047430The only two writers of +0x1F7; the first-event-frame derivation.80047430.txt; live write-watch
Magic kernel + finisherFUN_801DD0AC / FUN_801DD864 / FUN_801DDB30INT-driven roll pair, affinity / status / magic-level scale, re-roll; per-caster summon power table at 0x801F5468; finisher stages.overlay_battle_action_801dd0ac.txt
ApplicatorFUN_800402F4Selector dispatch; HP caps at DAT_8007655C; accuracy roll; buff ramps.800402f4.txt
MP-cost modifierFUN_801E295C state 0x28Subtract-a-shift (rounds up); MP-half short-circuits MP-quarter.overlay_0898_801e295c.txt
Formation advantageFUN_80051D84Mean-SPD roll, the two passives, the latch the escape roll reads.80051d84.txt
Escape rollFUN_801E791CParty / enemy score formulas, two draws, accessory bits, the pre-emptive tie.overlay 0898 dump
SpoilsFUN_8004E568Gold / EXP scaling; record fields +0x44..+0x49; runtime write-watch on the wallet.8004e568.txt
RNGFUN_80056798PsyQ LCG constants; 15-bit output.80056798.txt

Each claim of the community analysis is checked against the disassembly; every term is confirmed, with one correction (the random source is 15-bit, immaterial after the modulo) and two open magnitudes (the negative-angle behaviour; the walk-in distance is runtime state, not a constant). The full claim-by-claim table, register-level stage list and address appendix - plus the spell-XP accrual, the poison ticker and the monster escape roll - are in docs/subsystems/battle-formulas.md.

Credits and sources

  • ZetaPhoenix - the Offense Value / Defense Value shape, the per-command equipment halving, the Rnd(1..1.125) reading, the juggle / angle / distance terms, the juggleability question (his guess - the state of the damage animation - was right) and the worked example, all measured against the running game and checked here against the disassembly. His Legaia Arts Data spreadsheet is what the legaia-art trigger tables are validated against.
  • Meth962 - the original forum analyses the above corrects: thread 800 and thread 941 (Wayback Machine); his 100% walkthrough grounds the curated enemy tables.
  • the_rabidsquirel - how the AP gauge fills: the end-of-action +8, Spirit's +32 in its place, the AP a party member gains when an enemy's hit lands on them, the 9 AP a Miracle Art leaves, and the level-9 heal that doubles a target's AP, measured with save states on retail.

See also