From 7158c46d46b428f966180bf58bcb39d740f39722 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 May 2026 15:19:29 +0200 Subject: [PATCH] =?UTF-8?q?fix(B.6):=20smooth=20local=20rotation=20?= =?UTF-8?q?=E2=80=94=20remove=2020=C2=B0=20snap-on-approach=20(not=20retai?= =?UTF-8?q?l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Input/PlayerMovementController.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) 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;