At a glance

Where
Mostly the battle overlay, PROT 0898 at base 0x801CE818; a few in SCUS_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..2 party, 3.. monsters
Roster ids
DAT_8007BD10[slot], 1-based: Vahn, Noa, Gala, and 4 the AI companion
Slot B
The second overlay slot at 0x801F69D8 where cast modules page in
Port
engine-battle-vm::battle_action, battle_formulas, battle_cam_script; world glue in engine-core::world::battle

The helper families

Action SM FUN_801E295C owns the order of a turn Damage + status range FUN_8004E2F0 rolls 801DD4B0 / 6B4 DoT tick FUN_801E752C escape FUN_801E791C AI + turn budget queue FUN_801EED1C auto arts FUN_801F0450 monster FUN_801E9FD4 retarget FUN_801E7320 Effects + summons spawn FUN_801DFDF8 script FUN_801DEA50 flight FUN_801E09F8 loader B FUN_8003EC70 Camera + pose pose FUN_801D5854 tween FUN_801D829C framing 0x801F0348 bounds FUN_801EFE44 Audio + voice duck _DAT_8007B910 XA FUN_8003D53C arts FUN_8004C140 cast cue FUN_801F3990 actor[+0x16E] status word read by nearly every scan actor[+0x14C] live HP zero = dead; there is no KO bit
The state machine sequences a turn; the arithmetic lives in the helpers. Most are in the battle overlay (PROT 0898, base 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 / +0x38 against 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} at DAT_80078870; a larger class compares against size << 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.

WrapperStat mixed into both rollsFifth finisher argumentParty resist ladder
FUN_801DD4B0INT-working +0x1680runs: jewels, elemental guards, All Guard
FUN_801DD6B4ATK-working +0x1581skipped

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.

BitsCondition
0x0001 / 0x0002Venom / Toxic
0x0004Stone
0x0008 / 0x0010 / 0x0020Rot, one rolled limb
0x0380AI delegation, always set as a group
0x0400 / 0x0800Numb / Sleep
0x1000Curse
0x2000 and upunused: 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 reads 0 or 4.
  • 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 redraws rand() % monster_count + 3 until 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 through FUN_801E7320 at the turn seed.

The queue itself is on the action queue page.

Effects, summons and the per-frame walkers

RoutineRole
FUN_801DFDF8Public spawn into the effect pool (sprite animations)
FUN_801D8DE8Raises a HUD text or chrome widget by element id
FUN_801DEA50Steps the acting actor’s effect script and installs the move-power record
FUN_801E09F8Counts outstanding effects; flies and lands projectiles. Its hit arm ORs +0x1DC and turns the victim toward the attacker
FUN_801E0080Per-frame walker of the effect master and child pools
FUN_801DF6B8The scaling damage-number popup
FUN_8003EC70Loader B: pages a summon or cast module into slot B (raw index arg + 0x381)
0x801F1ED4 / 0x801F2160Jump-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.

1. Seed recentre FUN_801EFE44, every action origin target the formation moves with it 2. Make-way step state 0x14 + the SM tail, melee target attacker heading h ± 0x400, about size × 8 units 3. Separation pass FUN_80050BB8, once a battle frame gap < (r1 + r2) / 6: nudge both r = 0x280 party, 0x400 Terra
Top-down stage, party at the bottom. Each sketch is one helper; the round start runs the first one’s whole-formation form as well.
  • The seed recentre (FUN_801EFE44) runs for every category but Run. It squashes a span over 0x800, 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’s FUN_801DB318 does 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 0x14 gives 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 by FUN_80051078 after the action SM) reads each pair’s radii - the seat stager writes 0x280 on a party seat, 0x400 on 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_8007B910 is 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 by FUN_8004C140, and a fanfare per Hyper Art.
  • FUN_801F3990 fires the item and Spirit voice cue from the wait state 0x3D.

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.