Move-power / parameter table Confirmed
When an enemy uses a named special attack - Fire Breath, Tail Fire - how hard it hits and how
it looks on screen come from this table: one 26-byte record per battle move carrying the damage power, homing
motion, hit reaction, sound cue, and the effect lists that spawn the visuals. It lives in the battle-action
overlay (a chunk of code and data the game loads into RAM for battles, shipped as PROT entry 0898 - a
numbered entry of PROT.DAT, the disc's single big archive), not in the main executable. Two things make it
interesting: it covers enemy special attacks only (a party member's Tactical Art takes its power from
the art record instead, and basic attacks use the generic physical path), and one
record survives for a move no enemy can use - the dummied-out "Freeze Thunder" spell, whose power record is
intact but whose effect lists are empty. Parser legaia_asset::move_power; CLI asset
move-power.
Location - static overlay data
The table is loaded with the battle-action overlay image (PROT entry 0898), not built per battle: the 0x801F4F5C..0x801F69D8 window is byte-identical across two unrelated battle save states, and the raw PROT 0898 bytes byte-match the in-RAM table. (The "runtime VA" is the fixed RAM address the overlay occupies once loaded; because the data is static, the same bytes sit at a fixed file offset inside the PROT entry, which is how the parser reads it straight off the disc.)
| Thing | Value |
|---|---|
| PROT entry | 0898 (battle-action overlay) |
| Table runtime VA | 0x801F4F5C |
| Table file offset (in entry) | 0x26744 |
| Record stride | 26 bytes (the 26 * index math in FUN_801dd0ac) |
| id→index map VA | 0x801F4E63 (0x80 bytes before the table) |
| id→index map length | 0x80 (move ids 0x00..=0x7F) |
Indexing - power_table[map[move_id]]
(FUN_80xxxxxx names throughout are Ghidra-traced functions, labelled by their RAM address.) The record index is not the battle move id directly. The setup site reads the actor's move id at actor[+0x1df], looks it up in the 128-byte id→index map, and indexes the table with the result. A map byte of 0x00 or 0xFF means "no record". The move id is the same id space as the SCUS spell-name table (spell table, also indexed by actor[+0x1df]), so joining the two labels every record:
- records
0x10..=0x2b(move ids0x25..=0x74) are the named monster special attacks (Fire Breath0x25, Tail Fire0x27, …) - their special-attack power, separate from the name the spell table carries. - records
0x01..=0x0f(move ids0x04..=0x1f) are the spell table's unnamed internal enemy-attack tiers.
Record 0 is an all-zero unused slot.
Special-attack-only. The map covers 44 special-attack ids (internal tiers 0x04..=0x07/0x12..=0x1F + named monster attacks 0x25..=0x74); the basic-attack / Tactical-Art bands 0x08..=0x11 and 0x16..=0x18 are unmapped. From a live capture, a party member's queued Art (Vahn's Somersault) carries move id 0x0F and an enemy basic attack (Gobu Gobu) 0x09 - both resolve to record 0 (zero power). So a party member's Tactical Art takes its power from the art-record power byte (art data), enemy special attacks roll through this table, and basic attacks use the generic physical path.
Cross-checked against the enemy table. Of the 29 mapped named ids (≥0x25), 28 are exactly the special attacks enemies cast (monster record +0x21..=+0x23) - the records line up with the roster. But the table is a subset: across 186 monsters, 46 distinct attack ids appear and only 28 are here; the other 18 (incl. 0xA7/0xB8, beyond the 0x00..=0x7F map) have no record - the magic / elemental casts, whose damage is caster-state-derived, consistent with this being the physical/arts table. 95 of 186 monsters carry no magic attack at all (basic physical only). The one mapped record with no caster (move id 0x2C) is the unused “Freeze Thunder” enemy spell - a dummied-out attack (community testing: forcing it crashes with an “Opcode 14 UNK” missing-asset error); its power record (37) survived but its effect lists are empty.
Record layout (26 bytes)
Three battle-action functions consume the record. The damage kernels FUN_801dd0ac / 801f3990 read only +0x00 (at full / half / quarter scale). The setup FUN_801dea50 computes the record address once and stashes the pointer at ctx+0x1014; the per-frame tick FUN_801e09f8 dereferences that held pointer ~25× and reads the residual fields off it - the byte offsets it loads are exactly +0x02,+0x06,+0x08,+0x09,+0x0a,+0x0b,+0x0d,+0x0e,+0x12,+0x16, and never +0x0c.
| Off | Type | Field | Meaning | Confidence |
|---|---|---|---|---|
+0x00 | i16 | power | Damage roll modulus, used >>0/1/2 at full/half/quarter. | Confirmed |
+0x02 | i16 | strike Y offset | Subtracted from the per-arm Y lane when the hit point is seeded from the target. | Inferred |
+0x04 | u16 | move counter | Whole-move timing counter (→ ctx+0x6c6), decremented per frame. | Confirmed |
+0x06 | u16 | phase duration | Per-arm phase duration (→ ctx + arm*2 + 0x6c6) at the strike / re-arm transitions. | Inferred |
+0x08 | u8 | homing speed | Scales the per-frame XY step toward the target; 0x40 - x reseeds the approach counter. | Inferred |
+0x09 | u8 | tracking flag | When set, the live XY is copied into the spawned effect each frame. | Confirmed (read) |
+0x0a | u8 | impact effect | Enum 1..5; stored at actor+0x21f, indexes the 5-entry packed-config table at 0x801f53d4 ((v-1)*4) into actor+0x04 (packed u32 words, not pointers); 3/4/5 branch extra status-proc rolls. | Confirmed (read) |
+0x0b | u8 | trail texpage | Trail / afterimage sprite-page id (GP0 word 0x7700 + id). | Confirmed |
+0x0c | u8 | designer tag | 'C'/'E'/'G'/0 annotation on the internal-tier records only; no runtime reader. | Unknown (no reader) |
+0x0d | u8 | sound cue | Handed to the UI/voice cue dispatcher FUN_8004fcc8. | Confirmed |
+0x0e | u8 | list mode | 0xFF broadcasts the trail to all four arms; otherwise the head of a small effect-id list. | Confirmed (read) |
+0x12 | [u8;4] | on-contact effects | Effect-id list dispatched on the hit branch (0x00/0xFF-terminated; tables 0x801f6324/0x801f6418). | Confirmed |
+0x16 | [u8;4] | launch effects | Effect-id list dispatched at the initial-strike transition; same dispatch. | Confirmed |
Worked example (real disc bytes)
Record 3 (move id 0x06, the third internal-tier attack), from PROT 0898:
power 1500 ctr 0 phase 480 homing 0x20 yoff 250 impact 1 trail 0
sfx 0x4d list 0x00 tag C contact[0x27,0x8e,0x8d] launch[0x28,0x64,0x9d]
A homing physical strike: it approaches at speed 0x20, runs its strike phase for 480 frames, plays impact effect 1 + cue 0x4d, spawns one effect list on launch and a different one on contact, and carries the unused designer tag C.
Effect-id lists (+0x12 / +0x16)
Both lists are up to 4 ids walked until 0x00, dispatched identically by FUN_801e09f8 (the only difference is when they fire - on contact vs at launch). Each byte multiplexes two distinct id spaces by its bit 7:
0x01..=0x63(bit 7 clear, move-FX space) - spawn 3D effect prototype0x801f6324[id]viaFUN_80050ED4→FUN_80021B04+ play SFX0x801f6418[id](when non-zero)0x64(100) - fixed screen-flash0x80-bit set, not0xFF(bit 7 set, 2D-sprite space) - route id& 0x7FtoFUN_801dfdf0→ theefect.datpack1[id & 0x7F]billboard (FUN_801DFDF8special-cases pack10x04→0x801F5D90,0x13→0x801F5CF8; names unknown)0xFF/ unused0x65..=0x7F- no effect
The engine models this split at EffectListEntry::classify: 0x01..=0x63 as Spawn (3D scene-graph), 0x80-bit ids as AltEffect (2D pool), 0x64 as the flash.
0x801f6324 (file 0x27B0C) is a pointer table: each u32 is an overlay VA to a variable-length move-VM scene-graph record (ids 0x27/0x28 → 0x801F5BBC/0x801F5BDC). 0x801f6418 (file 0x27C00) is the per-effect SFX byte. Exactly 61 entries (bounded by the SFX table that follows). Parsed by EffectAuxTables; dispatch by EffectListEntry::classify.
For a 0x01..=0x63 entry the dispatch calls FUN_80050ed4(world_pos, src_pos, 0x801f6324[id], 0x1000), which forwards to the shared spawn stager FUN_80021B04: it reads the record's +0x00 model_sel (mesh = DAT_8007C018[model_sel + gp[0x754]]; -1/0x4000/0x4001 are transform-node sentinels), stages an actor with the record as its move-VM buffer (+0x48) and PC at +0x04 (+0x70 = 2), and drives it through the move VM (FUN_80023070). So each record is byte-identical to a summon part record (+0x00 i16 model_sel, +0x02 u16 flags, +0x04 move-VM bytecode) and reuses the same machinery as legaia_asset::summon_overlay. The high-bit (0x80) list bytes route instead to the 2D efect.dat pool (FUN_801dfdf0 → EffectCatalog). The library helper move_power::parse_effect_proto_records decodes the whole table to records (the engine's World::spawn_move_fx uses it), and all 54 unique records execute cleanly through the ported move VM - seeded as the part-stager does (PC = 2) and run under the same wait-gate / per-frame budget, with no unimplemented opcode (disc-gated move_fx_records_vm_exec_disc; ~20 distinct move-VM ops exercised). One of them, 0x801F5484 (entries 0/0x30/0x31), is the record a live enemy "Fire Tail" part-actor seats (firetail_movefx_liveness).
Effect-id → triggering-move inverse index (disc-derivable)
The join from a spawned effect back to the move(s) that trigger it is fully disc-derivable from the parsed table + id→index map - no runtime capture needed. Walk every mapped move record, read its +0x12/+0x16 effect-id lists, classify each byte with EffectListEntry::classify, and invert: each effect id gains the set of move ids that cite it. The index is keyed on (space, id) because +0x12/+0x16 multiplex two id spaces - Proto3D (0x01..=0x63 → 0x801f6324[id]) and Efect2D (bit 7 set → efect.dat pool id & 0x7F), plus the 0x64 FixedFlash singleton. The +0x0a impact-effect is a separate 1..5 config selector (0x801f53d4), not part of the effect-pool space - kept out of the index. There is no symbolic effect-name table, so id → move is the achievable join; move_power::effect_trigger_index builds it (EffectKey = Proto3D(id)/Efect2D(id)/Flash) and asset move-power --effect-index emits it directly from the disc.
Runtime resolution + engine wiring
The whole record is decoded and the auxiliary effect tables are parsed (EffectAuxTables); the 0x801f6324 spawn records are decoded too (summon-format move-VM records, above). The engine surfaces every field as a resolved descriptor (MovePowerCatalog::fx_for_move_id → MoveFx).
Summon exception
The summon branch of FUN_801dd0ac (attacker slot == 7) does not use this table - a summon's magnitude is caster/summon-state-derived (see spell table and the per-spell-power thread in open RE threads).
model_sel base semantics
The model_sel additive base gp[0x754] (global 0x8007BA6C) is party_count + 2 in battle (save-corpus-pinned: 0 when no battle effect-model library is resident, 3 for a 1-member party, 5 for the full 3-member party - it tracks party size, so there is no per-summon base), and model_sel is library-relative: a battle move-FX mesh is DAT_8007C018[model_sel + gp[0x754]] - the resident effect-model library, which the engine registers at a fixed DAT_8007C018[3..=32] (the equivalent 1-member layout).
Engine move-FX scene-graph render
The engine renders the move-FX scene-graph: World::spawn_move_fx parses a move's spawn records, stages them as a SummonScene at that fixed library base, and drives them through the ported move VM (tick_move_fx; play-window H cycles through the renderable moves enumerated by MovePowerCatalog::spawnable_move_ids() - the ids whose effect lists hold a Spawn entry with a resolved prototype) - reusing the summon machinery, so it shares the same interpreted-transform caveat (the exact per-part composition is the open FUN_801F811C/PROT-0900 piece). The effect-list AltEffect entries (high-bit 0x80 bytes, distinct from the 0x801f6324 scene-graph Spawn entries) spawn through the 2D efect.dat pool (World::try_spawn_effect / spawn_by_ui_id / FUN_801dfdf0) by their 7-bit id.
Presentation fields - trail texpage + sound cue
A spawn also surfaces the move's two presentation fields: the trail texpage (+0x0b → 0x7700 + id) via World::active_move_fx_trail_texpage(), and the sound cue (+0x0d) via World::take_pending_move_fx_cue(), which the host routes through the ported FUN_8004fcc8 dispatch decode (legaia_engine_audio::classify_cue). The 2D afterimage streak draw (FUN_801e1ab0) that consumes the trail texpage is ported as legaia_engine_render::afterimage::build_afterimage_quad - it assembles the jittered semi-transparent POLY_FT4 (per-corner rand wobble, random brightness band, UV/CLUT/texpage layout) from four projected screen corners + the trail id. The camera-coupled corner projection (FUN_800195a8) is ported too - legaia_engine_render::billboard::project_billboard (afterimage call shape afterimage::project_streak_corners: +0x120 Y push, dynamic half-width from the fx-state halfword +0x6c6 − 0x200, half-height 0x100).
Open
- The live note-on wiring of the resolved sound cue.
- The exact per-part transform composition of the spawned scene-graph (the
FUN_801F811C/PROT-0900 piece).
CLI
asset move-power <raw PROT 0898 entry> # dump every record (power, counters, effects, ...)