How it works

When you cast a fireball in battle, three things happen visually: the spell-cast pose plays on the caster, a sequence of sprites animates between caster and target (the "fireball flying" frames), and a hit explosion plays on the target. Internally that's one effect script doing all three — one entry in efect.dat describing the whole thing as a sequence of frame batches.

The effect VM is what runs that script. It owns a fixed pool of "master slots" (32 of them, one per simultaneous effect) and "child slots" (128 of them, one per active sprite within an effect). Every frame, the per-frame walker iterates the master slots and pushes each one through its state machine: spawn the next batch of children, update positions, decrement timers, retire finished sprites, advance to the next state.

Unlike the field VM or the move VM, this isn't a clean bytecode dispatcher with a switch table. It's a state machine where the "next state" tokens live inside the on-disc effect script, and the walker reads them inline. Producing a clean opcode table from it would mean extracting the per-state transition logic by hand (~10–20 cases). The cleaner port path is to model the walker as a state-machine class and accept its decompile shape rather than insisting on an opcode table.

The on-disc input is the runtime 2-pack wrapper at PROT entry 873 (data\battle\efect.dat): pack0 holds 14 sprite-anim records, pack1 holds 33 effect-ID scripts indexed by the public effect ID.

Where it lives

All three functions are in the battle overlay (0898_xxx_dat):

FunctionSpanRole
0x801DE9140x138Init / pack-fixup. Called from FUN_800520F0 case 0xE with (id=0x1000, param=0xA00).
0x801DFDF80x290Public spawn-effect API: (byte effect_id, short* world_pos, ushort angle).
0x801E00880x970Per-frame walker (update + render). 600+ instructions of inlined state transitions.

How it dispatches

Each 28-byte master slot carries:

(state, counter, ?, sub_state, pos_x, pos_y, pos_z, data_ptr)

The walker reads *data_ptr as the next-state token, but state transitions are inlined throughout FUN_801E0088. To produce a clean-room opcode table you'd need to extract per-state-byte transition logic by hand. The 32-master / 128-child slot pool, the spawn API, and the per-frame walker are all well-understood — the port itself is straightforward; the only question is whether to label the format an "opcode table" or a "state machine".

Pool layout

One contiguous 5008-byte block at _DAT_8007BD30:

+0x000  16 bytes   table-head record set by init
+0x010  4096 bytes 128 × 32-byte child slots — per-sprite render state
+0x1010 896 bytes  32 × 28-byte master slots — per-effect-instance state
+0x1390 1968 bytes (unused / future expansion)

32 max simultaneous effects × ~4 sprites avg = 128-child sprite pool.

Side-band streaming-effect handler

Function
0x801F17F8
Called from
FUN_800520F0 case 0xFF
Buffer size per slot
0x10800 = 67584 bytes

Streams two specific runtime-only files via FUN_800558FC:

FilePROTTrigger
data\battle\summon.dat0x37FSelected when _DAT_8007BD24[0x26B] & 0x80 != 0
data\battle\readef.dat0x380Opposite branch

Format unverified; may share the 2-pack layout but not yet confirmed.

Effect-ID → human effect name mapping

Effect IDs are anonymous; no string table maps id → "fireball / thunder / heal". To name effects, trace call sites of FUN_801DFDF8 in damage / battle-action code (in the town/level-up overlays). Each caller passes a literal byte for effect_id; correlate with the action that triggered it (a Tactical Arts move, an item use, a spell cast).