fix(B.6): smooth local rotation — remove 20° snap-on-approach (not retail)

User report: 'quick snap at like 30 degrees to the last position. Not
a smooth turn. Did you verify with retail?'

Verified against retail decomp at MoveToManager::HandleTurnToHeading
(0x0052a0c0). Retail's pattern:

  - Body rotates continuously via a TurnLeft/TurnRight motion cycle.
  - The ONLY snap is set_heading(target, 1) after heading_greater()
    detects we've passed the target (overshoot protection).
  - No 'snap when close to target' tolerance band — that's purely
    a sparse-update fudge in RemoteMoveToDriver (the remote-creature
    path with ~1Hz UpdatePosition broadcasts).

I'd copied the snap-on-approach tolerance from RemoteMoveToDriver to
ApplyAutoWalkOverlay. Wrong: local player rotates at per-tick
resolution, no sparse-update problem to compensate for. Removed.

The MathF.Min(|delta|, maxStep) clamp naturally lands the body on
the target heading without overshoot in the final partial tick, so
no separate snap-on-overshoot branch is needed for our integrator
either.

Visible effect: 1.8m humanoid rotating ~180° at π/2 rad/s takes ~2 s
of smooth turn now, instead of ~1.3 s of turn + instant 20° snap at
the end.
This commit is contained in:
Erik 2026-05-15 15:19:29 +02:00
parent cffb10ff18
commit 7158c46d46

View file

@ -490,15 +490,20 @@ public sealed class PlayerMovementController
float delta = desiredYaw - Yaw;
while (delta > MathF.PI) delta -= 2f * MathF.PI;
while (delta < -MathF.PI) delta += 2f * MathF.PI;
if (MathF.Abs(delta) <= RemoteMoveToDriver.HeadingSnapToleranceRad)
{
Yaw = desiredYaw;
}
else
{
float maxStep = RemoteMoveToDriver.TurnRateRadPerSec * dt;
Yaw += MathF.Sign(delta) * MathF.Min(MathF.Abs(delta), maxStep);
}
// Retail-faithful local rotation: rotate continuously at
// TurnRate, never snap until overshoot would occur. Retail's
// MoveToManager::HandleTurnToHeading (0x0052a0c0) only snaps
// when heading_greater() detects we've crossed the target —
// there's no "snap when close" tolerance band. The earlier
// 20° snap was borrowed wrongly from RemoteMoveToDriver
// (which is the sparse-update-fudge path for remotes).
//
// MathF.Min(|delta|, maxStep) naturally clamps the final
// fractional step to exactly delta, so we land on the
// target heading without overshoot.
float maxStep = RemoteMoveToDriver.TurnRateRadPerSec * dt;
Yaw += MathF.Sign(delta) * MathF.Min(MathF.Abs(delta), maxStep);
while (Yaw > MathF.PI) Yaw -= 2f * MathF.PI;
while (Yaw < -MathF.PI) Yaw += 2f * MathF.PI;