From 8408a413981e91fd0cc18916f1ab263f772ed198 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Wed, 15 Apr 2026 16:37:12 +0000 Subject: [PATCH] pathing: snap yaw for jump-ready grounded handoffs --- .../GroundedTemplateConvergenceTests.cs | 446 ++++++++++++++++++ .../Execution/Templates/AscendTemplate.cs | 36 +- .../Templates/GroundedSegmentController.cs | 45 +- 3 files changed, 519 insertions(+), 8 deletions(-) diff --git a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs index 88f64b96..a9b3fdb5 100644 --- a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs +++ b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs @@ -115,6 +115,452 @@ public sealed class GroundedTemplateConvergenceTests Assert.True(physics.DeltaMovement.X > 0.02); } + [Fact] + public void WalkTemplate_PrepareJump_WithPlannerHints_CompletesOnRunUpBlock() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(); + var current = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(1.5, 80, 0.5), + MoveType = MoveType.Traverse, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 0, 0.10, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(1.5, 80, 0.5), + End = new Location(3.5, 80, 0.5), + MoveType = MoveType.Parkour, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new WalkTemplate(current, next); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(current.Start, yaw: 270f); + + TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 120, out Location finalPos); + + Assert.Equal(TemplateState.Complete, state); + Assert.True( + TemplateFootingHelper.IsCenterInsideTargetBlock(finalPos, current.End), + $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}"); + } + + [Fact] + public void AscendTemplate_DiagonalPrepareJump_WithPlannerHints_CompletesOnRunUpBlock() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: -1, max: 6); + FlatWorldTestBuilder.ClearBox(world, -1, 79, -1, 6, 84, 6); + FlatWorldTestBuilder.SetSolid(world, 0, 79, 0); + FlatWorldTestBuilder.SetSolid(world, 1, 80, 1); + FlatWorldTestBuilder.SetSolid(world, 3, 80, 3); + + var current = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(1.5, 81, 1.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 1, 0.10, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(1.5, 81, 1.5), + End = new Location(3.5, 81, 3.5), + MoveType = MoveType.Parkour, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new AscendTemplate(current, next); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(current.Start, yaw: 315f); + var input = new MovementInput(); + var trace = new List(); + TemplateState state = TemplateState.InProgress; + Location finalPos = current.Start; + for (int tick = 0; tick < 140; tick++) + { + input.Reset(); + Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + state = template.Tick(pos, physics, input, world); + if (tick < 20 || state != TemplateState.InProgress) + { + trace.Add( + $"tick={tick} state={state} pos={pos} yaw={physics.Yaw:F1} vel={physics.DeltaMovement} " + + $"onGround={physics.OnGround} input(F={input.Forward},J={input.Jump},S={input.Sprint})"); + } + + if (state != TemplateState.InProgress) + { + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + break; + } + + physics.ApplyInput(input); + physics.Tick(world); + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + } + + Assert.True( + state == TemplateState.Complete, + $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + Assert.True( + TemplateFootingHelper.IsCenterInsideTargetBlock(finalPos, current.End), + $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + } + + [Fact] + public void AscendTemplate_AfterTraversePrepareJump_CompletesWithinTwentyTicks() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: -1, max: 6); + FlatWorldTestBuilder.ClearBox(world, -1, 79, -1, 6, 84, 2); + FlatWorldTestBuilder.SetSolid(world, 0, 79, 0); + FlatWorldTestBuilder.SetSolid(world, 1, 79, 0); + FlatWorldTestBuilder.SetSolid(world, 2, 80, 0); + FlatWorldTestBuilder.SetSolid(world, 3, 81, 0); + + var traverse = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(1.5, 80, 0.5), + MoveType = MoveType.Traverse, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 0, 0.0, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var ascend = new PathSegment + { + Start = new Location(1.5, 80, 0.5), + End = new Location(2.5, 81, 0.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 0, 0.10, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(2.5, 81, 0.5), + End = new Location(3.5, 82, 0.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.FinalStop + }; + + var physics = TemplateSimulationRunner.CreateGroundedPhysics(traverse.Start, yaw: 270f); + + TemplateState traverseState = TemplateSimulationRunner.Run(new WalkTemplate(traverse, ascend), physics, world, maxTicks: 80, out _); + + var template = new AscendTemplate(ascend, next); + var input = new MovementInput(); + TemplateState state = TemplateState.InProgress; + int elapsedTicks = 0; + Location finalPos = ascend.Start; + for (; elapsedTicks < 80; elapsedTicks++) + { + input.Reset(); + Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + state = template.Tick(pos, physics, input, world); + if (state != TemplateState.InProgress) + { + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + break; + } + + physics.ApplyInput(input); + physics.Tick(world); + } + + Assert.Equal(TemplateState.Complete, traverseState); + Assert.Equal(TemplateState.Complete, state); + Assert.True(elapsedTicks <= 20, $"elapsedTicks={elapsedTicks} finalPos={finalPos} vel={physics.DeltaMovement}"); + } + + [Fact] + public void AscendTemplate_PrepareJump_CompletesFromOppositeYawWithinTwentyTicks() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: 338, max: 344); + FlatWorldTestBuilder.ClearBox(world, 340, 80, 338, 344, 84, 342); + FlatWorldTestBuilder.FillSolid(world, 341, 80, 339, 341, 80, 341); + FlatWorldTestBuilder.FillSolid(world, 342, 81, 339, 342, 81, 341); + + var segment = new PathSegment + { + Start = new Location(340.5, 80, 340.5), + End = new Location(341.5, 81, 340.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 0, 0.10, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(341.5, 81, 340.5), + End = new Location(342.5, 82, 340.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new AscendTemplate(segment, next); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 90f); + var input = new MovementInput(); + TemplateState state = TemplateState.InProgress; + int elapsedTicks = 0; + Location finalPos = segment.Start; + var trace = new List(); + for (; elapsedTicks < 80; elapsedTicks++) + { + input.Reset(); + Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + state = template.Tick(pos, physics, input, world); + if (elapsedTicks < 20 || state != TemplateState.InProgress) + { + trace.Add( + $"tick={elapsedTicks} state={state} pos={pos} yaw={physics.Yaw:F1} vel={physics.DeltaMovement} " + + $"onGround={physics.OnGround} input(F={input.Forward},J={input.Jump},S={input.Sprint})"); + } + if (state != TemplateState.InProgress) + { + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + break; + } + + physics.ApplyInput(input); + physics.Tick(world); + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + } + + Assert.Equal(TemplateState.Complete, state); + Assert.True(elapsedTicks <= 20, $"elapsedTicks={elapsedTicks} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + Assert.True( + TemplateFootingHelper.IsCenterInsideTargetBlock(finalPos, segment.End), + $"elapsedTicks={elapsedTicks} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + } + + [Fact] + public void AscendTemplate_PrepareJump_SnapsYawImmediatelyFromOppositeYaw() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: 338, max: 344); + FlatWorldTestBuilder.ClearBox(world, 340, 80, 338, 344, 84, 342); + FlatWorldTestBuilder.FillSolid(world, 341, 80, 339, 341, 80, 341); + FlatWorldTestBuilder.FillSolid(world, 342, 81, 339, 342, 81, 341); + + var segment = new PathSegment + { + Start = new Location(340.5, 80, 340.5), + End = new Location(341.5, 81, 340.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 0, 0.10, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(341.5, 81, 340.5), + End = new Location(342.5, 82, 340.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new AscendTemplate(segment, next); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 90f); + var input = new MovementInput(); + + TemplateState state = template.Tick(segment.Start, physics, input, world); + + Assert.Equal(TemplateState.InProgress, state); + Assert.InRange(physics.Yaw, 269.9f, 270.1f); + Assert.True(input.Forward); + Assert.True(input.Sprint); + Assert.True(input.Jump); + } + + [Fact] + public void WalkTemplate_PrepareJump_FreezeForTurn_SnapsExitHeadingImmediately() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(); + var current = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(1.5, 80, 0.5), + MoveType = MoveType.Traverse, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(0, 1, 0.10, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(1.5, 80, 0.5), + End = new Location(1.5, 80, 1.5), + MoveType = MoveType.Parkour, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new WalkTemplate(current, next); + var physics = new PlayerPhysics + { + Position = new Vec3d(1.5, 80.0, 0.5), + DeltaMovement = Vec3d.Zero, + OnGround = true, + MovementSpeed = 0.1f, + Yaw = 180f, + Pitch = 0f + }; + var input = new MovementInput(); + + TemplateState state = template.Tick(new Location(1.5, 80, 0.5), physics, input, world); + + Assert.Equal(TemplateState.Complete, state); + Assert.InRange(physics.Yaw, -0.1f, 0.1f); + Assert.False(input.Forward); + Assert.False(input.Sprint); + Assert.False(input.Back); + } + + [Fact] + public void AscendTemplate_PrepareJump_CompletesFromOffCenterRunUpState() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: 338, max: 344); + FlatWorldTestBuilder.ClearBox(world, 340, 80, 338, 344, 84, 342); + FlatWorldTestBuilder.FillSolid(world, 341, 80, 339, 341, 80, 341); + FlatWorldTestBuilder.FillSolid(world, 342, 81, 339, 342, 81, 341); + + var segment = new PathSegment + { + Start = new Location(340.5, 80, 340.5), + End = new Location(341.5, 81, 340.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 0, 0.10, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(341.5, 81, 340.5), + End = new Location(342.5, 82, 340.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new AscendTemplate(segment, next); + var physics = new PlayerPhysics + { + Position = new Vec3d(340.25, 80.0, 340.25), + DeltaMovement = Vec3d.Zero, + OnGround = true, + MovementSpeed = 0.1f, + Yaw = 270f, + Pitch = 0f + }; + var input = new MovementInput(); + TemplateState state = TemplateState.InProgress; + Location finalPos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + var trace = new List(); + for (int tick = 0; tick < 80; tick++) + { + input.Reset(); + Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + state = template.Tick(pos, physics, input, world); + if (tick < 20 || state != TemplateState.InProgress) + { + trace.Add( + $"tick={tick} state={state} pos={pos} yaw={physics.Yaw:F1} vel={physics.DeltaMovement} " + + $"onGround={physics.OnGround} input(F={input.Forward},J={input.Jump},S={input.Sprint})"); + } + + if (state != TemplateState.InProgress) + { + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + break; + } + + physics.ApplyInput(input); + physics.Tick(world); + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + } + + Assert.True( + state == TemplateState.Complete, + $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + Assert.True( + TemplateFootingHelper.IsCenterInsideTargetBlock(finalPos, segment.End), + $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + } + + [Fact] + public void WalkTemplate_DiagonalPrepareJumpIntoAscend_CompletesFromTargetBlockEntry() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: 418, max: 426); + FlatWorldTestBuilder.ClearBox(world, 418, 79, 418, 426, 84, 424); + FlatWorldTestBuilder.SetSolid(world, 420, 79, 420); + FlatWorldTestBuilder.SetSolid(world, 421, 79, 421); + FlatWorldTestBuilder.SetSolid(world, 422, 79, 422); + FlatWorldTestBuilder.SetSolid(world, 423, 80, 422); + FlatWorldTestBuilder.SetSolid(world, 424, 81, 422); + + var current = new PathSegment + { + Start = new Location(421.5, 80, 421.5), + End = new Location(422.5, 80, 422.5), + MoveType = MoveType.Diagonal, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 0, 0.0, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(422.5, 80, 422.5), + End = new Location(423.5, 81, 422.5), + MoveType = MoveType.Ascend, + ExitTransition = PathTransitionType.PrepareJump, + ExitHints = new PathTransitionHints(1, 0, 0.0, double.PositiveInfinity, false, true, true, false, 10), + PreserveSprint = true + }; + + var template = new WalkTemplate(current, next); + var physics = new PlayerPhysics + { + Position = new Vec3d(422.25, 80.0, 422.25), + DeltaMovement = Vec3d.Zero, + OnGround = true, + MovementSpeed = 0.1f, + Yaw = 315f, + Pitch = 0f + }; + var input = new MovementInput(); + TemplateState state = TemplateState.InProgress; + Location finalPos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + var trace = new List(); + for (int tick = 0; tick < 80; tick++) + { + input.Reset(); + Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + state = template.Tick(pos, physics, input, world); + if (tick < 20 || state != TemplateState.InProgress) + { + trace.Add( + $"tick={tick} state={state} pos={pos} yaw={physics.Yaw:F1} vel={physics.DeltaMovement} " + + $"onGround={physics.OnGround} input(F={input.Forward},B={input.Back},S={input.Sprint})"); + } + + if (state != TemplateState.InProgress) + { + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + break; + } + + physics.ApplyInput(input); + physics.Tick(world); + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + } + + Assert.True( + state == TemplateState.Complete, + $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + Assert.True( + TemplateFootingHelper.IsCenterInsideTargetBlock(finalPos, current.End), + $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}"); + } + [Fact] public void WalkTemplate_TurnIntoParkour_CompletesOnlyWhenTurnEntryIsSlowAndJumpReady() { diff --git a/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs index a9126e8e..6117ebd4 100644 --- a/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs +++ b/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs @@ -18,6 +18,7 @@ namespace MinecraftClient.Pathing.Execution.Templates private int _tickCount; private Location _lastPos; private int _stuckTicks; + private bool _initiatedJump; public AscendTemplate(PathSegment segment, PathSegment? nextSegment) { @@ -37,15 +38,34 @@ namespace MinecraftClient.Pathing.Execution.Templates double dy = ExpectedEnd.Y - pos.Y; double horizDistSq = dx * dx + dz * dz; + bool groundedPrepareJumpHandoff = physics.OnGround + && Math.Abs(dy) < 0.2 + && _segment.ExitTransition == PathTransitionType.PrepareJump + && _segment.ExitHints.RequireJumpReady + && TemplateFootingHelper.IsCenterInsideTargetBlock(pos, _segment.End); + float targetYaw = TemplateHelper.CalculateYaw(dx, dz); float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz); - physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw); + bool snapYawForJumpCommit = !_initiatedJump && !groundedPrepareJumpHandoff; + physics.Yaw = TemplateHelper.AlignYaw( + physics.Yaw, + targetYaw, + snapYawForJumpCommit ? YawAlignmentMode.Snap : YawAlignmentMode.Smooth); physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch); - input.Forward = true; - input.Sprint = true; + float headingPenalty = YawDifference(physics.Yaw, targetYaw); + bool headingReady = headingPenalty <= 8.0; + bool turnInPlace = !_initiatedJump && !headingReady; + input.Forward = !turnInPlace; + input.Sprint = !turnInPlace; - if (physics.OnGround && dy > 0.1) + bool diagonalAscend = _segment.HeadingX != 0 && _segment.HeadingZ != 0; + bool jumpReady = headingReady + && (diagonalAscend || TemplateHelper.RemainingDistanceAlongSegment(pos, _segment) <= 1.05); + if (physics.OnGround && dy > 0.1 && jumpReady) + { input.Jump = true; + _initiatedJump = true; + } if (physics.OnGround && Math.Abs(dy) < 0.2) { @@ -64,5 +84,13 @@ namespace MinecraftClient.Pathing.Execution.Templates return TemplateState.InProgress; } + + private static float YawDifference(float current, float target) + { + float delta = target - current; + while (delta > 180f) delta -= 360f; + while (delta < -180f) delta += 360f; + return Math.Abs(delta); + } } } diff --git a/MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs b/MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs index 62a642b4..f1cb0723 100644 --- a/MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs +++ b/MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs @@ -1,4 +1,5 @@ using MinecraftClient.Mapping; +using MinecraftClient.Pathing.Core; using MinecraftClient.Physics; namespace MinecraftClient.Pathing.Execution.Templates @@ -9,6 +10,20 @@ namespace MinecraftClient.Pathing.Execution.Templates internal static void Apply(PathSegment segment, PathSegment? nextSegment, Location pos, PlayerPhysics physics, MovementInput input, World world) { + if (segment.ExitTransition == PathTransitionType.PrepareJump + && segment.ExitHints.RequireJumpReady + && physics.OnGround + && TemplateFootingHelper.IsCenterInsideTargetBlock(pos, segment.End) + && IsReadyToFreezeForTurn(segment, pos) + && TemplateHelper.HeadingPenaltyDegrees(physics.Yaw, segment) > 8.0) + { + input.Forward = false; + input.Sprint = false; + input.Back = false; + TemplateHelper.FaceExitHeading(physics, segment, YawAlignmentMode.Snap); + return; + } + if (TemplateHelper.ShouldBiasTowardExitHeading(pos, segment)) TemplateHelper.FaceExitHeading(physics, segment); @@ -49,11 +64,19 @@ namespace MinecraftClient.Pathing.Execution.Templates if (segment.ExitTransition == PathTransitionType.PrepareJump && physics.OnGround && segment.ExitHints.RequireJumpReady - && segment.ExitHints.MinExitSpeed <= 0.0 - && TemplateFootingHelper.IsCenterInsideTargetBlock(pos, segment.End) - && TemplateHelper.RemainingDistanceAlongSegment(pos, segment) <= 0.30) + && TemplateFootingHelper.IsCenterInsideTargetBlock(pos, segment.End)) { - return true; + if (segment.MoveType == MoveType.Ascend) + return true; + + if (segment.MoveType == MoveType.Parkour + || (segment.HeadingX != 0 && segment.HeadingZ != 0)) + { + return TemplateHelper.RemainingDistanceAlongSegment(pos, segment) <= 0.30; + } + + if (segment.MoveType is not MoveType.Parkour) + return true; } if (exitSpeed < segment.ExitHints.MinExitSpeed) @@ -86,5 +109,19 @@ namespace MinecraftClient.Pathing.Execution.Templates _ => physics.OnGround && TemplateHelper.IsSettledOnTargetBlock(pos, segment.End, physics) }; } + + private static bool IsReadyToFreezeForTurn(PathSegment segment, Location pos) + { + if (segment.MoveType == MoveType.Ascend) + return true; + + if (segment.MoveType == MoveType.Parkour + || (segment.HeadingX != 0 && segment.HeadingZ != 0)) + { + return TemplateHelper.RemainingDistanceAlongSegment(pos, segment) <= 0.30; + } + + return true; + } } }