feat: converge grounded path segment completion

This commit is contained in:
BruceChen 2026-04-12 21:33:24 +08:00
parent 3b4e552d70
commit 6b449cc72a
12 changed files with 489 additions and 43 deletions

View file

@ -47,25 +47,11 @@ namespace MinecraftClient.Pathing.Execution.Templates
if (physics.OnGround && dy > 0.1)
input.Jump = true;
if (physics.OnGround && Math.Abs(dy) < 0.15)
if (physics.OnGround && Math.Abs(dy) < 0.2)
{
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldBack)
TemplateHelper.FaceSegmentHeading(physics, _segment);
if (_segment.ExitTransition == PathTransitionType.ContinueStraight && horizDistSq < 0.25)
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
if (_segment.ExitTransition != PathTransitionType.ContinueStraight
&& TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics, horizThresholdSq: 0.0025))
{
return TemplateState.Complete;
}
}
else if (horizDistSq < 0.25 && Math.Abs(dy) < 0.8)
{
return TemplateState.Complete;
}
double movedSq = TemplateHelper.HorizontalDistanceSq(pos, _lastPos);

View file

@ -59,26 +59,14 @@ namespace MinecraftClient.Pathing.Execution.Templates
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
if (physics.OnGround && Math.Abs(dy) < (_hasFallen ? 0.8 : 0.5))
if (physics.OnGround && Math.Abs(dy) < (_hasFallen ? 1.0 : 0.6))
{
if (horizDistSq > 0.01)
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldBack)
TemplateHelper.FaceSegmentHeading(physics, _segment);
if (_segment.ExitTransition == PathTransitionType.ContinueStraight)
{
double completionThreshold = _hasFallen ? 0.5 : 0.25;
if (horizDistSq < completionThreshold)
return TemplateState.Complete;
}
else if (TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics, horizThresholdSq: 0.0025))
{
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
}
}
else if (physics.OnClimbable)
{

View file

@ -0,0 +1,27 @@
using MinecraftClient.Mapping;
using MinecraftClient.Physics;
namespace MinecraftClient.Pathing.Execution.Templates
{
internal static class GroundedSegmentController
{
internal static void Apply(PathSegment segment, PathSegment? nextSegment, Location pos, PlayerPhysics physics, MovementInput input, World world)
{
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(segment, nextSegment, pos, physics, world);
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldBack)
TemplateHelper.FaceSegmentHeading(physics, segment);
}
internal static bool ShouldComplete(PathSegment segment, Location pos, PlayerPhysics physics)
{
return segment.ExitTransition switch
{
PathTransitionType.ContinueStraight => TemplateHelper.IsNear(pos, segment.End, horizThresholdSq: 0.09),
PathTransitionType.PrepareJump => TemplateHelper.HasReachedSegmentEndPlane(pos, segment)
&& TemplateHelper.ProjectHorizontalSpeedAlongSegment(physics, segment) > 0.02,
_ => physics.OnGround && TemplateHelper.IsSettledOnTargetBlock(pos, segment.End, physics)
};
}
}
}

View file

@ -0,0 +1,60 @@
using System;
using MinecraftClient.Mapping;
using MinecraftClient.Physics;
namespace MinecraftClient.Pathing.Execution.Templates
{
public static class TemplateFootingHelper
{
private const double HalfWidth = PhysicsConsts.PlayerWidth / 2.0;
public static bool IsFootprintInsideTargetBlock(Location pos, Location target, double epsilon = 1.0E-4)
{
double minX = pos.X - HalfWidth;
double maxX = pos.X + HalfWidth;
double minZ = pos.Z - HalfWidth;
double maxZ = pos.Z + HalfWidth;
double blockMinX = Math.Floor(target.X);
double blockMaxX = blockMinX + 1.0;
double blockMinZ = Math.Floor(target.Z);
double blockMaxZ = blockMinZ + 1.0;
return minX >= blockMinX - epsilon
&& maxX <= blockMaxX + epsilon
&& minZ >= blockMinZ - epsilon
&& maxZ <= blockMaxZ + epsilon;
}
public static bool WillLeaveTargetBlockNextTick(Location pos, PlayerPhysics physics, Location target, double epsilon = 1.0E-4)
{
Location nextPos = new(
pos.X + physics.DeltaMovement.X,
pos.Y,
pos.Z + physics.DeltaMovement.Z);
return !IsFootprintInsideTargetBlock(nextPos, target, epsilon);
}
public static bool WillCrossSupportExitNextTick(Location pos, PlayerPhysics physics, PathSegment segment, double epsilon = 1.0E-4)
{
double nextX = pos.X + physics.DeltaMovement.X;
double nextZ = pos.Z + physics.DeltaMovement.Z;
double blockMinX = Math.Floor(segment.End.X);
double blockMaxX = blockMinX + 1.0;
double blockMinZ = Math.Floor(segment.End.Z);
double blockMaxZ = blockMinZ + 1.0;
if (segment.HeadingX > 0 && nextX > blockMaxX - HalfWidth + epsilon)
return true;
if (segment.HeadingX < 0 && nextX < blockMinX + HalfWidth - epsilon)
return true;
if (segment.HeadingZ > 0 && nextZ > blockMaxZ - HalfWidth + epsilon)
return true;
if (segment.HeadingZ < 0 && nextZ < blockMinZ + HalfWidth - epsilon)
return true;
return false;
}
}
}

View file

@ -90,14 +90,57 @@ namespace MinecraftClient.Pathing.Execution.Templates
input.Back = decision.HoldBack;
}
internal static bool HasReachedSegmentEndPlane(Location pos, PathSegment segment, double tolerance = 0.05)
{
GetNormalizedSegmentDirection(segment, out double dirX, out double dirZ);
double relX = pos.X - segment.End.X;
double relZ = pos.Z - segment.End.Z;
return relX * dirX + relZ * dirZ >= -tolerance;
}
internal static double ProjectHorizontalSpeedAlongSegment(PlayerPhysics physics, PathSegment segment)
{
GetNormalizedSegmentDirection(segment, out double dirX, out double dirZ);
return physics.DeltaMovement.X * dirX + physics.DeltaMovement.Z * dirZ;
}
internal static bool IsSettledOnTargetBlock(Location pos, Location target, PlayerPhysics physics,
double speedThresholdSq = 0.0016)
{
double horizontalSpeedSq = physics.DeltaMovement.X * physics.DeltaMovement.X
+ physics.DeltaMovement.Z * physics.DeltaMovement.Z;
return TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, target)
&& !TemplateFootingHelper.WillLeaveTargetBlockNextTick(pos, physics, target)
&& horizontalSpeedSq <= speedThresholdSq;
}
internal static bool IsSettledAtEnd(Location pos, Location target, PlayerPhysics physics,
double horizThresholdSq = 0.0025, double speedThresholdSq = 0.0016)
{
if (IsSettledOnTargetBlock(pos, target, physics, speedThresholdSq))
return true;
double dx = target.X - pos.X;
double dz = target.Z - pos.Z;
double horizontalSpeedSq = physics.DeltaMovement.X * physics.DeltaMovement.X
+ physics.DeltaMovement.Z * physics.DeltaMovement.Z;
return dx * dx + dz * dz <= horizThresholdSq && horizontalSpeedSq <= speedThresholdSq;
}
private static void GetNormalizedSegmentDirection(PathSegment segment, out double dirX, out double dirZ)
{
dirX = segment.End.X - segment.Start.X;
dirZ = segment.End.Z - segment.Start.Z;
double len = Math.Sqrt(dirX * dirX + dirZ * dirZ);
if (len < 1.0E-6)
{
dirX = 0.0;
dirZ = 0.0;
return;
}
dirX /= len;
dirZ /= len;
}
}
}

View file

@ -40,22 +40,21 @@ namespace MinecraftClient.Pathing.Execution.Templates
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldBack)
TemplateHelper.FaceSegmentHeading(physics, _segment);
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
if (_segment.ExitTransition == PathTransitionType.ContinueStraight && TemplateHelper.IsNear(pos, ExpectedEnd, horizThresholdSq: 0.09))
return TemplateState.Complete;
if (_segment.ExitTransition != PathTransitionType.ContinueStraight && TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics))
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
double movedSq = TemplateHelper.HorizontalDistanceSq(pos, _lastPos);
_stuckTicks = movedSq < 0.0005 ? _stuckTicks + 1 : 0;
_lastPos = pos;
int maxTicks = _segment.ExitTransition == PathTransitionType.ContinueStraight ? 100 : 140;
int maxTicks = _segment.ExitTransition switch
{
PathTransitionType.ContinueStraight => 100,
PathTransitionType.PrepareJump => 80,
_ => 140
};
if (_stuckTicks > 40 || _tickCount > maxTicks)
return TemplateState.Failed;