Spell table Confirmed
Where the game keeps what every spell is: MP cost, display name, who it targets, and whether it can be captured. It is one static table baked into SCUS_942.54 (the game's main executable on the disc), 12 bytes per spell, and a single shared numbering - the spell id space - covers everything from unnamed enemy-attack tiers through named monster attacks to the party's Seru magic and the Ra-Seru summons. When anything casts, the battle engine reads the cost and name straight out of this table. The twist: the table carries no damage number at all - a spell's magnitude is computed from battle state at cast time, never stored here (see below).
Table base + record layout
A note on naming: DAT_80xxxxxx is a Ghidra label (Ghidra is the disassembler this project uses) for a fixed RAM address - here, where the table sits once the PlayStation loads the executable. Because the data is static, the same bytes can be read straight out of the SCUS_942.54 file on the disc; the parser translates the RAM address to a file offset. "Stride" is the size of one record - step that many bytes to reach the next spell.
The table is viewed through two interleaved base pointers over the same records: DAT_800754C8 (the stats base, read for MP + class + target) and DAT_800754D0 (the same base shifted +8, read for the name pointer). Both step 0xC per spell id, so record id sits at DAT_800754C8 + id*0xC.
| Field | Value |
|---|---|
| Stats base | DAT_800754C8 |
| Name-pointer base | DAT_800754D0 (= stats base +8) |
| Index form | DAT_800754C8 + spell_id*0xC |
| Stride | 0xC bytes |
| Offset | Type | Field |
|---|---|---|
+0 | u8 | class byte - 'c' (0x63) marks a capture-class spell |
+1 | u8 | sub-index within the class |
+2 | u8 | target shape (below) |
+3 | u8 | MP cost |
+4 | u8 | animation id |
+5..+8 | - | padding (zero) |
+8 | u32 | name_ptr - pointer to the display-name C-string (leading 0xCE colour-control prefix from MES, the game's text markup, before the ASCII name) |
Target shape (+2)
Two independent bits over a side/scope pair: bit 0x02 = ally side (clear = enemy side), bit 0x20 = all targets on that side (clear = single).
| Value | Shape |
|---|---|
0x44 | one enemy |
0x64 | all enemies |
0x06 | one ally |
0x26 | all allies |
One documented exception: the revive Ra-Seru Horn ("Resurrector", id 0x9C) carries an enemy-side 0x24 byte even though it revives all allies - the summon projects toward the enemy field and the revive is special-cased by spell id. Decoded by legaia_asset::spell_names::SpellEntry::target_shape.
Id ranges
| Ids | Contents |
|---|---|
0x00..=0x24 | internal enemy-attack tiers - MP / element / target populated, empty name pointers |
0x25..=0x7F | named monster attacks (Fire Breath 0x25, Tail Fire 0x27, …) + capture-class spells ('c' at +0) |
0x81..=0x95 | the 21 player Seru-magic spells (Gimard 0x81 … Gilium); the engine-pinned block retail_magic::SERU_MAGIC covers 0x81..=0x8B, anim ids 0x25..=0x2F |
0x9A..=0xA0 | Ra-Seru summons - Palma, Mule, Horn, Jedo, Meta, Terra, Ozma (the hidden 8th, Juggernaut, is outside this contiguous named region) |
An enemy cast is named exactly like a party cast: the AI spell picker FUN_801E9FD4 (a Ghidra-traced function in the battle overlay - a chunk of code the game loads into RAM on demand; extraction entry 0898 / raw TOC index 0x384) reads a global spell id from the monster record's magic-attack array at +0x21..=+0x23, writes it into the live battle actor at +0x1DF, and the battle-action SM prints &DAT_800754D0 + id*0xC. The monster archive's local +0x4C entry ids (0x0C..=0x1F) only gate action cost - they are not this table's id space.
No per-spell power scalar
The table carries no magic-power field - bytes +5..+8 are zero on every record, and the whole player Seru block shares class = 0x32, sub = 0, so the table cannot even distinguish Gimard from Nova. Summon magnitude is battle-state-derived, not static data:
- Damage summons roll via the shared kernel
FUN_801dd0ac-rand % (summon_AGL + 1) + summon_HP + caster_AGL×2against a defender roll, then element-affinity / status-weaken / magic-power scaling inFUN_801dd864. Ported as pure kernels inlegaia_engine_vm::battle_formulas. - Heal summons compute
(power_byte << 5) + 0xE0inline, withpower_bytefetched from the caster's character record (spell-id match at+0x705, power byte at+0x729). - The genuine per-move power scalar that does exist - the 26-byte-stride table at overlay VA
0x801F4F5C- feeds the arts/physical branch and shares this table's id space; see the move-power table.
Parser + engine consumption
legaia_asset::spell_names resolves the table from a SCUS_942.54 image (PSX-EXE t_addr → file-offset map); CLI asset spell-names <SCUS> [--json]. The engine sources the player Seru-magic catalog's MP + target from the user's executable via legaia_engine_core::retail_magic::seru_magic_catalog_from_scus, falling back to the pinned retail_seru_magic_catalog on disc-free builds. The legaia-gamedata magic_vs_disc oracle joins the curated magic chart to this table by name: MP byte-exact for all 21 Seru + 7 Ra-Seru joins, target shapes agreeing everywhere except the explicit Horn exception.
Superseded readings
Superseded readings
- The earlier "bit
0x40= enemies" target reading is equivalent for the four player-block values but misclassifies the internal enemy-attack tiers (byte0x04:0x40clear yet enemy-targeting). The0x02-bit / low-nibble reading is correct. - The "spell magnitude is in the summon jump table" hypothesis is falsified: the JT at
0x801F69D8resolves into the resident render overlay (extraction entry 0900) and is animation/rendering only - no multiply, no HP write, no power read. The magnitude is applied by the paired stager overlays (extraction entries 0903..0915). - The
0xC5MES substitution table atDAT_80075EC4, once mistaken for a spell-name source, is the Tactical Arts name table (see art records) - per-character art names, no spells.