From 4b92781d10f955dcf8e2f16e6871c6e47b8623de Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 23:42:25 +0800 Subject: [PATCH] fix: brake landing recovery before turns --- .../Execution/LivePathingRegressionTests.cs | 45 +++++++++++++++++++ .../TransitionBrakingPlannerTests.cs | 33 ++++++++++++++ .../Execution/TransitionBrakingPlanner.cs | 11 ++++- 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 MinecraftClient.Tests/Pathing/Execution/LivePathingRegressionTests.cs diff --git a/MinecraftClient.Tests/Pathing/Execution/LivePathingRegressionTests.cs b/MinecraftClient.Tests/Pathing/Execution/LivePathingRegressionTests.cs new file mode 100644 index 00000000..610d8e2d --- /dev/null +++ b/MinecraftClient.Tests/Pathing/Execution/LivePathingRegressionTests.cs @@ -0,0 +1,45 @@ +using MinecraftClient.Mapping; +using MinecraftClient.Pathing.Core; +using MinecraftClient.Pathing.Execution; +using MinecraftClient.Pathing.Execution.Templates; +using Xunit; + +namespace MinecraftClient.Tests.Pathing.Execution; + +public sealed class LivePathingRegressionTests +{ + [Fact] + public void SprintJumpTemplate_LandingRecoveryIntoTurn_CompletesInsideLandingBlock() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 126); + FlatWorldTestBuilder.ClearBox(world, 118, 79, 108, 126, 90, 112); + FlatWorldTestBuilder.SetSolid(world, 120, 79, 110); + FlatWorldTestBuilder.SetSolid(world, 122, 79, 110); + FlatWorldTestBuilder.SetSolid(world, 122, 79, 111); + FlatWorldTestBuilder.SetSolid(world, 120, 80, 111); + FlatWorldTestBuilder.SetSolid(world, 120, 81, 111); + + var segment = new PathSegment + { + Start = new Location(120.5, 80, 110.5), + End = new Location(122.5, 80, 110.5), + MoveType = MoveType.Parkour, + ExitTransition = PathTransitionType.LandingRecovery + }; + var next = new PathSegment + { + Start = new Location(122.5, 80, 110.5), + End = new Location(122.5, 80, 111.5), + MoveType = MoveType.Traverse, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new SprintJumpTemplate(segment, next); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 270f); + + TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 140, out Location finalPos); + + Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}"); + Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End), $"finalPos={finalPos} vel={physics.DeltaMovement}"); + } +} diff --git a/MinecraftClient.Tests/Pathing/Execution/TransitionBrakingPlannerTests.cs b/MinecraftClient.Tests/Pathing/Execution/TransitionBrakingPlannerTests.cs index 262c2c17..6d22b78e 100644 --- a/MinecraftClient.Tests/Pathing/Execution/TransitionBrakingPlannerTests.cs +++ b/MinecraftClient.Tests/Pathing/Execution/TransitionBrakingPlannerTests.cs @@ -96,6 +96,39 @@ public sealed class TransitionBrakingPlannerTests Assert.True(release); } + [Fact] + public void Plan_BackBrakes_ForLandingRecovery_WhenNextSegmentTurns() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 126); + FlatWorldTestBuilder.ClearBox(world, 118, 79, 108, 126, 90, 112); + FlatWorldTestBuilder.SetSolid(world, 120, 79, 110); + FlatWorldTestBuilder.SetSolid(world, 122, 79, 110); + FlatWorldTestBuilder.SetSolid(world, 122, 79, 111); + + var physics = CreatePhysics(0.118, 0.018, onGround: true); + var current = new PathSegment + { + Start = new Location(120.5, 80, 110.5), + End = new Location(122.5, 80, 110.5), + MoveType = MoveType.Parkour, + ExitTransition = PathTransitionType.LandingRecovery, + PreserveSprint = false + }; + var next = new PathSegment + { + Start = new Location(122.5, 80, 110.5), + End = new Location(122.5, 80, 111.5), + MoveType = MoveType.Traverse, + ExitTransition = PathTransitionType.FinalStop + }; + + TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(current, next, new Location(122.56, 80, 110.68), physics, world); + + Assert.False(decision.HoldForward); + Assert.False(decision.HoldSprint); + Assert.True(decision.HoldBack); + } + private static PlayerPhysics CreatePhysics(double deltaX, double deltaZ, bool onGround) { return new PlayerPhysics diff --git a/MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs b/MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs index 1899d389..89cab765 100644 --- a/MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs +++ b/MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs @@ -22,6 +22,9 @@ namespace MinecraftClient.Pathing.Execution double forwardSpeed = Math.Max(0.0, ProjectHorizontalSpeedAlongHeading(physics, current.HeadingX, current.HeadingZ)); double coastStopDistance = EstimateGroundStopDistance(physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: false); double hardBrakeDistance = EstimateGroundStopDistance(physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: true); + bool landingNeedsTurnBrake = current.ExitTransition == PathTransitionType.LandingRecovery + && next is not null + && !HasSameHeading(current, next); if (current.ExitTransition == PathTransitionType.FinalStop) { @@ -35,7 +38,8 @@ namespace MinecraftClient.Pathing.Execution return TransitionBrakingDecision.CarryMomentum(preserveSprint: false); } - if (current.ExitTransition == PathTransitionType.Turn && remaining <= hardBrakeDistance + TurnBrakeLead) + if ((current.ExitTransition == PathTransitionType.Turn || landingNeedsTurnBrake) + && remaining <= hardBrakeDistance + TurnBrakeLead) { return TransitionBrakingDecision.Brake; } @@ -103,5 +107,10 @@ namespace MinecraftClient.Pathing.Execution { return physics.DeltaMovement.X * headingX + physics.DeltaMovement.Z * headingZ; } + + private static bool HasSameHeading(PathSegment current, PathSegment next) + { + return current.HeadingX == next.HeadingX && current.HeadingZ == next.HeadingZ; + } } }