diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 333b948..353f8b4 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -226,8 +226,9 @@ public sealed class GameWindow : IDisposable
///
/// True while a server MoveToObject/MoveToPosition packet is the
/// active locomotion source. Retail runs these through MoveToManager
- /// and CMotionInterp using the packet's runRate; deriving velocity
- /// from sparse UpdatePosition deltas under-speeds combat chases.
+ /// and CMotionInterp using the packet's runRate; until we port the
+ /// full target solver, use this only to protect packet-derived
+ /// animation speed from velocity-cycle clobbering.
///
public bool ServerMoveToActive;
///
@@ -2452,16 +2453,6 @@ public sealed class GameWindow : IDisposable
if (_remoteDeadReckon.TryGetValue(update.Guid, out var remoteMot))
{
remoteMot.ServerMoveToActive = update.MotionState.IsServerControlledMoveTo;
- if (remoteMot.ServerMoveToActive && !IsPlayerGuid(update.Guid))
- {
- // Retail MoveTo packets already carry enough state
- // for CMotionInterp to drive velocity. A velocity
- // inferred from sparse UpdatePosition packets lags
- // during combat chases and visibly under-speeds the
- // run cycle until the next hard snap.
- remoteMot.HasServerVelocity = false;
- remoteMot.ServerVelocity = System.Numerics.Vector3.Zero;
- }
// Forward axis (Ready / WalkForward / RunForward / WalkBackward).
remoteMot.Motion.DoInterpretedMotion(
@@ -2685,6 +2676,11 @@ public sealed class GameWindow : IDisposable
if (IsPlayerGuid(serverGuid)) return;
if (rm.Airborne) return;
if (ae.Sequencer is null) return;
+ // MoveTo packets already seeded the retail speed/runRate cycle.
+ // Keep UpdatePosition-derived velocity for render position only;
+ // using it to choose the cycle reverts fast chases back to slow
+ // velocity-estimated animation.
+ if (rm.ServerMoveToActive) return;
var plan = AcDream.Core.Physics.ServerControlledLocomotion
.PlanFromVelocity(velocity);
@@ -2771,7 +2767,6 @@ public sealed class GameWindow : IDisposable
System.Numerics.Vector3? serverVelocity = update.Velocity;
if (serverVelocity is null
&& !IsPlayerGuid(update.Guid)
- && !rmState.ServerMoveToActive
&& rmState.LastServerPosTime > 0.0)
{
double elapsed = nowSec - rmState.LastServerPosTime;
@@ -5047,6 +5042,15 @@ public sealed class GameWindow : IDisposable
rm.Body.Velocity = rm.ServerVelocity;
}
}
+ else if (!IsPlayerGuid(serverGuid) && rm.ServerMoveToActive)
+ {
+ // We only parse enough of MoveTo to recover retail
+ // animation speed. Do not let apply_current_movement
+ // extrapolate position from an incomplete target
+ // solver; hold until the next UpdatePosition-derived
+ // velocity arrives.
+ rm.Body.Velocity = System.Numerics.Vector3.Zero;
+ }
else
{
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);