Move-power / parameter table Confirmed
When an enemy uses a named special attack - Fire Breath, Tail Fire - how hard it hits, how it homes in on its target, what sound plays and which effects burst on contact all come from one 26-byte record in this table. It is the enemy special-attack table: a party member's Tactical Art takes its power from the art record instead, and plain physical attacks never look here. One record survives for an attack no enemy can cast - the dummied-out “Freeze Thunder”.
At a glance
- In the game
- Every named monster special attack's damage roll, approach motion, hit reaction, sound cue and effect bursts
- Lives in
- Battle-action overlay, PROT entry 0898: table at runtime VA
0x801F4F5C(file0x26744); id→index map at0x801F4E63(file0x2664B) - Size / stride
- 44 records × 26 bytes; map is 128 bytes (move ids
0x00..=0x7F) - Indexed by
table[map[move_id]]- the move id is the same id space as the spell-name table- Parser
legaia_asset::move_power; CLIasset move-power; engineengine-core::move_power::MovePowerCatalog- Confidence
- Confirmed - the raw PROT bytes byte-match the in-RAM table across unrelated battle states, and every field read is pinned in the instruction stream (how we know)
- Used by
- Battle formulas, Battle action SM, Move VM, Randomizer (move-power slider)
Record layout (26 bytes)
+00.The damage kernel reads only +0x00. The action setup computes the record address once and caches it in the battle context; the per-frame tick reads the rest off that pointer. No consumer ever touches +0x0c.
| Offset | Size | Field | Meaning | Confidence |
|---|---|---|---|---|
+0x00 | 2 (i16) | power | Damage magnitude. A signed halfword - a byte-wide reading truncates most of the table. Four derived shifts feed the roll (details). | Confirmed |
+0x02 | 2 (u16) | strike Y offset | Subtracted from the per-arm Y lane when the hit point is seeded from the target. | Inferred (read confirmed) |
+0x04 | 2 (u16) | move counter | Whole-move timing counter, decremented each frame. | Confirmed |
+0x06 | 2 (u16) | phase duration | Per-arm phase length written at the strike / re-arm transitions. | Inferred (read confirmed) |
+0x08 | 1 | homing speed | Scales the per-frame XY step toward the target; 0x40 - speed reseeds the approach counter. | Inferred (read confirmed) |
+0x09 | 1 | tracking flag | Non-zero: the live XY is copied into the spawned effect each frame, so the effect follows the strike. | Confirmed (read); semantic Inferred |
+0x0a | 1 | impact-effect selector | Enum 1..5 (0 = none) into a 5-entry packed-config table; 3/4/5 branch to extra status-proc rolls. | Confirmed (read); naming Inferred |
+0x0b | 1 | trail texpage | Afterimage sprite-page id; becomes the GP0 texpage word 0x7700 + id. | Confirmed |
+0x0c | 1 | designer tag | 'C'/'E'/'G'/0 annotation on the internal-tier records only; no runtime reader (the sweep). | Unknown (no reader) |
+0x0d | 1 | sound cue id | Handed to the cue dispatcher, which queues the SFX ring. | Confirmed |
+0x0e | 1 | effect-list head | 0xFF broadcasts the trail to all four arms; otherwise the head of the effect-id list (two consumers frame it differently). | Confirmed (read) |
+0x12 | 4 | on-contact effects | Effect-id list dispatched on the hit branch, terminator-ended. | Confirmed |
+0x16 | 4 | launch effects | Effect-id list dispatched at the initial strike; same dispatch as +0x12. | Confirmed |
Worked example. Record 3 (move id 0x06, the third internal-tier attack) decodes as a homing physical strike: power 1500, approach speed 0x20, a 480-frame strike phase, Y offset 250, impact effect 1, cue 0x4d, designer tag C, one effect list on launch and a different one on contact.
What indexes it - table[map[move_id]]
The record index is not the move id. Setup reads the actor's move id (actor[+0x1df]), looks it up in the 128-byte map, and indexes the table with the result; a map byte of 0x00 or 0xFF means “no record”. Joining the map with the spell table labels every record:
- records
0x10..=0x2b(move ids0x25..=0x74) - the named monster special attacks; - records
0x01..=0x0f(move ids0x04..=0x1f) - the spell table's unnamed internal enemy-attack tiers (escalating-power triplets); - record 0 - an all-zero unused slot.
Special-attack-only
The map covers exactly 44 ids. The basic-attack and Tactical-Art bands (0x08..=0x11, 0x16..=0x18) are unmapped - a queued Somersault or a Gobu Gobu basic attack both resolve to record 0. Damage sources split cleanly:
| Attack | Power source |
|---|---|
| Enemy special attack | This table, +0x00 |
| Party Tactical Art | Art-record power byte at record0 +0x24 (art data) |
| Basic physical attack | Generic physical path (battle formulas) |
| Seru summon | Caster / summon state - the kernel's summon arm never reads the table |
Of the 29 mapped named ids, 28 are attacks enemies cast. The table is a subset of the enemy roster's attack ids: the 18 ids missing from it are the magic / elemental casts, whose damage is caster-derived. The one mapped record with no caster (move id 0x2C) is the unused Freeze Thunder: power 37, cue 0x4A, empty effect lists; forcing it crashes on a missing asset (roster numbers).
Effect-id lists (+0x12 / +0x16)
Both lists hold up to four ids and are dispatched identically - the only difference is when they fire (contact vs launch). Each byte multiplexes two id spaces by bit 7:
| Entry | Meaning |
|---|---|
0x00 | Terminator |
0x01..=0x63 | Move-FX space: spawn the 3D prototype record the effect-prototype table points at, and copy the effect's CLUT row from the sibling table when non-zero |
0x64 (100) | Fixed screen flash, no table lookup |
bit 7 set, != 0xFF | 2D-sprite space: id & 0x7F into the efect.dat pack1 billboard pool |
0xFF, unused 0x65..=0x7F | No effect, scan continues |
Both lists index the same two side tables, which sit after the power table in the same overlay (parser move_power::EffectAuxTables):
| VA | File | Table | Contents |
|---|---|---|---|
0x801f6324 | 0x27B0C | effect-prototype pointers | 61 × u32 overlay VAs, each a variable-length move-VM scene-graph record; packed, not a fixed stride |
0x801f6418 | 0x27C00 | per-effect CLUT source x | 61 × u8, 0 = no palette copy. Not a sound id |
0x801F6470 | 0x27C58 | cue-group table | 13 records × 5 bytes, indexed by group id (details) |
A 0x01..=0x63 byte spawns a record that is byte-identical to a summon part record (i16 model_sel, u16 reserved, then move-VM bytecode) through the same stager and move VM the summon renderer uses (spawn path). The join from effect back to triggering move is disc-derivable: asset move-power --effect-index emits it.
Engine wiring
MovePowerCatalogpairs the table with the map onWorld::tables.move_powerand resolves a move id to a fullMoveFxdescriptor: every behavioural field, the impact-config join, each list byte classified asSpawn(3D),AltEffect(2D pool) orFlash.- The monster special-attack damage path rolls
+0x00through the arts/physical kernel (battle formulas). World::spawn_move_fxstages a move's spawn records as aSummonSceneat the fixed library base and drives each part through the ported move VM;play-windowHcycles the spawnable moves. The shared cast path requests the spawn for any non-summon move with a spawnable entry.- The trail texpage (
+0x0b) feeds the ported afterimage quad (afterimage::build_afterimage_quad); the sound cue (+0x0d) goes through the ported cue decode into the SFX ring. - Player Seru-magic ids
0x81..=0x8broute to the summon-creature path; plain physical attacks carry no move id.
Open: the exact per-part transform composition of the spawned scene-graph (the shared piece the summon renderer also waits on), and the cue-group dispatch from the damage-application primitive is not wired in the port.
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Damage kernel | FUN_801dd0ac | Reads +0x00 only (lhu, sign-extended); stride built as 13a << 1; map base materialised at 0x801dd1a0 and 0x801dd36c; summon arm (bne a1,0x7 at 0x801dd104) skips the table. | funcs/801dd0ac.txt |
| Action setup | FUN_801dea50 | Computes table[map[actor+0x1df]] (map base at 0x801df27c) and caches it at ctx+0x1014 (sw at 0x801df284); walks the effect list from +0x0e. | funcs/801dea50.txt |
| Per-frame tick | FUN_801e09f8 | Dereferences the cached pointer ~25 times for every field except +0x00 / +0x0c; dispatches both effect lists; calls the streak draw FUN_801e1ab0 and the cue dispatcher FUN_8004fcc8. | funcs/801e09f8.txt |
| Streak draw | FUN_801e1ab0 | +0x0b becomes the texpage word 0x7700 + id at 0x801e1d54. | funcs/801e1ab0.txt |
| Spawn allocator + stager | FUN_80050ed4 → FUN_80021B04 | Move-FX spawn chain, pinned live by an exec breakpoint with the prototype base and list id in registers. | funcs/80050ed4.txt, 80021b04.txt |
| Cue expander | FUN_801E22C8 | Sole reader of the cue-group table (addiu v1, v0, 0x6470 at 0x801E2374); called from FUN_800402F4. | funcs/801e22c8.txt |
| Table extent | - | 0x801F4F5C + 44*26 = 0x801F53D4, exactly where the impact-config table begins; the record count is pinned by the next referenced address. | overlay image |
| Byte identity | - | The 0x801F4F5C..0x801F69D8 window is byte-identical across unrelated battle saves and matches the raw PROT 0898 bytes at file 0x26744; the overlay loads at slot-A base 0x801CE818 (file 0x25178). | battle save states |
| Engine oracles | - | move_power_map_is_special_attack_only, move_fx_records_vm_exec_disc (all 54 unique prototype records execute through the ported move VM), model_library_base_tracks_party_size. | disc-gated tests |
Details
The four power shifts in the damage kernel
The kernel reads +0x00 at three load sites and derives four shifts. They are not a full / half / quarter ladder: two bound a random roll, two are summed straight into the damage accumulator.
| Load | Shift | Site | Role |
|---|---|---|---|
0x801dd1c0 | >> 2 | 0x801dd1cc | roll modulus, div at 0x801dd1d4 |
0x801dd1c0 | >> 0 | 0x801dd240 | additive damage term |
0x801dd38c | >> 1 | 0x801dd39c | additive threshold term |
0x801dd3cc | >> 3 | 0x801dd3d8 | roll modulus, div at 0x801dd3e0 |
0x801dd3cc | >> 1 | 0x801dd448 | additive damage term |
Both moduli are used as rand % (x + 1), so power < 4 rolls a constant 0 on the >> 2 branch and < 8 on >> 3. The half and eighth scales appear only in the retry arm, which re-floors a too-low attacker score. The roll is then scaled by the element-affinity matrix FUN_801dd864, a sibling table in the same overlay.
Indexing in instructions - the odd map base and the delay-slot lui
The map lookup in the tick materialises 0x801F4E64 and reads lbu a0,-0x1(v1); the 0x801F4E63 base is that addiu 0x4e64 / lbu -0x1 pair, so the constant 0x4e63 appears nowhere in the code. The stride is built as 13a << 1, never a literal, so a search for 0x1a finds nothing either.
Of the three sites that materialise the base, the one at 0x801dd36c defeats a linear lui+addiu matcher: its lui a0,0x801f sits in the branch delay slot at 0x801dd2f8 and the addiu is the branch target, 0x74 bytes later with a0 clobbered in between. A sweep folding pairs must follow branch targets, not adjacency.
Per-field load sites: +0x02 at 0x801e0dc4, 0x801e13b8; +0x04 at 0x801df288; +0x06 at 0x801e0d70, 0x801e1360; +0x08 at 0x801e1018, 0x801e1074, 0x801e1274; +0x09 at 0x801e10d4; +0x0a stored at actor+0x21f, indexing 0x801f53d4 ((v-1)*4, packed u32 words with 0x3FF-masked lanes, not pointers) into actor+0x04; +0x0b at 0x801e0ca0, 0x801e0cd0; +0x0d at 0x801e184c; +0x0e at 0x801e0c54, 0x801df408, 0x801df4fc; +0x12 at 0x801e0d00, 0x801e114c, 0x801e1250, 0x801e12ac; +0x16 at 0x801e0ddc, 0x801e0f54, 0x801e13d0, 0x801e1550, 0x801e1800.
The +0x0c no-reader sweep - what the negative covers
Swept over bytes, not dumps: SCUS_942.54 plus every overlay image in static-overlays.toml, every 4-byte word decoded independently, for every valid R3000 load / store and j / jal target (not COP2, which cannot address a record). Access shapes modelled: absolute constants (lui+addiu/ori pairs folded and tested against [table, table + 44*26)), base-folded displacements, ×26-indexed registers, pointer-relative reads through lw rD, 0x1014(rB), straddling loads, and data-resident pointers. No reference lands at a shifted offset, no data word holds an in-table address, and PROT 0898's five byte-width loads at literal displacement 0xc all sit on unrelated RAM structs (the image has 134 memory operations at that displacement; the other 38 byte-width ones are stores). The one form an absolute scan cannot see is impossible here: $gp is 0x8007B318, so a signed-16 displacement off it reaches 0x80073318..0x80083317 and the table sits about 1.5 MB outside that window. PROT 0965 and 0971 are now in the overlay map and inside the sweep; PROT 0896 is closed by bytes, producing zero hits in either base-independent encoding. +0x0d, by contrast, is read - once, as a byte, at 0x801E184C, feeding the cue dispatcher.
Effect-list framing - the two consumers disagree
- Setup walks one contiguous run from
+0x0E, stopping only on a zero byte.0xFFis not a terminator there: it has bit 7 set and routes to the 2D path with id0x7F. - Tick reads
+0x12and+0x16as two separate 4-byte lists and treats+0x0E == 0xFFas a broadcast to all four arms.
Ids 0x0A, 0x2D and 0x2E additionally latch the spawned handle into ctx+0x1028 and seed the per-frame delta triple at ctx+0x1184, which is what makes their effect follow the actor. The 2D path (FUN_801dfdf0 → FUN_801DFDF8) special-cases pack1 0x04 → 0x801F5D90 and 0x13 → 0x801F5CF8, the two burst-arm move-VM trigger programs. The 61-entry space bounds both side tables; the runtime's < 100 spawn guard is a loose safety check.
The cue-group table 0x801F6470
Indexed by group id rather than effect id; the cue expander reads one 5-byte record per call:
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 1 | cue count | 0..=4; zero spawns nothing |
+0x01 | 4 | cue ids | Only the first count are read. Bit 0x80 = an actor cue (FUN_801DFDF0(id & 0x7F, pos, yaw), the strike pose); any other id indexes both effect side tables |
13 records fill 0x801F6470..0x801F64B5, followed by padding and the summon.DAT path literal; callers never index past 0xC. The table has exactly one consumer - a reference scan of SCUS plus every base-mapped overlay finds the address materialised once - while its two sibling tables are reached from five sites each, always in pairs. The caller is the damage-application primitive FUN_800402F4 (eleven branches, group ids 5..0xC literal or computed). The port has the kernel (legaia_engine_vm::battle_cue_group::expand_cue_group) and both tables but not that dispatch.
Effect-prototype records - the spawn path and model_sel
The tick calls FUN_80050ed4(world_pos, src_pos, 0x801f6324[id], 0x1000), a 0x60-slot allocator that tail-calls the shared stager FUN_80021B04 (the disassembly preserves a0..a3; the C decomp drops them), which reads the record's +0x00 model_sel (< 0 / 0x4000 / 0x4001 are transform-node / render-mode sentinels, else the mesh is DAT_8007C018[model_sel + gp[0x754]]), allocates an actor (FUN_80020de0), stores the record as the actor's move-VM buffer (+0x48), forces the PC to u16-index 2 (bytecode at record+4) and drives it through the move VM (FUN_80023070). move_power::parse_effect_proto_records decodes the whole table (ids 0x27 / 0x28 → 0x801F5BBC / 0x801F5BDC); a live Gimard “Fire Tail” mid-cast holds a part-actor whose +0x48 is 0x801F5484.
gp[0x754] (global 0x8007BA6C) is party_count + 2 in battle: 0 when no effect-model library is resident, 3 for the 1-member training party, 5 for the full party. The two fixed pool slots plus the live party meshes precede the effect-model library (PROT 0871, engine global_tmd_pool[3..=32]), so model_sel is library-relative and there is no per-summon base.
Roster cross-check numbers
Across 186 monsters, 46 distinct attack ids appear in the monster record's special-attack bytes (+0x21..=+0x23); 28 are in this table. The other 18 (0x2E / 0x2F / 0x3C / 0x4A..=0x6E, plus 0xA7 / 0xB8 beyond the map) are the magic / elemental casts. 95 of 186 monsters carry no magic attack at all. Freeze Thunder is record 22.
History: readings that were wrong
- Dumps labelled
overlay_0897_*/overlay_magic_*/overlay_muscle_dome_*carry the battle-overlay bytes at a wrong load base, which produced a claim that0x801F3990was a second damage kernel. At the real VA it is the cast audio-cue dispatcher and reads no field of the record. Take addresses from the extracted overlay image. 0x801f6418is a CLUT source-x, not a per-effect sound id, and each effect list uses both side tables rather than one each. A “~0x20-byte struct” reading of the prototype table was a coincidence of one record's size.
Full entries in do-not-re-walk § battle.
CLI
asset move-power <raw PROT 0898 entry> # dump every record (power, counters, effects, ...)
asset move-power <raw PROT 0898 entry> --effect-index # effect id -> triggering moves
The randomizer's move-power slider rewrites +0x00 in place through the same parser. Full reference: docs/formats/move-power.md.