From 6e4cf4a10e0f0532841249d18e47d3037fd0271c Mon Sep 17 00:00:00 2001 From: BruceChen Date: Mon, 13 Apr 2026 00:11:12 +0800 Subject: [PATCH] fix: stabilize descend landings after braking --- .../GroundedTemplateConvergenceTests.cs | 54 +++++++++++++++++++ .../Execution/Templates/DescendTemplate.cs | 50 +++++++++++++++-- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs index f56e9fc3..24f9cade 100644 --- a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs +++ b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs @@ -2,6 +2,7 @@ using MinecraftClient.Mapping; using MinecraftClient.Pathing.Core; using MinecraftClient.Pathing.Execution; using MinecraftClient.Pathing.Execution.Templates; +using MinecraftClient.Physics; using Xunit; namespace MinecraftClient.Tests.Pathing.Execution; @@ -81,4 +82,57 @@ public sealed class GroundedTemplateConvergenceTests Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}"); Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End)); } + + [Fact] + public void DescendTemplate_FinalStop_WithWallAndMisalignedYaw_CompletesOnLandingBlock() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: 198, max: 204); + FlatWorldTestBuilder.ClearBox(world, 198, 79, 198, 204, 84, 202); + FlatWorldTestBuilder.FillSolid(world, 201, 79, 199, 203, 79, 201); + FlatWorldTestBuilder.SetSolid(world, 200, 80, 200); + FlatWorldTestBuilder.SetSolid(world, 200, 80, 199); + FlatWorldTestBuilder.SetSolid(world, 201, 80, 199); + FlatWorldTestBuilder.SetSolid(world, 202, 80, 199); + FlatWorldTestBuilder.SetSolid(world, 201, 81, 199); + FlatWorldTestBuilder.SetSolid(world, 202, 81, 199); + + var segment = new PathSegment + { + Start = new Location(200.5, 81, 200.5), + End = new Location(201.5, 80, 200.5), + MoveType = MoveType.Descend, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new DescendTemplate(segment, null); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 0f); + + var input = new MovementInput(); + var trace = new List(); + TemplateState state = TemplateState.InProgress; + Location finalPos = segment.Start; + for (int tick = 0; tick < 240; 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 || !physics.OnGround) + { + trace.Add($"tick={tick} state={state} pos={pos} 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.IsFootprintInsideTargetBlock(finalPos, segment.End)); + } } diff --git a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs index e88e91be..5337decc 100644 --- a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs +++ b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs @@ -1,5 +1,6 @@ using System; using MinecraftClient.Mapping; +using MinecraftClient.Pathing.Execution; using MinecraftClient.Physics; namespace MinecraftClient.Pathing.Execution.Templates @@ -12,6 +13,8 @@ namespace MinecraftClient.Pathing.Execution.Templates /// public sealed class DescendTemplate : IActionTemplate { + private const float PreDropYawToleranceDeg = 12f; + public Location ExpectedStart { get; } public Location ExpectedEnd { get; } @@ -61,10 +64,14 @@ namespace MinecraftClient.Pathing.Execution.Templates if (physics.OnGround && Math.Abs(dy) < (_hasFallen ? 1.0 : 0.6)) { - if (horizDistSq > 0.01) + TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world); + if (horizDistSq > 0.01 && !decision.HoldBack) physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw); - GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world); + TemplateHelper.ApplyDecision(input, decision); + if (decision.HoldBack) + TemplateHelper.FaceSegmentHeading(physics, _segment); + if (GroundedSegmentController.ShouldComplete(_segment, pos, physics)) return TemplateState.Complete; } @@ -79,12 +86,45 @@ namespace MinecraftClient.Pathing.Execution.Templates else if (horizDistSq > 0.01) { physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw); - input.Forward = true; - if (_needsSprint) - input.Sprint = true; + if (_hasFallen || YawDifference(physics.Yaw, targetYaw) <= PreDropYawToleranceDeg) + { + if (!_hasFallen && !_needsSprint && ShouldCoastOffLedge(pos)) + { + // For short descends into a stop or turn, release forward near the lip + // so the landing stays on the intended support instead of overshooting it. + } + else if (!_hasFallen && !_needsSprint) + { + GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world); + } + else + { + input.Forward = true; + if (_needsSprint) + input.Sprint = true; + } + } } return TemplateState.InProgress; } + + private bool ShouldCoastOffLedge(Location pos) + { + if (_segment.ExitTransition == PathTransitionType.ContinueStraight) + return false; + + double remaining = (_segment.End.X - pos.X) * _segment.HeadingX + + (_segment.End.Z - pos.Z) * _segment.HeadingZ; + return remaining <= 0.55; + } + + 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); + } } }