Battle action state machine
The layer between “the player picked Attack” and “the actor's body has finished swinging the sword and HP has been deducted.” A battle action takes many frames to play out - windup, strike, damage, recovery - so the game tracks it as a two-level finite state machine: each frame it checks what the current action is waiting on and, when the condition is met, advances to the next state. It lives in the battle overlay (0898 - an overlay is a chunk of code the game loads into RAM on demand, this one at 0x801C0000+ during fights); the driver is FUN_801E295C (Ghidra's name for the traced function at that address; ~16 KB / 4099 instructions, the largest function in the battle overlay).
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 (Attack0x14–0x20, Magic/Item0x28–0x2E, Summon0x32–0x38, Spirit0x3C–0x40+0x46–0x48, Done0x50–0x52/0x5A, Run/Capture0x64–0x6B, Magic-capture0x6E–0x71, terminal0xFD/0xFF). The dispatch is a 256-entryjrjump table at0x801CED44with nodefault; 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 (state0x67is the one written-but-inert value, set by case0x66). - Per-actor sub-state -
actor[+0x1DC](flag bits) plus several scratch fields (+0x1DAqueued anim,+0x1D9current anim,+0x1DF..+0x1F2action-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::RunEscape → BattleEndCause::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 8 → PROT 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 0x2B → 938, Mystic Shield 0x2D → 940, Guilty Cross 0x31 → 944, evolved-form Final Crisis / Ultra Charge 0x42/0x43 → 961/962, and Cort's Evil Seru Magic 0x47 → 966 - 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
+ 0x381raw-vs-extraction off-by-2; the loader constant is raw-TOC space and the extraction entry isparam + 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
0x5800file / no move VM in the stager" readings; under the correct slot-B base0x801F69D8every part-record pointer resolves in-file, and thejal 0x80023070(move VM) lives in the SCUS stagerFUN_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, or0x4000. - 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 poolDAT_8007062c) - either directly, or through the thin pool wrapperFUN_80050ED4(stores the spawned actor pointer in the 0x60-slot pool atDAT_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 meshDAT_8007C018[model_sel + gp[0x754]](actor[+0x5A] = 1), any negative value (-1canonical) = 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 = ado { FUN_80021B04(...) } while(< 8)loop spawning 8 flame parts, each withrand()-seeded actor paramsactor[+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
0x801F69D8eachFUN_80021B04record pointer resolves to PROT 905 file0x180C..0x1E00-[i16 model_sel][u16 flags][move-VM bytecode @+4], recovered bylegaia_asset::summon_overlay(disc-gated). Thejal 0x80023070(move VM) lives in the SCUS stagerFUN_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 drawFUN_80048A08= 35-64×/frame → TRS-keyframe decoderFUN_8004998C→ cluster-AFUN_80043390. The player summon is drawn as an ordinary battle actor; the faithful path isengine-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-tailFUN_80021DF4over 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+0x8flag word of pool slots 0..=6 with0x7CFFFFFF(the state-0x5Aper-actor anim-flag clear).FUN_801DB318- formation span-normalise + recentre: when the included slots' X/Z extent exceeds0x800it 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 intoctx[+0x244], takes the acting actor's+0x1DDcurrent 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+0x14Cliveness halfword, else7.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 at0x8006F4C8, and adds the per-octant base (0x000=-Z,0x400=+X). Ported asbearing_12bitwith 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_bytereturnsNonefor unmapped values (so the dispatcher surfaces them asStepOutcome::UnknownStatefor engine logging).ActionCategory- symbolic enum for the action-category byte atactor[+0x1DE].BattleActor- the per-actor fields the state machine reads or writes. Field names mirror the+0xNNNbyte 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+0x6D8countdown timer, etc.BattleActionHost- engine callbacks for every cited helper (FUN_801D5854→pose,FUN_801D8DE8→ui_element,FUN_8004E2F0→range_check,FUN_801DABA4→recompute_battle_order,FUN_801EFE44→camera_bounds,FUN_801EED1C/FUN_801E7320→party_setup/monster_setup,func_0x80056798→rng, ...). All methods have default impls so a minimal host compiles.step(host, ctx) -> StepOutcome- runs one frame's worth of dispatch; returnsStay(still waiting on a precondition),Transition { from, to },BattleComplete(terminal), orUnknownState { 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).ActionValidatorHostabstracts the per-slot(hp, hp_max, mp, mp_max)quad, status words, record stats, and flag banks;validity_bitsmodels the retail per-slot validity byte atgp + 0x9A8with the per-arm clear/set/overwrite discipline intact. Its arm-0x82leaf calleeFUN_80046898(inventory count below the 224-slot cap) is ported alongside asitem_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:
- Miracle Art match. If the input command sequence equals the character's Miracle Art command string (
R D L U L U R D Lfor Vahn's Craze, etc.), the entire queue is replaced with the Miracle Art's replacement string (L/R/D/U× 4 →SpecialStarter→art1, art2, …). The first 4 directional bytes carry the on-disc MSB-set quirk and are masked to0x0C..=0x0F. - Super Art find/replace at tail. For each chained art the runtime walks all the character's Super Art
findpatterns and replaces the matched tail with areplacetail ending in the Super Art's finisher action constant. Triggers require: the last art offindis 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_chainrecognises 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_rowsresolves the row's per-strike profile from the Miracle's finisher-replacement queue viaresolve_action_queue: each art constant contributes its stagedArtRecordpower, or one tier-0 (x12) synthetic strike when that art's record isn't loaded. The nativeplay-windowHUD 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_sequencetokenizes a saved chain's flat directional string into its ordered named arts (each identified by its ownArtRecord::commands, greedy longest-match), andSuperMatcher::trigger_by_art_sequencetail-matches that ordering against each Super'sSuperArt::art_sequence()- thefindpattern projected to its art constants only ([0x27, 0x1F, 0x27]for Tri-Somersault), with the0x19starters and the interleaved connector directions stripped.battle_arts::super_for_chain/World::build_battle_arts_rowsflag the row (ArtRow::super_art = Some(name)) and resolve theSuperArt::replace-queue strike profile through the same helper the Miracle path uses; theplay-windowHUD 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's0x27is followed by0Fin Tri-Somersault but0Ein 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/replacestrings are capture-validated, read out of live battle RAM:0x801F64F4/0x801F6504/0x801F6514hold the three Miracle-Art replacement strings,0x801F6524the 15 Superfindentries (fixed 13-byte stride,super_art.rstable order: Vahn ×5, Noa ×5, Gala ×5),0x801F65E8the 15replacestrings (16-byte stride, zero-padded) - every resident string byte-identical tosuper_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 atactor[+0x1DF..+0x1F2](notctx[+0x274], which a capture showed is the turn-order active-actor index written byrecompute_battle_order), a Noa Miracle Art capture matches the engine's modeled replacement string byte-exact, and a Vahn Tri-Somersault capture's queue tail19 27 0F 19 1F 0E 1A 2B 2B 2Bis byte-identical tosuper_art.rs'sTri-Somersaultreplace(validating that combo's connectors0x27 → 0F,0x1F → 0Eand the finisher tail; dequeue at pc0x801D89D8). Direction/connector bytes encode as0x0C/0x0D/0x0E/0x0F= Left/Right/Down/Up and0x1A=SpecialStarter. The byte-exact matcher (SuperMatcher::try_trigger_at_tail) is also ported and exercised byresolve_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_scus → SfxBank::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.