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.