How it works

FUN_801E295C runs every frame from the battle main dispatcher. It picks the global battle context (_DAT_8007BD24, a pointer to the live struct at 0x800EB654), resolves the active actor (the live record for one combatant, party member or monster) via the 8-slot actor pointer table at 0x801C9370, and pumps the action through three nested keys:

  • Action category - actor[+0x1DE]: 0=Tactical Arts, 1=Item, 2=Magic, 3=Attack, 4=Spirit, 5=Run/Defend. Read at the action-start case (ctx[7] == 0xC) and used to seed the next state.
  • Execution phase - ctx[7]: the handled states are organised in contiguous bands (Attack 0x14–0x20, Magic/Item 0x28–0x2E, Summon 0x32–0x38, Spirit 0x3C–0x40 + 0x46–0x48, Done 0x50–0x52/0x5A, Run/Capture 0x64–0x6B, Magic-capture 0x6E–0x71, terminal 0xFD/0xFF). The dispatch is a 256-entry jr jump table at 0x801CED44 with no default; any byte value outside the handled set indexes a slot that falls straight to the shared post-switch epilogue - an inert no-op advance, not a crash (state 0x67 is the one written-but-inert value, set by case 0x66).
  • Per-actor sub-state - actor[+0x1DC] (flag bits) plus several scratch fields (+0x1DA queued anim, +0x1D9 current anim, +0x1DF..+0x1F2 action-parameter byte stream).

It is not a bytecode VM. There is no opcode table, no PC stride. Each case ctx[7] body waits on a per-actor condition (animation matched, timer expired, distance check passed) and writes the next ctx[7] value when ready. Actions that need multiple frames do nothing on the frames where their condition isn't met yet.

Run band (0x64–0x67): state 0x66 is the successful-escape teardown, not a failed run. The 0x65 wait branches on the run outcome: a failed run routes back to 0x50 (Done band - the action is consumed, the battle continues), a successful escape routes to 0x66, which stages a 0x40-frame black→white screen fade through the fade-primitive spawner (FUN_80024E80 + the FUN_80020B00 fade-state loader, 10.6 fixed-point ramp), sets the battle-end signal DAT_8007BD71 = 0xFE (the same byte the 0x5A wipe gate sets), and parks in the 0x67 terminal hold. An earlier reading labelled 0x66 “run failed, battle continues” - falsified by that signal byte. Engine: ActionState::RunEscapeBattleEndCause::Escaped (no loot, no defeat) + the engine_core::fade kernel.

The 0x5A monster-wipe victory arm stages the win pose off the acting actor's party slot, re-picking a living party member only when the acting actor is dead (the alive-skip at 0x801E6690). Retail is safe because the wipe scan and the scheduler share the +0x14C != 0 && !(+0x16E & 0x4) standing predicate, so an alive acting actor is always a party member - the randomizer's enemy-ally charm widens that mask to 0x384 and breaks the invariant. Full chain + the engine and disc-side fixes: battle § charm softlock.

Effect spawning is indirect: the state machine doesn't call the effect VM directly - it calls FUN_801D8DE8(ui_element_id, mode) (the hottest battle helper, ~30 sites), which fans out through the effect cluster. The state machine knows UI element IDs (0x07/0x0F/0x34/0x43/0x4C/0x52/0x66); the effect VM resolves them to actual sprite-anim spawns. That path drives the 2D UI/sprite layer (FUN_801DFDF8 emits POLY_FT4 billboard quads); the 3D summon model is a separate mechanism (below).

Seru-magic summon-overlay dispatch

The 3D visual of a player Seru-magic cast - the summoned Seru and its attack mesh, e.g. Gimard's Burning Attack flame (the player summon; the enemy boss move Fire Tail is distinct) - is not spawned by an opcode and does not live in befect_data. It is a per-summon code overlay paged in on demand. In outer state case 0x29, when the queued action's spell id actor[+0x1df] falls in the player Seru-magic block 0x81..0x8b:

_DAT_8007bd24[7] = 0x32;                                   // advance to the cast band
_DAT_8007ba2c = (&PTR_s_re_check_801f6734)[id - 0x81];     // per-summon effect-data pointer
FUN_8003ec70(id - 0x79, 0);                                // overlay loader B: PROT (id - 0x79 + 0x381)

FUN_8003EC70(param) (overlay loader B) loads raw TOC index param + 0x381 into slot B - the second overlay window at *DAT_80010390 = 0x801F69D8, sitting above the resident battle overlay (0898) in slot A. That raw index is extraction entry param + 0x37F, two below the raw value (see PROT § In-RAM TOC). So the player summons map to extraction PROT 903..913 (Gimard Burning Attack 0x81 → param 8PROT 903). The capture-class ('c') spell branch loads from a different base: FUN_8003EC70(spell_record[+1] + 0x28). The whole block is capture-pinned: every spell id 0x81..=0x8B was observed mid-cast loading its arithmetic slot, zero exceptions.

The same loader serves the neighbouring blocks. The evolved-Seru block 0x8C..=0x95 continues the run to extraction 914..923 (stager-shaped; eight of ten legs capture-pinned mid-cast - 0x8C..=0x8F/0x92..=0x95, only 0x90/0x91 still predicted). Enemy boss specials ride their own stagers through the same loader: the final-boss Cort mid-cast corpus lands every leg on the same arithmetic, byte-resident at slot B - Mystic Circle 0x2B938, Mystic Shield 0x2D940, Guilty Cross 0x31944, evolved-form Final Crisis / Ultra Charge 0x42/0x43961/962, and Cort's Evil Seru Magic 0x47966 - distinct from the player-side Juggernaut stager 0927: the player and enemy arms of the same spell ship separate stagers. PROT 0907 on the 0x85 slot is Nighto's stager; its head title "Hell's Music" is the attack's display name (the SCUS spell table carries the same string). See static overlay pipeline.

Superseded readings
  • An earlier mapping placed the summon block at "extraction 905..915 / Gimard → 905" - that was the + 0x381 raw-vs-extraction off-by-2; the loader constant is raw-TOC space and the extraction entry is param + 0x37F.
  • PROT 0907's "Hell's Music" head string was read as a Disco King dance song; refuted - the dance overlay 0980 has no slot-B loader callsite, and the string is the Nighto attack's display name.
  • A wrong link base produced "records beyond the 0x5800 file / no move VM in the stager" readings; under the correct slot-B base 0x801F69D8 every part-record pointer resolves in-file, and the jal 0x80023070 (move VM) lives in the SCUS stager FUN_80021B04.
  • A reported 0x1000/0x8000-class population of record-first-word "sentinels" was over-read contamination from untrimmed extraction files; trimmed stagers carry only -1, small mesh indices, or 0x4000.
  • The Gimard flame flicker was read as CLUT/palette animation; falsified - the CLUT band is byte-identical across two animation-distinct capture frames while the framebuffer differs ~21%, so the motion is the spawned part-actors moving.

Inside a summon stager (reference: the spell-0x83 slot, extraction PROT 905)

The deep-dive below reads one representative stager file - extraction 905, the spell-0x83 slot; every stager in the block (including Gimard's 0x81 → 903) parses identically under the same link base. The live-capture findings (flame mesh DAT_8007C018[26], part-actor motion) are capture-derived and independent of which file is dissected. Parse counts quoted for any stager must come from the entry trimmed to its TOC-gap footprint - the entry's own content is only the first (next_start_lba - start_lba) * 0x800 bytes (unique_content_len); the extraction .BINs over-read into the neighbouring stagers and inflate the spawn-site/record census.

The summon overlay carries no embedded TMD geometry (no 0x80000002 magic). The summon's meshes are the separately-loaded DAT_8007C018 model library: extraction entry 0871 (etmd.dat), a 30-entry asset::pack of Legaia TMDs that the battle scene loader FUN_800520F0 pulls at battle init (case 0xb, raw TOC index 0x369, retail dev path h:\prot\battle\etmd.dat) and registers via FUN_80026B4C, populating DAT_8007C018[3..32] ([0..2] are the party battle meshes). Its extraction filename label says sound_data (the +2 label shift; the retail define block is befect_data); its texture sibling etim.dat = extraction 0870 (the flame atlas) loads via loader case 0x8 - see effect § battle effect cluster. The overlay spawns and animates part-actors over those meshes. Decompiled (PROT 905 imported raw at base 0x801F0000, ghidra/scripts/dump_summon_overlay.py):

  • The overlay spawns part-actors via the SCUS part-stager FUN_80021B04(world_pos, render_slots, record_ptr, 0x1000) (param_1 = world position → actor[+0x14..0x18], param_3 = a part record, allocated from the effect pool DAT_8007062c) - either directly, or through the thin pool wrapper FUN_80050ED4 (stores the spawned actor pointer in the 0x60-slot pool at DAT_801C90F0, then forwards the same arguments; the dominant call form in the high-summon and enemy boss stagers). record[+0] (model_sel) drives the spawn-time render seat: ≥ 0 → library mesh DAT_8007C018[model_sel + gp[0x754]] (actor[+0x5A] = 1), any negative value (-1 canonical) = no-mesh transform/pivot node (actor[+0x56] = 0, +0x5A = 0), 0x4000/0x4001 = special render-mode nodes (actor[+0x5A] = 3 / 5).
  • Three staging functions drive the spawn: FUN_801F16A0 (phase 0 = a do { FUN_80021B04(...) } while(< 8) loop spawning 8 flame parts, each with rand()-seeded actor params actor[+0x84] / +0xb4 = rng%15+16 / +0xb6 = rng%255+512 / +0x28; phase 1 = 1 more), FUN_801F36A0, FUN_801F4DD0. The per-frame motion is the standard actor-tick consuming those RNG-seeded fields.
  • Part records are in-file move-VM bytecode: under the slot-B link base 0x801F69D8 each FUN_80021B04 record pointer resolves to PROT 905 file 0x180C..0x1E00 - [i16 model_sel][u16 flags][move-VM bytecode @+4], recovered by legaia_asset::summon_overlay (disc-gated). The jal 0x80023070 (move VM) lives in the SCUS stager FUN_80021B04, not the overlay.
  • But the scene-graph is NOT the player render path (live trace): a player Gimard Burning Attack cast shows FUN_801F7088 = 0×, move VM = 2-3× (noise), and the battle per-actor draw FUN_80048A08 = 35-64×/frame → TRS-keyframe decoder FUN_8004998C → cluster-A FUN_80043390. The player summon is drawn as an ordinary battle actor; the faithful path is engine-vm/anim_vm.rs. The engine drives the move-VM records as a stand-in only. Scope: the enemy Gimard Fire Tail mid-cast holds PROT 0900 resident but its screen-widget family is dormant (zero live widgets - the widget path stays exclusive to the 8 ending scenes); the live effect is a single move-VM part-actor ticked by the SCUS render-tail FUN_80021DF4 over a [model_sel][flags][bytecode] record in the battle overlay (0898) data (below the 0900 link base), not a 0900 record - so Fire Tail does not drive the widget path.

The flame renders as Gouraud-textured (POLY_GT3/POLY_GT4) prims sampling the resident etim page (832,256) 4bpp; cba/tsb are applied at render time. In a live mid-cast Gimard capture (battle_gimard_tail_fire_a) the summon library occupies DAT_8007C018[3..32]; ten of those ([23..32]) are fire-textured meshes (cba row 478 0x778B baked), and the active Gimard flame is DAT_8007C018[26] - the only rendered model baking etim, with both rendering actors carrying actor[+0x64]=26 and actor[+0x56]=5 (full-TMD mode → FUN_8002735C).

Enemy boss stagers + the record-table trim

The six final-boss Cort special-attack stagers (extraction PROT 0938 Mystic Circle / 0940 Mystic Shield / 0944 Guilty Cross / 0961 Final Crisis / 0962 Ultra Charge / 0966 Evil Seru Magic) parse as summon stagers under the same 0x801F69D8 link base and record format as the player block (summon_overlay::ENEMY_BOSS_STAGER_PROT; disc-gated enemy_stager_real), spawning dominantly through the FUN_80050ED4 pool wrapper. Stager extraction entries are over-read windows: each .BIN runs past the next entry's start LBA, so only the first (next_start_lba - start_lba) * 0x800 bytes are the entry's own content (unique_content_len) - a boundary the Cort mid-cast saves pin byte-exactly against the slot-B resident image (0938 → 0x1800, 0940/0944/0961 → 0x2000, 0962 → 0x2800, 0966 → 0x4000).

The enemy-cast stager path is not Cort-specific. Mid-cast captures of ordinary bosses pin the same mechanism on the universal extraction = id + 895 arithmetic (loader-B id at 0x8007BC4C, byte-resident at slot B; disc+library-gated enemy_stager_binding): the Delilas brothers - Gi / Blazing Slash 0x3F → 0958, Che / Megaton Press 0x40 → 0959, Lu / Plasma Strike 0x41 → 0960 - and Zeto, whose Call Wave and Big Wave are one logical attack over two turns and so share a single stager (0x33 → 0946). None of these four carries a 0x4000 render-mode record, and at the captured instants the part pool DAT_801C90F0 is empty, so the render-mode draw still has no live exerciser.

That trim resolves the record-first-word "sentinel" question: across every trimmed stager (player 0903..0913, the evolved-Seru block 0914..0923, high 0927..0934, the six Cort entries) the first word is only ever -1 (transform node, dominant), a small library-mesh index, or 0x4000 - matching FUN_80021B04's own dispatch. The 0x4000 render-mode records live in five stagers: the Sim-Seru trio Palma 0928 (4) / Mule 0929 / Jedo 0931, plus two evolved-Seru casts - 0x8E → 0916 (4, Aluru) and 0x93 → 0921 (6, Iota). All five are player casts (both evolved carriers are now capture-pinned mid-cast), so none seats a live render-mode part. Live correlation: every live pooled part-actor in the Cort states carries actor[+0x48] pointing at a -1 record inside the trimmed table, with the spawn-time +0x56/+0x5A zeros rebound post-spawn by the move-VM ops (+0x56 = 4 / +0x5A = 2 dominate mid-cast). No 0x4000/0x4001 part-actor was live in these captures, so those render modes' draw behaviour stays open.

Actor-pool leaf helpers

Small self-contained routines the SM and its round driver call over the 8-slot battle-actor pool (&DAT_801C9370) and the ctx target queue - each ported as a pure function in engine-vm::battle_action (pool_ops), transcribed from the disassembly dumps:

  • FUN_801DB9C4 - end-of-action flag scrub: AND-masks the +0x8 flag word of pool slots 0..=6 with 0x7CFFFFFF (the state-0x5A per-actor anim-flag clear).
  • FUN_801DB318 - formation span-normalise + recentre: when the included slots' X/Z extent exceeds 0x800 it rescales every included coordinate (and the matching camera-focus accumulator), then subtracts the centroid from every included slot.
  • FUN_801D8A88 - attack target-queue builder: counts live monsters into ctx[+0x244], takes the acting actor's +0x1DD current target as the wrap slot +0x245, and appends the three nearest alive non-target monster slots to +0x246.. ordered by bearing offset from the current-target direction.
  • FUN_801D8D00 - target-cycle accessor: locates the current target inside that ring and steps next / previous with wrap.
  • FUN_801DB8B4 - first live monster slot: scans slots 3..6 for a non-zero +0x14C liveness halfword, else 7.
  • FUN_801DBA04 / FUN_801DB81C - selectable-participant scans: same three predicates (action-state byte != 4, alive, no can't-select ailment +0x16E & 0xF84); one starts at slot 0 (first selectable target), the other after the current actor (next participant).
  • FUN_80019B28 - 12-bit bearing (atan2): folds the displacement into a quadrant by sign, divides the shorter leg into the longer, indexes the retail arctan LUT at 0x8006F4C8, and adds the per-octant base (0x000 = -Z, 0x400 = +X). Ported as bearing_12bit with the LUT caller-supplied - Sony data, no table bytes embedded.
  • FUN_801DB124 - dead-target redirect roll: when a queued action's chosen target is dead and the category qualifies (Attack always; Magic and Item conditionally), re-rolls a living slot on the same side until alive.

Engine port

crates/engine-vm/src/battle_action.rs ports the state graph as a per-frame edge-triggered state machine. Surface:

  • ActionState - symbolic enum for every named state byte; from_byte returns None for unmapped values (so the dispatcher surfaces them as StepOutcome::UnknownState for engine logging).
  • ActionCategory - symbolic enum for the action-category byte at actor[+0x1DE].
  • BattleActor - the per-actor fields the state machine reads or writes. Field names mirror the +0xNNN byte offsets so the link to the decompile stays explicit.
  • BattleActionCtx - the subset of the live ctx struct (_DAT_8007BD24-pointed) the state machine touches: action_state, active_actor, the +0x6D8 countdown timer, etc.
  • BattleActionHost - engine callbacks for every cited helper (FUN_801D5854pose, FUN_801D8DE8ui_element, FUN_8004E2F0range_check, FUN_801DABA4recompute_battle_order, FUN_801EFE44camera_bounds, FUN_801EED1C / FUN_801E7320party_setup / monster_setup, func_0x80056798rng, ...). All methods have default impls so a minimal host compiles.
  • step(host, ctx) -> StepOutcome - runs one frame's worth of dispatch; returns Stay (still waiting on a precondition), Transition { from, to }, BattleComplete (terminal), or UnknownState { state }.
  • validate_action(host, arm, sub_case, slot, validity_bits) -> bool - the 18-arm action validator (FUN_8003FB10, SCUS-resident): the gate the menu / battle UI runs against a candidate slot before committing an action (heal/revive/MP targets, status presence, stat-below-cap checks, out-of-battle flag gates). ActionValidatorHost abstracts the per-slot (hp, hp_max, mp, mp_max) quad, status words, record stats, and flag banks; validity_bits models the retail per-slot validity byte at gp + 0x9A8 with the per-arm clear/set/overwrite discipline intact. Its arm-0x82 leaf callee FUN_80046898 (inventory count below the 224-slot cap) is ported alongside as item_count_gate.

crates/engine-core/src/world.rs composes this with the actor VM, move VM, and effect VM into a single World struct that engines drive via World::tick.

Action queue and Tactical Arts trigger ordering

Before FUN_801E295C reaches the inner-state machinery, the battle code resolves the player's command-input sequence into a flat action queue of ActionConstant bytes. The queue is built incrementally from directional inputs and accumulated arts; once the player commits, the runtime applies two trigger passes in order:

  1. Miracle Art match. If the input command sequence equals the character's Miracle Art command string (R D L U L U R D L for Vahn's Craze, etc.), the entire queue is replaced with the Miracle Art's replacement string (L/R/D/U × 4 → SpecialStarterart1, art2, …). The first 4 directional bytes carry the on-disc MSB-set quirk and are masked to 0x0C..=0x0F.
  2. Super Art find/replace at tail. For each chained art the runtime walks all the character's Super Art find patterns and replaces the matched tail with a replace tail ending in the Super Art's finisher action constant. Triggers require: the last art of find is the last action in the queue, and all participating arts paid AP.

Both passes are clean-room ports in legaia_art::MiracleMatcher / legaia_art::SuperMatcher. The engine-vm BattleActionHost exposes an art_record(char_id, art_id) callback so the SM can fetch the art record for power-byte resolution, hit timing, and status-effect application during the 0x14..0x20 Attack chain.

The reusable helper is legaia_engine_vm::battle_action::resolve_action_queue(character, command_input, chained_arts) - runs Miracle then Super to fixpoint. Returns an ActionQueue ready to feed into ctx.queued_action.

Retail runs both passes inside the queue-builder FUN_801EED1C (battle overlay, called from ActionSeed state 0x0C at 0x801E2C7C): it normalizes the saved arrow chain seeded at actor[+0x1DF..] by FUN_801DA34C (char record +0x76F/+0x77F) into art constants (row index + 0x18 at 0x801EF6F0), validates each art against the learned list (FUN_801EFBFC), pays AP (+0x170), copies the Miracle replacement from 0x801F64F4 + (char-1)*0x10 inline (0x801EF4E8..0x801EF524), then calls the Super applier FUN_801EF9E4 - find at 0x801F6524 (13-byte stride) matched against the queue tail, replace from 0x801F65E8 (16-byte stride, write at 0x801EFB7C).

Miracle / Super in the live player-driven Arts submenu

The player-driven battle Arts submenu (legaia_engine_core::battle_arts) models an art as a saved directional chain (SavedChainRecord, raw command bytes), not an in-gauge buffered input. The two trigger paths interact with that model differently:

  • Miracle Arts are wired. A Miracle Art's trigger is an exact directional-string match, so battle_arts::miracle_for_chain recognises a saved chain whose command string equals the caster's Miracle Art and flags the menu row (ArtRow::miracle = Some(name)). World::build_battle_arts_rows resolves the row's per-strike profile from the Miracle's finisher-replacement queue via resolve_action_queue: each art constant contributes its staged ArtRecord power, or one tier-0 (x12) synthetic strike when that art's record isn't loaded. The native play-window HUD shows the Miracle name on the row.
  • Super Arts are wired, with the queue connectors abstracted. A Super fires when the player chains several named arts ending on a known combination. legaia_art::recognize_art_sequence tokenizes a saved chain's flat directional string into its ordered named arts (each identified by its own ArtRecord::commands, greedy longest-match), and SuperMatcher::trigger_by_art_sequence tail-matches that ordering against each Super's SuperArt::art_sequence() - the find pattern projected to its art constants only ([0x27, 0x1F, 0x27] for Tri-Somersault), with the 0x19 starters and the interleaved connector directions stripped. battle_arts::super_for_chain / World::build_battle_arts_rows flag the row (ArtRow::super_art = Some(name)) and resolve the SuperArt::replace-queue strike profile through the same helper the Miracle path uses; the play-window HUD shows the Super name. Super is checked after Miracle (retail order). The match is connector-abstracted: the connector direction after each art is combo-specific (Vahn's 0x27 is followed by 0F in Tri-Somersault but 0E in Power Slash), so it can't be derived from each art's commands - the connectors are per-combo data in the battle overlay's resident trigger table, and the live menu matches on the named-art ordering because a saved chain carries no connector bytes, not because the byte-exact strings are unknown. All 15 Supers' find/replace strings are capture-validated, read out of live battle RAM: 0x801F64F4/0x801F6504/0x801F6514 hold the three Miracle-Art replacement strings, 0x801F6524 the 15 Super find entries (fixed 13-byte stride, super_art.rs table order: Vahn ×5, Noa ×5, Gala ×5), 0x801F65E8 the 15 replace strings (16-byte stride, zero-padded) - every resident string byte-identical to super_art.rs's modeled fields, and every replace preserving its find minus the final [19, art] pair then appending [1A, finisher…]. The runtime effect is capture-validated too: the queue is the per-actor action-parameter byte stream at actor[+0x1DF..+0x1F2] (not ctx[+0x274], which a capture showed is the turn-order active-actor index written by recompute_battle_order), a Noa Miracle Art capture matches the engine's modeled replacement string byte-exact, and a Vahn Tri-Somersault capture's queue tail 19 27 0F 19 1F 0E 1A 2B 2B 2B is byte-identical to super_art.rs's Tri-Somersault replace (validating that combo's connectors 0x27 → 0F, 0x1F → 0E and the finisher tail; dequeue at pc 0x801D89D8). Direction/connector bytes encode as 0x0C/0x0D/0x0E/0x0F = Left/Right/Down/Up and 0x1A = SpecialStarter. The byte-exact matcher (SuperMatcher::try_trigger_at_tail) is also ported and exercised by resolve_action_queue's tail pass.

When the active actor's chosen_art is set and art_record returns a record, attack_chain (state 0x1A) calls a second host hook apply_art_strike(ArtStrikeInfo) alongside the existing apply_damage. ArtStrikeInfo carries the strike-indexed power byte, dmg_timing, hit cue, and the art's flat status effect. Engines drive HP deduction, status application, sound-effect scheduling, and visual hit-cue dispatch off this struct.

The engine-side translator at crates/engine-core/src/art_strike.rs (apply_art_strike(attack, defense, info) -> ArtStrikeOutcome) folds an ArtStrikeInfo into a concrete HP delta + status flag + scheduled SFX cues using art_strike_damage in legaia_engine_vm::battle_formulas. The world's BattleActionHost::apply_art_strike impl resolves per-slot weapon attack from World::battle_attack and the right defense (UDF or LDF, picked from World::battle_defense_split) before calling the translator, then emits a BattleEvent::ApplyArtStrike with the resolved outcome. Engines apply each strike's damage / enemy_effect / cues through their runtime path for HP / status / SFX dispatch.

World::fold_battle_event folds the ApplyArtStrike outcome: HP / status into the target, and the outcome's sound cues (cue.is_sound(), the HitCue::kind SfxBank ids - distinct from the move-power +0x0d FUN_8004fcc8 namespace) into a per-frame BattleSfxCue queue the host drains via World::drain_battle_sfx_cues (the audio sibling of drain_battle_hit_fx). The host plays each through SfxBank::play_one_shot at the cue's timing_frames delay. The live battle loop wires this end to end: the SFX bank is decoded from the user's executable at boot (SfxTable::from_scusSfxBank::from_descriptors) and the cues key on through the per-scene VAB, driven once per simulation tick by the BGM director's SFX scheduler.

Spirit / Run in the live command menu

The live player-driven command menu (legaia_engine_core::battle_input::BattleCommand) carries all six commands: Attack (target cursor + physical strike), Arts / Magic / Item (host-submenu hand-offs), Spirit and Run. Spirit resolves without a target: the live loop charges the caster's AP gauge (ApGauge::charge_spirit, the retail Square-press +5) and raises a per-slot guard stance - the engine model of the retail pending-action byte +0x1DE == 4 that the damage finisher's guard-halve stage reads (DamageFinish::defender_guarding, over >>= 1) - held until that actor's next turn starts. Run arms the ported run band (Begin → category 5 → RunBegin/RunWait/RunEscape) with the roll outcome staged on multi_cast_gate; a success tears the battle down Escaped (no loot, downed members floored alive at 1 HP), a failure consumes the turn through the Done band. The escape probability is the retail roll FUN_801E791C - the writer of the _DAT_8007726C outcome pointer states 0x64/0x65 test, decoded from its overlay dump: party score = per-slot (SPD*3)>>1 + missingHP>>4 (downed members included), enemy score = SPD + missingHP>>5, two BIOS-rand draws modulo the scores, caught iff roll_p < roll_e (strict) or the scripted no-escape flag ctx+0x287 is set. The two Chicken accessory bits fold from the living party members' second ability word: Escape Boost (passive 0x34, Chicken Heart) scales the party roll x1.5, Great Escape (0x37, Chicken King) forces the compare to a tie - assured escape, but still blocked by ctx+0x287, which is why it's "non-boss". Ported as battle_formulas::escape_roll, rolled live by World::roll_battle_escape.

Full reference

The full per-state table (every ctx[7] case body, what it runs, and which state it transitions to next) lives at docs/subsystems/battle-action.md in the repo. That doc cites ghidra/scripts/funcs/overlay_battle_action_801e295c.txt at the line level.

See also