feat(anim): integrate Omega for TurnRight/TurnLeft dead-reckoning

Remote entities in a Turn cycle had no rotational dead-reckoning: their
Rotation quaternion only updated on UpdatePosition arrival, making
in-place turns look jumpy when the server sent updates at 5-10Hz. The
sequencer exposes Omega (radians/sec per axis) via the same SetVelocity/
SetOmega pair retail uses, so all we need to do is integrate it.

Implementation in TickAnimations:
  float angle = |omega| * dt;
  Quaternion delta = CreateFromAxisAngle(normalize(omega), angle);
  entity.Rotation = normalize(entity.Rotation * delta);

Gated on the low-byte motion being TurnRight (0x0D) or TurnLeft (0x0E)
so we don't apply spin to non-turning cycles that happen to carry a
nonzero omega (e.g. creature sway emotes). Matches ACE
Sequence.apply_physics L221-L229:
  frame.Rotate(Omega * quantum)
which treats the argument as a local-axis scaled rotation.

No new tests — Omega is the rotational dual of Velocity, already covered
by the velocity-integration tests. 659 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-19 10:42:08 +02:00
parent 24974cfbb9
commit dc317a321b

View file

@ -3436,6 +3436,35 @@ public sealed class GameWindow : IDisposable
}
}
}
// Rotation integration: if the sequencer's Omega is non-zero
// (TurnRight / TurnLeft / any cycle with baked-in spin), rotate
// the entity's quaternion around the omega axis by |omega|*dt.
// Matches ACE Sequence.apply_physics L221-L229:
// frame.Rotate(Omega * quantum)
// where frame.Rotate treats the argument as a local-axis
// rotation. Only kicks in for Turn cycles (low byte 0x0D/0x0E)
// — other motions either have zero omega or integrate rotation
// server-side.
var seqOmega = ae.Sequencer.CurrentOmega;
if (seqOmega.LengthSquared() > 1e-6f)
{
uint mlo2 = ae.Sequencer.CurrentMotion & 0xFFu;
bool isTurning = mlo2 == 0x0D || mlo2 == 0x0E; // TurnRight / TurnLeft
if (isTurning)
{
// Omega as a scaled axis-angle. Build a delta quaternion
// and compose it on the entity's current rotation.
float angle = seqOmega.Length() * dt;
if (angle > 1e-5f)
{
var axis = System.Numerics.Vector3.Normalize(seqOmega);
var deltaRot = System.Numerics.Quaternion.CreateFromAxisAngle(axis, angle);
ae.Entity.Rotation = System.Numerics.Quaternion.Normalize(
ae.Entity.Rotation * deltaRot);
}
}
}
}
// ── Get per-part (origin, orientation) from either sequencer or legacy ──