Monster (enemy) battle animation Confirmed
How the battle monsters move. Legaia's enemies are rigid-body puppets: each body part is a separate object of the monster's TMD mesh, and every action - idle, walk, attack, knockdown - is a stream of per-part position + rotation keyframes stored right next to the mesh in the monster's archive block (extraction entry 0867 = raw TOC index 869; retail-space CDNAME block monster_data, inside PROT.DAT, the disc's single big archive). This is a different system from the ANM container that drives player / field actors - a recurring theme on this disc. Mesh, texture, and every animation export together as a standard .glb (see Export).
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), AGL (action) 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 walk/approach cycle, and the rest correspond to the monster's spell / special actions. The entry's first byte is a semantic action tag: 2/3 light hit reactions, 4 knockdown, 5 get-up, 0x0B block (the battle loaders cache a tag → entry-index map at actor +0x1EF..+0x1F3 the damage path stages reactions from), and 0x20/0x21/0x22 the attack pre-approach / close-in / victory family the action SM resolves by first-byte search.
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).
The transform is absolute model-space, not a delta from a rest pose: each TMD object is modelled at its own local origin, and the per-part [tx, ty, tz] places that object at its socket while [rx, ry, rz] orients it about its local origin (world = Rz·Ry·Rx · v_local + t). Frame 0 is therefore the assembled rest pose - and a raw monster TMD rendered without its animation looks like a collapsed heap of parts piled at the origin; assembling frame 0 spreads it into its full silhouette.
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 per-frame cursor advance lives in the anim-node tick FUN_80047430: phase += (frame_dt * actor[+0x21D] * record[+0x78]) >> 1, where +0x21D is the actor's speed scale (normally 4) and the entry's +0x78 byte is its playback rate (1 or 2 across the retail corpus). The enemy-table viewer mirrors this: it plays each action applying each part's keyframe transform as the absolute model-space pose, so frame 0 is exactly the assembled rest pose.
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.