fix: stabilize descend landings after braking

This commit is contained in:
BruceChen 2026-04-13 00:11:12 +08:00
parent 33ff02042c
commit 6e4cf4a10e
2 changed files with 99 additions and 5 deletions

View file

@ -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<string>();
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));
}
}

View file

@ -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
/// </summary>
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);
}
}
}