Battle formulas
How much a hit does, how a Seru spell is rolled, and whether you get away - plus the smaller rolls around them: turn order, MP costs, spoils, the RNG. Every formula is traced from the game's own code. The Offense Value / Defense Value shape follows ZetaPhoenix's community analysis, which corrected Meth962's earlier forum write-ups - see credits.
At a glance
- Physical hits
FUN_801EC3E4in the battle overlay (PROT 0898) - every direction command and Art strike- Magic
FUN_801DD0AC+ finisherFUN_801DDB30- Seru magic, monster specials, boss casts; INT-driven- Applicator
FUN_800402F4inSCUS_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, drawingrand()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)
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.
| Hit | Base Offense | Rnd | Offense terms | Offense | Defense | Damage |
|---|---|---|---|---|---|---|
| 1 - Left arm | 188 + 98/2 = 237 | +7 → 244 | 244×20/16 = 305, HP 10, juggle 1 → 3, angle 0 | 318 | UDF 42 +1, distance +1 = 44 | 274 |
| 2 - Right arm | 188 + 100/2 = 238 | +10 → 248 | 248×20/16 = 310, HP 10, juggle 2 → 7 | 327 | UDF 42 +2 = 44 | 283 |
| 3 - Hyper Elbow | 188 + (98+86+100)/2 = 330 | +20 → 350 | 350×20/16 = 437, HP 10, juggle 1 → 5; sum 452, ×13/10 | 587 | LDF 49 +2 = 51 | 536 |
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:
| Command | Slot read | Fold |
|---|---|---|
| Left arm | slot 2 | atk[2] / 2 |
| Right arm | slot 3 | atk[3] / 2 |
| High / Low | slot 4 (footwear) | atk[4] / 2 |
| Art hit | slots 0-4 | (sum of all five) / 2 |
| Monsters | none | no 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
0x10is 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
+0x77is0x3Bdrains: the raw roll becomesraw × 7/4, the hit is that minus Defense, and the attacker heals by it. - The status arm. The clip’s
+0x7Aselects a roll on every swing:3Venom and4Toxic at 1/8,5a Rot limb always on a party target unless a guard passive stops it,6Curse at 1/4. A monster special reads the same ladder off its move record and stops at5, 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.
- Attack rolldraw 30 + 200 + 2×80 = 390
- Defense rolldraw 10 + 3 + 7 + 6 + 2×40 = 106
- Elementneutral, ×100/100 = 390
- Magic level 5390 + 390×4/8 = 585
- Re-roll check106 + 200 = 306, not above 585 - kept
- 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
- Partythree members, SPD 40 each, unhurt: 3 × 60 = 180
- Enemiestwo, SPD 50 each, unhurt: 100
- Draws
roll_pin 0..179,roll_ein 0..99 - Caughtwhen
roll_p < roll_e: 27.5% of the time - 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
| Term | What it is |
|---|---|
Rnd(1..1.125) | x + rand() % (x/8 + 1) - between x and x + x/8. Drawn separately for attack and defense. |
| Power | The 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 / 256 | The attacker's current HP, shifted down. |
| Juggle | 1 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. |
| Angle | The 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. |
| Element | From 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 DEF | Defender UDF for a high-class strike, LDF for a low-class one. Party defenders do get their equipment's defence bytes here. |
| Distance | Accumulated 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 stance | Defense ×3 when the defender chose Spirit, or is fleeing while a party member wears Safe Escape. |
| Venom / Toxic | Scales 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 / rateticks 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.
| Defender | Flinch beat/rate | Window | Read as |
|---|---|---|---|
| Rogue | 1/2 | 2 ticks | effectively un-jugglable |
| Bee family | 5/2 | 10 ticks | |
| Gobu Gobu | 8/2 | 16 ticks | |
| Zeto | 16/2 | 32 ticks | |
| Caruban | 10/1 | 40 ticks | hit again within ~0.67 s and the counter climbs |
| Vahn (party) | 3/1 | 12 ticks | why 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
| Formula | Expression | Worked example |
|---|---|---|
| Accuracy / evasion | rand() % (acc + eva); hit when eva < roll - hit chance acc / (acc + eva) | acc 120 vs eva 40: hits 75% of the time |
| Turn order | initiative key = SPD + rand(0..SPD/2) + 1 per round; highest living key acts | SPD 60 draws 61..91 - a SPD 40 actor (41..61) can still edge a bad roll |
| Back attack / pre-emptive | compare 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 cost | MP-half passive: base - (base >> 1); MP-quarter: base - (base >> 2); half wins when both are worn | a 5-MP spell costs 3 with MP-half (rounds up, not down); 40 → 30 with MP-quarter |
| Spirit turn | next 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 damage | AGL 150: the bar grows from 150 to 218 AP for the next Arts input |
| Escape (Run) | see running away | a fast, wounded party out-rolls a slow healthy pack most rounds |
| Stat buff | working stat × 6/5, clamp 0xFFFF; each stat is a working/base halfword pair | ATK 300 → 360 for the fight |
| Seru-magic stat debuff | stat - 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 Stoned | two survivors of a 400-EXP fight get 150 each |
| RNG | seed = seed × 1103515245 + 12345; return (seed >> 16) & 0x7FFF | range 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
+8goes on - or+32for 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 past0xFFFF(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:
| Record | Actor pair | Stat | Consumed by |
|---|---|---|---|
+0x0C | +0x14C | HP | the applicator's HP delta |
+0x10 | +0x150 | MP | cast costs |
+0x0E | +0x154 | AGL | the per-turn action budget (the party's arts bar, the AI's move queue), restored each round; "Power Up" raises it |
+0x12 | +0x158 | ATK | attacker's offense |
+0x14 | +0x15C | UDF | defense vs high-class strikes |
+0x16 | +0x160 | LDF | defense vs low-class strikes |
+0x18 | +0x168 | INT | magical damage and magic defense; seeds accuracy / evasion |
+0x1A | +0x164 | SPD | the 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 element | Effect | lv 3-4 | lv 5-6 | lv 7-8 | lv 9 |
|---|---|---|---|---|---|
| 0 earth | DEF down - all four defence halfwords | 5% | 10% | 15% | 20% |
| 1 water | AGL down - the gauge base +0x156 only | 5% | 10% | 15% | 20% |
| 2 fire | ATK down | 5% | 10% | 15% | 20% |
| 3 wind | SPD down | 5% | 10% | 15% | 20% |
| 4 thunder | INT down | 5% | 10% | 15% | 20% |
| 5 light | cure class (party targets) | 1 | 2 | 3 | 4 |
| 6 dark | MP down - current MP +0x150 only | 5% | 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:
| Record | Field | Note |
|---|---|---|
+0x44 u16 | base gold | scaled, then committed to the wallet (clamp 99,999,999) |
+0x46 u16 | base EXP | ×3/4, ceiling-split over members standing and not Stoned; a fallen member stands at 1 HP and earns nothing |
+0x48 u8 | drop item id | 0 = no drop |
+0x49 u8 | drop 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 / data | Address | What it proves | Dump |
|---|---|---|---|
| Melee kernel | FUN_801EC3E4 | The 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 arms | PTR_801CF4B4 table, arms 0x801ECBC4..0x801ECDE4 | Which slot each command reads, halved; the arms live in a listing gap and are read straight from overlay bytes. | overlay 0898 bytes |
| Battle loader | FUN_80053CB8 | ATK is seeded base, equipment folded only at swing time. | 80053cb8.txt |
| Reeling byte | FUN_80047430 | The only two writers of +0x1F7; the first-event-frame derivation. | 80047430.txt; live write-watch |
| Magic kernel + finisher | FUN_801DD0AC / FUN_801DD864 / FUN_801DDB30 | INT-driven roll pair, affinity / status / magic-level scale, re-roll; per-caster summon power table at 0x801F5468; finisher stages. | overlay_battle_action_801dd0ac.txt |
| Applicator | FUN_800402F4 | Selector dispatch; HP caps at DAT_8007655C; accuracy roll; buff ramps. | 800402f4.txt |
| MP-cost modifier | FUN_801E295C state 0x28 | Subtract-a-shift (rounds up); MP-half short-circuits MP-quarter. | overlay_0898_801e295c.txt |
| Formation advantage | FUN_80051D84 | Mean-SPD roll, the two passives, the latch the escape roll reads. | 80051d84.txt |
| Escape roll | FUN_801E791C | Party / enemy score formulas, two draws, accessory bits, the pre-emptive tie. | overlay 0898 dump |
| Spoils | FUN_8004E568 | Gold / EXP scaling; record fields +0x44..+0x49; runtime write-watch on the wallet. | 8004e568.txt |
| RNG | FUN_80056798 | PsyQ 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 thelegaia-arttrigger 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+32in 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.