At a glance

Where
SCUS_942.54, RAM DAT_800754C8; the name pointers are read through the shifted base DAT_800754D0 (= stats base + 8)
Size
190 records (ids 0x00..=0xBD), 12 bytes each. Ends exactly where the description-pointer table 0x80075DB0 begins
In the port
legaia_game_tables::spell_names (SPELL_COUNT = 190); CLI asset spell-names <SCUS> [--json]; engine mirror engine-battle::retail_magic
Confidence
Confirmed - readers traced in the battle and menu overlays; the curated magic chart joins byte-exact on MP
Used by
Battle-action SM (cast + banner), field menu (descriptions), spell-cost slider, Magic viewer

Record layout

Record id sits at base + id × 12. Every record has the same shape:

The stats reader uses base DAT_800754C8 and reads +0..+4; the name reader uses DAT_800754D0, 8 bytes on, over the same rows.

FieldMeaning
cast class0x32 player summon, 0x14 plain cast, 0x63 ('c') capture-class module (below)
sub-indexWithin the class; for capture-class records the module index (PROT 935 + sub)
target shapeSide / scope bits (below)
MP costDeducted from the caster when the cast starts
description indexInto the pause-menu description pointer array; 0 = none
paddingZero on every record - there is no power byte hiding here
name_ptrDisplay-name C string, prefixed by a colour-control code (0xCE, element colour, space)
The extent is 190, and nothing enforces it

The 12-byte walk does not stop at record 0xBD; only the reader bounds it. A tool that sweeps ids up to 255 walks straight into the neighbouring tables, and a write there corrupts the description pointers and the arts-name table on a patched disc. SPELL_COUNT is 190 (reading past the extent).

Id ranges

IdsContents
0x00..=0x24Internal enemy-attack tiers - MP / target populated, empty name, no description
0x25..=0x7FNamed monster attacks (Fire Breath 0x25, Tail Fire 0x27, …) and capture-class spells
0x80Boundary entry (MP and description both 0)
0x81..=0x95Player Seru magic, Gimard … Gilium. The first 11 (0x81..=0x8B) are the engine-pinned block retail_magic::SERU_MAGIC
0x9A..=0xA0Ra-Seru summons - Palma, Mule, Horn, Jedo, Meta, Terra, Ozma (the hidden eighth, Juggernaut, sits outside this run)
0xA1..=0xBDBoss cinematic casts and late capture-class spells (Dead End Crisis 0xA1 … Jugger Power 0xBA)

Player Seru-magic block (0x81..=0x8B)

MP and target are byte-exact from the executable; element comes from the curated game-data chart, which matches on every MP value.

IdNameElementMPTarget+0+1
0x81Gimardfire10one enemy0x320x00
0x82Theederthunder24one enemy0x320x00
0x83Veralight6one ally0x000x03
0x84Gizamwater28all enemies0x320x00
0x85Nightodark13one enemy0x320x00
0x86Zenoirfire36one enemy0x320x00
0x87Vigurothunder64all enemies0x320x00
0x88Swordiewind32one enemy0x320x00
0x89Orblight18all allies0x010x04
0x8AFreedwater40all enemies0x320x00
0x8BNovawind48one enemy0x320x00

The nine enemy-side spells share +0 = 0x32, +1 = 0x00: the table cannot tell Gimard from Nova, which is the first sign that damage lives elsewhere. The two ally-side spells break the pattern (0x83 Vera is 0x00 / 0x03, 0x89 Orb is 0x01 / 0x04).

Target shape (+2)

Two bits, read relative to the caster: 0x40 = the caster's opponents (clear = its own side), 0x20 = every target on that side (clear = single). Both target pickers test exactly these: the monster AI branches on byte & 0x60 and the player picker on 0x40 then 0x20.

ValueShape
0x44one opponent
0x64all opponents
0x06, 0x04one ally, or the caster (heals; the monster buff tier)
0x26, 0x24all allies (group heals; the revive summon Horn)
0x66, 0x46all / one of the party (the boss specials: Mystic Circle, Final Crisis, Evil Seru Magic, Ultra Charge)

Bit 0x02 is a separate flag, field-castable: the field Magic list greys every spell without it. Read as the side bit it happens to agree on the four player values, and misreads the rest - it would aim Cort's Mystic Circle at Cort himself. Decoder SpellEntry::target_shape.

Cast classes (+0)

The class byte chooses which band of the battle-action state machine runs the cast:

+0ClassWhat happens
0x32Player summonSummon band; pages a per-summon overlay (PROT 903..) that spawns the Ra-Seru body and applies damage
0x14Plain castOrdinary magic band with the caster's animation (Tail Fire, Astral Wave, …)
0x63Capture-classPages the per-spell cast module PROT 935 + sub and starts its voice cue. Covers Seru capture, the Amulet, and the boss cinematic casts; the module carries its own baked damage constants

Because the sub-index is static table data, the identity of every capture-class module is readable from the executable alone (spell_names::capture_class_records). The 32 modules (PROT 935..966) are shared: a multi-spell module dispatches per spell id at its head.

The byte has two readers, and they agree. The pager streams the module in; the battle image's cast-tick dispatcher FUN_801F2160 re-reads the acting actor's queued id, indexes this table, takes +1, bounds it with sltiu …, 0x20 and jumps through a 32-slot table - the same PROT 935 + sub entry, ticked once per frame while the cast runs. Outside the capture class nothing dispatches on the byte, because no other class reaches that routine.

Capture-class module index (PROT 935..966)
PROTSpellsPROTSpells
9350x4A Earthquake9510x36 Chaos Flare; 0x5B Scythe Wind
9360x4B Hyper Crush9520x5C Bloody Horns; 0xB8 Astral Slash
9370x4C Hyper Lightning9530x5D Terio Punch; 0x5E Bull Charge
9380x4E Chaos Breath; 0xB7 Mystic Circle9540x5F Fatal Decision
9390x4F Spore Gas9550x60 White Shield; 0x6E Kiss of Death; 0x6F Melt Spray; 0x70 Terror Scream; 0x72 Power Charge; 0x73 Void Accessories
9400x3C Glare; 0x50 Divide; 0xAC Mystic Shield; 0xAE Clone9560x71 Water Hazard; 0x75 Paralyzing Wave
9410x51 Steal; 0xB9 Stone Circle9570x76 Death Game; 0x77 Thunder Storm
9420x52 Power Up; 0xAA Dark Typhoon9580x79 Blazing Slash
9430x40 Curse; 0xB5 Lapis Wave9590x7A Megaton Press
9440x37 Guilty Cross; 0x53 Curse All9600x7B Plasma Strike; 0xA6 Neo Star Slash
9450x54 Water Column; 0xBA Jugger Power9610xA1 Dead End Crisis; 0xB4 Final Crisis
9460x55 Call Wave; 0x56 Big Wave9620xA2 Blade Breath; 0xA3 Thunder Needle; 0xA4 Gigaton Press; 0xA5 Ultra Charge
9470x57 V-Windhash; 0xA7 Neo Windhash9630xB3 Genocidal Cannon
9480x58 Cross Beam9640xAF Element Change; 0xB0 Rogue Wind; 0xB1 Rogue Thunder; 0xB2 Rogue Flame
9490x59 Water Crystals9650xB6 Doomsday
9500x5A Rolling Flare; 0xAB Shadow Break9660xAD Evil Seru Magic

The sub-id space covers 935..=966 with no orphan slots. The status-only modules (940, 954, 955) are exactly the cells with no damage spells. An older attribution of 965 as a sibling of the battle-tutorial overlay was an entry-size over-read (do-not-re-walk).

How an enemy's cast lands in the same id space

An enemy is named exactly like a party caster. Two selection mechanisms both end in the same byte on the live actor, and the banner reads the row that byte names:

  1. Record arrayThe AI picker reads a global spell id from the monster record's three-slot magic-attack array.
  2. Special-cast switchThe picker's tail runs a hardcoded switch on the monster's formation id - about 30 boss and species cases gated on HP fraction, MP, round counter, RNG cadence and a not-charmed check.
  3. QueueThe chosen id is written to the actor's action byte.
  4. BannerThe action SM reads this table at that id - the same row a party cast would use.

The monster archive's own local action ids (0x0C..=0x1F) only gate action cost - they are not this table's id space. Two spells, Curse 0x40 and Curse All 0x53, are sourced by neither mechanism and are casterless in retail.

Notable special-cast switch cases
Formation idCasts queuedTrigger
0x4B ZetoCall Wave 0x55 → Big Wave 0x56 via a chain cell40% + MP ≥ 100
0x8B XainBull Charge 0x5E → Terio Punch 0x5D; Bloody Horns 0x5C from the record arrayrand % 3
0xA2..0xA4 Gi / Che / Lu Delilasspell = formation id − 0x29 (Blazing Slash / Megaton Press / Plasma Strike)every third round
0xA6 Sim-Seru GazaNeo Star Slash 0xA6odd rounds, MP ≥ 200
0xA8 RogueElement Change 0xAF, then Rogue Wind / Thunder / Flameelement-cycling counter
0xB3 Songi (Seru-Kai)Genocidal Cannon 0xB3two-turn charge
0xB4..0xB6 Cort formsEvil Seru Magic / Mystic Circle / Mystic Shield; Ultra Charge → Final Crisis, Doomsday; the Dead End Crisis ladderround-scripted
species bandsSteal, Power Up, White Shield, Rolling Flare, Power Charge, Void Accessories, Paralyzing Wave, Death Game, Thunder Storm, Stone Circle, Chaos Breath, Jugger Power, Lapis Wave, …HP-fraction / cadence gates

The three Delilas jump-table slots share one arm that computes the spell id arithmetically from the formation id; the whole mapping is the single instruction word 0x2442FFD7 (addiu v0,v0,-0x29) at PROT 0898 file offset 0x1CFFC. Two id spaces collide on 0xB5 (spell Lapis Wave, formation Cort); the branch at 0x801E6D04 is a formation test, not a spell test - do-not-re-walk.

Descriptions and names

The pause menu's spell info window reads the +4 byte and indexes a flat pointer array at 0x80075DB0; index 0 means no description. Player-block strings are a title line, a line break, then an effect line. Parser SpellEntry::desc.

Each name string is followed by NUL bytes up to the next word boundary - that padding is the whole edit budget for a rename, and it is word alignment, not a fixed slot (a 13-byte name has three spare bytes, a 15-byte name one). Names nest - a text search for one finds the longer name that contains it - so the only safe way to rewrite one is through its record's pointer (spell_names::name_field).

Reading past the extent

Both neighbouring tables start on exact multiples of the 12-byte stride, so an over-read lands on whole words:

IdsWhat the walk lands in
0xBE..=0xD4the description-pointer table
0xD5..the arts-name table (20-byte stride, name pointer at +0xC)

Whenever the arts index is 1 mod 3, the arts name pointer and a phantom spell record's name pointer are the same four bytes (spell 0xD7 = arts record 1, Burning Flare). A rename through either coordinate is one write. asset spell-names sweeps 256 ids for inspection, so the aliased rows show above 0xD4; the patcher stops at 190.

Where the damage comes from

The table carries no power field. The cast state reads only MP, the class byte and the name pointer; the static attack-vs-defence kernel is melee / arts only. Magnitude is computed at cast time by whichever overlay runs the spell:

  • Damage summons (PROT 0903 / 0904 / 0906 / 0908 / 0909 / 0910 / 0912 / 0913, plus 0914 and one arm of 0915) roll rand % (summon AGL + 1) + summon HP + caster AGL × 2 against a defender roll, then apply element affinity, status weakening and the 9999 cap - the kernels on the battle-formulas page, ported in engine-battle-vm::battle_formulas.
  • Heal summons are just two of the eleven, and their formulas differ: Vera (PROT 0905) restores level × 0x20 + 0xE0 on one ally, Orb (PROT 0911) restores (level << 6) + 0x1C0 across the party and cleanses status from level 3. Both read the caster's per-magic level byte out of the character record, not a power byte.
  • Neither: Nighto (PROT 0907) computes no amount at all - a kill roll zeroes the target's HP outright, otherwise it sets the confuse bits.
  • Capture-class casts carry baked constants in their cast module.
  • The per-move power scalar that does exist - the move-power table - feeds the arts / physical branch and shares this id space (0x25..=0x74 are the named monster attacks).

How we know

Provenance by function
FunctionAddressWhat it provesDump
Battle-action SM, cast states 0x28 / 0x3CFUN_801E295C (PROT 0898)MP at +3, class at +0, name via DAT_800754D0 + id*0xC; no power readoverlay_battle_action_801e295c.txt
Enemy AI pickerFUN_801E9FD4 (PROT 0898)Record array + formation switch both write the actor action byte; Delilas arm at 0x801EB7C0overlay_battle_action_801e9fd4.txt
Menu spell info windowFUN_801D2E74 (PROT 0899)+4 indexes the description pointers at 0x80075DB0overlay_menu_801d2e74.txt
Melee kernelFUN_801EC3E4Returns early unless the queued action is a physical id - magic never reaches itoverlay_battle_action_801ec3e4.txt
Summon damage kernelsFUN_801DD0AC / FUN_801DD864 / FUN_801DDB30Summon magnitude is battle-state-derivedoverlay_battle_action_801dd0ac.txt + siblings
Overlay pagerFUN_8003EC70Capture-class sub + 0x28 and summon id - 0x79 resolve to PROT 935.. / 903..8003ec70.txt
Disc join-All 11 pinned targets + MP byte-exact; the curated chart joins on MP for 21 Seru + 7 Ra-Seru-

Source of record: docs/formats/spell-table.md.

See also