Monster (enemy) battle animation Confirmed
Per-object rigid-transform keyframe animation for battle monsters, stored inside the monster's archive block (PROT entry 867). Distinct from the ANM container, which drives player / field actors. Implementation: legaia_asset::monster_archive (MonsterAnimation, PartPose, animations, idle_animation). Seen live on the enemy table, which plays each monster's idle loop.
Where it lives
Each monster's decoded archive block is [stat record + +0x4C action-offset array][name][TMD mesh @ +0x04][per-action entries][texture pool @ +0x08]. The magic_count (+0x4A) action entries the +0x4C u32 array points at are not just “spells” — each is an action descriptor whose head holds the action id (+0x00), SP cost (+0x74), and a sub-id (+0x77), and whose +0x8c field begins a packed transform-keyframe stream. (The runtime keyframe pointers at entry +0x04/+0x08 and the self-pointer at +0x88 are zero on disc; the loader reconstructs them, with +0x88 pointing at the +0x8c stream.)
Action index 0 (id 0x00) is the neutral idle animation the engine loops when the monster isn't acting; index 1 (id 0x01) is the basic attack, and the rest correspond to the monster's spell / special actions.
Texture pool (record +0x08)
The same archive block carries the model's colours in a texture pool the battle loader decodes from FUN_80055468: a 0x1E0-byte region of fifteen 16-colour palettes followed by a 4bpp index page (always 256 rows tall, 128 or 256 texels wide). Each TMD primitive selects its palette from the low six bits of its CBA (cba & 0x3F) and samples the page at its UVs — exactly the index-then-palette lookup the PSX GPU performs from VRAM (see the TIM 4bpp / CLUT model and the renderer). 186 of the 194 archive slots carry a clean TMD + texture pool; the rest are filler ids.
Packed stream (entry +0x8c)
u8 part_count // animated objects per frame == TMD object count
u8 frame_count
frames[frame_count]:
parts[part_count]:
u8 b[9] // six packed 12-bit fields (see below)
Each part record is 9 bytes encoding six 12-bit fields. Low bytes sit at [0,1,3,4,6,7]; the high nibbles are packed into [2,5,8]:
v0 = b0 | (b2 & 0x0f) << 8 tx (translation X)
v1 = b1 | (b2 & 0xf0) << 4 ty
v2 = b3 | (b5 & 0x0f) << 8 tz
v3 = b4 | (b5 & 0xf0) << 4 rx (rotation X)
v4 = b6 | (b8 & 0x0f) << 8 ry
v5 = b7 | (b8 & 0xf0) << 4 rz
tx, ty, tzare sign-extended 12-bit (-2048..2047) translation in TMD model units.rx, ry, rzare unsigned 12-bit Euler angles (0..4095, where4096= a full turn); values near4095are small negative rotations.
One part maps to one TMD object (a rigid body part). Across the retail roster the part count equals the TMD object count for >98% of actions (one model carries an extra non-animated object).
Playback
The renderer (FUN_80048a08) keeps a 12.4 fixed-point phase in the per-actor draw struct (+0x68): integer frame index = phase >> 4, sub-frame fraction = phase & 0xf. The decoder (FUN_8004998c) interpolates between frame i and i+1: linear for translation, shortest-path angle-wrap for rotation. The result is written to a pose buffer (6 shorts per object) and applied per object via the GTE in the draw loop, then FUN_800495c8 / FUN_8005b038 blend it onto the object vertices. The enemy-table viewer mirrors this: it plays each action and poses every part relative to its rest frame, so frame 0 is exactly the static model.
Export (glTF)
legaia_asset::monster_gltf::export_glb packs a monster's mesh, its baked texture, and every action animation into one binary glTF (.glb) — the universal interchange format. The rigid-per-object model maps straight onto glTF node animation: each TMD object becomes a node, the keyframe stream's translation + Euler rotation drive that node's translation / rotation channels (the Rz·Ry·Rx order recomposed as a quaternion), and a root node rotates the rig 180° about X to convert PSX +Y-down space to glTF's +Y-up. The per-prim CLUTs (cba & 0x3F) that a single material can't index are baked into a vertical palette atlas, with each vertex's V remapped into its band.
asset monster-archive … --id N --glb enemy.glbThe enemy table exposes the same export as a per-enemy download button.
Provenance (Ghidra trace)
FUN_8004998c— packed-stream decoder + frame interpolation.FUN_80048a08— per-actor battle draw; reads the phase, drives the decoder, applies the pose per object.FUN_800495c8/FUN_8005b038— GTE vertex blend of the decoded pose.FUN_80054cb0— monster init; copies the action/effect pointer (record+0x04) into actor+0x230.