diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 1633f4a..9ce420c 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -5226,6 +5226,23 @@ public sealed class GameWindow : IDisposable
// updated orientation rotates that into the right
// world direction toward the target.
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
+
+ // Clamp horizontal velocity so we don't overshoot
+ // the arrival threshold during the final tick of
+ // approach. Without this, a 4 m/s body advances
+ // ~6 cm/tick and visibly runs slightly through
+ // the target before the swing UM lands.
+ float arrivalThreshold = rm.MoveToMoveTowards
+ ? rm.MoveToDistanceToObject
+ : rm.MoveToMinDistance;
+ rm.Body.Velocity = AcDream.Core.Physics.RemoteMoveToDriver
+ .ClampApproachVelocity(
+ rm.Body.Position,
+ rm.Body.Velocity,
+ rm.MoveToDestinationWorld,
+ arrivalThreshold,
+ (float)dt,
+ rm.MoveToMoveTowards);
}
}
}
diff --git a/src/AcDream.Core/Physics/RemoteMoveToDriver.cs b/src/AcDream.Core/Physics/RemoteMoveToDriver.cs
index 0981666..90a0388 100644
--- a/src/AcDream.Core/Physics/RemoteMoveToDriver.cs
+++ b/src/AcDream.Core/Physics/RemoteMoveToDriver.cs
@@ -240,6 +240,58 @@ public static class RemoteMoveToDriver
originZ);
}
+ ///
+ /// Cap horizontal velocity so the body lands exactly at
+ /// rather than overshooting past
+ /// it during the final tick of approach. Without this clamp, a body
+ /// running at RunAnimSpeed × speedMod ≈ 4 m/s can overshoot
+ /// the 0.6 m arrival window by up to one tick's advance (~6 cm at
+ /// 60 fps) — visible as the creature "running slightly through" the
+ /// player it's about to attack (user-reported 2026-04-28).
+ ///
+ ///
+ /// The clamp is a strict scale-down of the horizontal component
+ /// (X/Y); the vertical component (Z) is left to gravity / terrain
+ /// handling. false (flee branch) is a
+ /// no-op since fleeing has no overshoot risk — the body wants to
+ /// move AWAY from the destination.
+ ///
+ ///
+ public static Vector3 ClampApproachVelocity(
+ Vector3 bodyPosition,
+ Vector3 currentVelocity,
+ Vector3 destinationWorld,
+ float arrivalThreshold,
+ float dt,
+ bool moveTowards)
+ {
+ if (!moveTowards || dt <= 0f) return currentVelocity;
+
+ float dx = destinationWorld.X - bodyPosition.X;
+ float dy = destinationWorld.Y - bodyPosition.Y;
+ float dist = MathF.Sqrt(dx * dx + dy * dy);
+ float remaining = MathF.Max(0f, dist - arrivalThreshold);
+
+ float vxy = MathF.Sqrt(currentVelocity.X * currentVelocity.X
+ + currentVelocity.Y * currentVelocity.Y);
+ if (vxy < 1e-3f) return currentVelocity;
+
+ float advance = vxy * dt;
+ if (advance <= remaining) return currentVelocity;
+
+ // Already inside or right at the threshold: zero horizontal
+ // velocity, keep Z. (The arrival predicate in Drive() should
+ // have fired this tick, but this is the belt-and-braces guard.)
+ if (remaining < 1e-3f)
+ return new Vector3(0f, 0f, currentVelocity.Z);
+
+ float scale = remaining / advance;
+ return new Vector3(
+ currentVelocity.X * scale,
+ currentVelocity.Y * scale,
+ currentVelocity.Z);
+ }
+
/// Wrap an angle in radians to [-π, π].
private static float WrapPi(float r)
{
diff --git a/tests/AcDream.Core.Tests/Physics/RemoteMoveToDriverTests.cs b/tests/AcDream.Core.Tests/Physics/RemoteMoveToDriverTests.cs
index ece3f9b..39182cb 100644
--- a/tests/AcDream.Core.Tests/Physics/RemoteMoveToDriverTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/RemoteMoveToDriverTests.cs
@@ -205,6 +205,78 @@ public class RemoteMoveToDriverTests
Assert.Equal(bodyRot, newOrient);
}
+ [Fact]
+ public void ClampApproachVelocity_NoOverShoot_LandsExactlyAtThreshold()
+ {
+ // Body 1 m from destination, running at 4 m/s, dt = 0.1 s.
+ // Naive advance = 0.4 m → would end at 0.6 m from dest, exactly
+ // on the threshold. With threshold=0.6 and remaining=0.4, the
+ // clamp should let the full velocity through (advance == remaining).
+ var bodyPos = new Vector3(0f, 0f, 0f);
+ var dest = new Vector3(0f, 1f, 0f);
+ var vel = new Vector3(0f, 4f, 0f);
+
+ var clamped = RemoteMoveToDriver.ClampApproachVelocity(
+ bodyPos, vel, dest, arrivalThreshold: 0.6f, dt: 0.1f, moveTowards: true);
+
+ // Within float-precision: 4 m/s × 0.1 s = 0.4 m, exactly the
+ // remaining distance. The clamp may apply a 0.99999×-style
+ // tiny scale due to FP rounding — accept anything ≥ 99.9% of
+ // the input as "no meaningful overshoot prevention applied."
+ Assert.InRange(clamped.Y, 4f * 0.999f, 4f);
+ Assert.Equal(0f, clamped.X);
+ Assert.Equal(0f, clamped.Z);
+ }
+
+ [Fact]
+ public void ClampApproachVelocity_WouldOverShoot_ScalesDownToExactLanding()
+ {
+ // Body 1 m from destination, running at 4 m/s, dt = 0.2 s.
+ // Naive advance = 0.8 m → would overshoot 0.6 m threshold by 0.4 m.
+ // remaining = 0.4 m, advance = 0.8 m → scale = 0.5.
+ // Velocity should be halved → 2 m/s.
+ var bodyPos = new Vector3(0f, 0f, 0f);
+ var dest = new Vector3(0f, 1f, 0f);
+ var vel = new Vector3(0f, 4f, 0f);
+
+ var clamped = RemoteMoveToDriver.ClampApproachVelocity(
+ bodyPos, vel, dest, arrivalThreshold: 0.6f, dt: 0.2f, moveTowards: true);
+
+ Assert.InRange(clamped.Y, 2f - Epsilon, 2f + Epsilon);
+ Assert.Equal(0f, clamped.X);
+ }
+
+ [Fact]
+ public void ClampApproachVelocity_AlreadyAtThreshold_ZeroesHorizontal()
+ {
+ // Body exactly 0.6 m from dest with threshold 0.6 → remaining ≈ 0.
+ // Any horizontal velocity would overshoot; clamp must zero it.
+ var bodyPos = new Vector3(0f, 0f, 0f);
+ var dest = new Vector3(0f, 0.6f, 0f);
+ var vel = new Vector3(0f, 4f, 0.5f); // some Z to confirm Z is preserved
+
+ var clamped = RemoteMoveToDriver.ClampApproachVelocity(
+ bodyPos, vel, dest, arrivalThreshold: 0.6f, dt: 0.016f, moveTowards: true);
+
+ Assert.Equal(0f, clamped.X);
+ Assert.Equal(0f, clamped.Y);
+ Assert.Equal(0.5f, clamped.Z); // gravity / Z handling unaffected
+ }
+
+ [Fact]
+ public void ClampApproachVelocity_FleeBranch_NoOp()
+ {
+ // moveTowards=false (flee): no overshoot risk, return velocity unchanged.
+ var bodyPos = Vector3.Zero;
+ var dest = new Vector3(0f, 1f, 0f);
+ var vel = new Vector3(0f, -4f, 0f);
+
+ var clamped = RemoteMoveToDriver.ClampApproachVelocity(
+ bodyPos, vel, dest, arrivalThreshold: 5f, dt: 0.5f, moveTowards: false);
+
+ Assert.Equal(vel, clamped);
+ }
+
[Fact]
public void OriginToWorld_AppliesLandblockGridShift()
{