diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 32ba7ed..ffdcd95 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -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;