Battle action helpers
The battle action state machine owns the order of a turn and almost none of its arithmetic. Range tests, damage rolls, the escape roll, AI picks, effect and summon spawns, camera framings and voice cues all live in helper routines it calls, or that run beside it each frame. This page is the map of those helpers: what each reads and writes, and the constants it carries.
At a glance
- Where
- Mostly the battle overlay, PROT 0898 at base
0x801CE818; a few inSCUS_942.54 - Context
- The battle context struct, pointer at
0x8007BD24:ctx[7]is the action state,ctx[+0x13]the acting slot - Actors
- Pointer table
0x801C9370, eight slots:0..2party,3..monsters - Roster ids
DAT_8007BD10[slot], 1-based: Vahn, Noa, Gala, and4the AI companion- Slot B
- The second overlay slot at
0x801F69D8where cast modules page in - Port
engine-battle-vm::battle_action,battle_formulas,battle_cam_script; world glue inengine-core::world::battle
The helper families
0x801CE818); the range metric, stat aggregator, XA player and loader B are in the main executable. Every scan pairs the status halfword with the liveness halfword, because a dead actor is only one whose HP reads zero.Range metric
FUN_8004E2F0 answers “is the attacker close enough” for the attack chain and for the animation tick’s root-motion gate. It returns a 16-bit distance, and zero means in range.
- The position read is asymmetric: the attacker’s live pair
+0x34 / +0x38against the target’s seat pair+0x3C / +0x40. - A monster’s size class is its record byte
+0x1F. Small classes compare against the thresholds{256, 384, 1024}atDAT_80078870; a larger class compares againstsize << 4, unsigned. - Each party member carries a reach offset at
DAT_80078878:{+43, 0, -53, -100}for roster ids 1 to 4. It is added to the distance, so a positive value is a shorter reach. - A target code the law reads as out of range is how the flee staging walks the party away: the walk’s root motion never finds its target.
The two damage-roll wrappers
Every capture-class hit goes through one of two sibling wrappers. Each draws an attacker roll and a defender roll, applies the element-affinity scale FUN_801DD864, then the closed-form finisher FUN_801DDB30.
| Wrapper | Stat mixed into both rolls | Fifth finisher argument | Party resist ladder |
|---|---|---|---|
FUN_801DD4B0 | INT-working +0x168 | 0 | runs: jewels, elemental guards, All Guard |
FUN_801DD6B4 | ATK-working +0x158 | 1 | skipped |
The fifth argument is the resist bypass: the finisher tests it once (bne a3,zero at 0x801DDBD4) and jumps past the whole party-defender block. That is the mechanism behind the boss casts that ignore elemental guards. The affinity scale still reads the caster’s element either way. Formulas: battle formulas.
The status word
One actor halfword, +0x16E, carries every condition. Its bits are pinned by the HUD icon selector’s priority ladder and by the appliers that set them.
| Bits | Condition |
|---|---|
0x0001 / 0x0002 | Venom / Toxic |
0x0004 | Stone |
0x0008 / 0x0010 / 0x0020 | Rot, one rolled limb |
0x0380 | AI delegation, always set as a group |
0x0400 / 0x0800 | Numb / Sleep |
0x1000 | Curse |
0x2000 and up | unused: no applier in the dumped corpus |
Three masks are read as units: 0xF84 gates a slot out of the selectable scans, 0x0F80 is what taking damage clears, and 0x0404 is the whole-actor inert test. There is no KO bit: a dead actor is one whose HP halfword is zero, which is why every consumer pairs the two.
The escape roll
FUN_801E791C decides a flee. Both sides score from speed plus how hurt they are, and each rolls rand() % score:
party_score = sum over party (SPD * 3) >> 1 + (maxHP - curHP) >> 4
enemy_score = sum over enemies SPD + (maxHP - curHP) >> 5
Escape Boost: roll_p += roll_p >> 1
Great Escape, a pre-emptive strike, or the forced-flee flag: roll_p = roll_e
FAIL iff not forced and (roll_p < roll_e or ctx[+0x287] != 0)
- “Assured” describes the compare, not the outcome. Great Escape forces a tie so the compare cannot fail, but the scripted-fight flag
ctx[+0x287]is tested afterwards and still blocks the flee. That is why the Chicken King is assured escape from non-boss fights. ctx[+0x287]is not a counter-attack byte. Battle setup derives it as(DAT_8007BD60 >> 5) & 4, so it reads0or4.- The pre-emptive latch
ctx[+0x291]is re-copied from a cell the first round clears, so a pre-emptive strike’s unfailable compare lasts one round. - On success the routine also stages the scene: the party turns its back, takes an out-of-range target and walks off; downed members are floored at 1 HP in their records.
AI picks and the turn budget
- The companion watches the wrong actor by design.
FUN_801EED1C’s character-id-4 arm reads seat 0’s HP and status and writes seat 1’s queue. Its physical leg redrawsrand() % monster_count + 3until the seat is alive. - Auto and Rage members get their queue from the auto arts-combo assembler
FUN_801F0450, which fills the same byte string a player would enter. - Monsters queue several actions out of an agility budget (
FUN_801E9FD4); a delegated or confused monster re-rolls its target throughFUN_801E7320at the turn seed.
The queue itself is on the action queue page.
Effects, summons and the per-frame walkers
| Routine | Role |
|---|---|
FUN_801DFDF8 | Public spawn into the effect pool (sprite animations) |
FUN_801D8DE8 | Raises a HUD text or chrome widget by element id |
FUN_801DEA50 | Steps the acting actor’s effect script and installs the move-power record |
FUN_801E09F8 | Counts outstanding effects; flies and lands projectiles. Its hit arm ORs +0x1DC and turns the victim toward the attacker |
FUN_801E0080 | Per-frame walker of the effect master and child pools |
FUN_801DF6B8 | The scaling damage-number popup |
FUN_8003EC70 | Loader B: pages a summon or cast module into slot B (raw index arg + 0x381) |
0x801F1ED4 / 0x801F2160 | Jump-table dispatch into the slot-B routine, by summon id or by effect class |
The pool model is on the effect VM page; what a paged module does is on cast modules.
Camera and pose
FUN_801D5854 is the pose driver: one presentation program per pose id 0..9, called with the acting slot. Three of its arms read the per-action style byte ctx[+0xD] through the same compare ladder: values 1 and 3 add half a turn of yaw, 2 and 3 tilt the pitch by 0x80 and store TR.y = 0x400. FUN_801D829C arms the eased tween every framing change goes through. The framings themselves are on battle stage and camera.
Keeping the party apart
Nothing walks a combatant home after an action. Three helpers, plus the round start’s recentre, are what keep a formation in frame and its members out of each other’s way.
- The seed recentre (
FUN_801EFE44) runs for every category but Run. It squashes a span over0x800, then boxes only an Attack’s target (or its whole side for the group codes) and subtracts the box’s centre from every included actor, so a melee opens with its target on the stage origin. The round start’sFUN_801DB318does the same over the whole formation. - Empty party seats count. Retail keeps a struct behind every pool slot, so in a solo fight the two empty party seats move with the formation and weigh in every later box. The port keeps their ground positions for exactly that reason.
- The make-way step: state
0x14gives each bystander a heading a quarter turn off the attack line, on whichever side it already stands, and seeds a countdown from the melee monster’s size class. The action SM’s shared tail then steps those actors each pass until the countdown drains. Without it, an approach runs straight through a partner standing in the lane. - The separation pass (
FUN_80050BB8, driven byFUN_80051078after the action SM) reads each pair’s radii - the seat stager writes0x280on a party seat,0x400on Terra’s, a monster’s size class<< 5- and nudges both live positions apart when their body pairs sit closer than a sixth of the summed radii. Neither routine reads the frame step, so the port runs it once a battle frame on either cadence.
Ports: battle_action::seed_formation_recentre, arm_make_way and make_way_step in engine-battle-vm, and battle_separation::push_apart driven by World::tick_battle_separation.
Audio and voice
_DAT_8007B910is the live audio level, not screen brightness. The summon and capture arms ramp it down and back; two of those duck arms sit behind the scripted-fight flag and never run in an ordinary encounter.- Battle voices are CD-XA clips played through
FUN_8003D53C: a grunt per normal move, a shout per Tactical Art chosen byFUN_8004C140, and a fanfare per Hyper Art. FUN_801F3990fires the item and Spirit voice cue from the wait state0x3D.
The overlay also carries its own small PRNG (FUN_801D0290). It is a shape hash for the lightning ribbon emitter and feeds no gameplay roll - those draw the BIOS rand.
See also
Every helper with its full read / write set, the leaf scans, the audio cue tables and the dump-alias list for the 0x801Fxxxx band are in docs/subsystems/battle-action-helpers.md.