Per-actor motion VM
A small per-actor pursue / patrol / face-target VM at FUN_8003774C in SCUS_942.54. Distinct from the actor / sprite VM (sprite spawn / despawn) and the move-table VM (Tactical Arts / battle animation): this one drives NPC pathing, camera follow, and "face the speaker" cinematic posing during dialog.
How it works
Each script entry is 1 + N bytes:
+0 u8 op_byte ; bit 0x7F = opcode, bit 0x80 = "select target"
+1 u8 target_id ; only present if bit 0x80 set in op_byte
+N u8 operand[...] ; opcode-specific operands
When the high bit is set, the VM resolves a target actor before applying the body. 0xF8 resolves to "this actor", 0xFB follows a linked list at _DAT_8007c34c looking for a matching record-class signature, and any other id linearly scans the actor list at _DAT_8007c354 matching against the actor's id field at +0x14.
Opcodes
| byte | retail addr | name | semantics |
|---|---|---|---|
0x37 | 80037894 | TranslateY | accumulate Y axis by per-frame speed |
0x38 | 80037de0 | RotateToAngle | yaw rotates toward absolute angle; 12-bit fixed-point math, shortest-path CCW/CW select (body0 & 0x80), explicit direction override (body1 & 0x80), proportional interpolation step, 16-entry ANGLE_TABLE at DAT_80073f04 |
0x41 | 80037894 | TranslateX | accumulate X axis by per-frame speed |
0x43 | 80037f5c | NoOp | tick budget consumed, no actor mutation |
0x47 | 80037ba8 | MoveTowardTarget | step actor XZ toward (tx, tz) |
0x4C | 80037de0 | FaceTarget | yaw rotates to bearing computed via atan2(dx, dz) (bearing_to_yaw); sub-modes 0x85 / 0x8E / 0x8F gate which component is rotated |
| (default) | - | Done | terminate |
Clean-room port
legaia_engine_vm::motion_vm in crates/engine-vm/src/motion_vm.rs is the clean-room port. All opcodes are ported: 0x37 TranslateY, 0x38 RotateToAngle, 0x41 TranslateX, 0x43 NoOp, 0x47 MoveTowardTarget, 0x4C FaceTarget. The angle-math opcodes use the 12-bit fixed-point shortest-path quadrant logic from FUN_8003774C lines 690+.
Camera integration
The runtime Camera in engine-core::camera consumes:
- The field-VM op-
0x45event stream (CameraConfigure/CameraSave/CameraLoad/CameraApply) for the high-level camera state. - The motion VM (optional) for cinematic pre-baked camera paths via
Camera::tick_script.
The default mode follows a target actor slot at a configured distance + height - same shape as the retail "follow the player" camera.
Field-NPC walking
World::tick_field_npc_motions (engine-core) drives MAN-placed field NPCs through the 0x47 MoveTowardTarget pursue step, one motion-VM step per field tick, writing the live position back into World::field_npc_positions so the moving NPC's ±40-unit collision box and its interact box follow it. Three start paths feed it: autonomous patrol routes (each placement's own pre-text 0x4C 0x51 move-to-tile ops, decoded by man_field_scripts::placement_motion_route; opt-in via play-window --live-npcs, paused while a dialogue is up), interaction-prologue runs (a record's own 0x4C 0x51 walks the interacted NPC), and the actor-VM start_motion glide (op 0x09, retail FUN_800358c0). Residue: the exact retail per-NPC glide speed is unpinned (the engine paces NPCs at the player walking step), and per-actor field-VM channels are not executed - the engine loops the decoded waypoint list.