Minecraft-Console-Client/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs
BruceChen c1831b2730 pathing: lock Descend air-yaw and shortcut Ascend->Turn landing
Two coupled fixes for live 258<->237 / 237<->244 routes that previously
hit 5 replans and gave up on Ascend exit=Turn segments.

DescendTemplate: when biasTowardExitInAir is false on a multi-block
diagonal descend, the per-tick targetYaw rotates as the bot drifts past
the landing column mid-fall (e.g. dx=-1 dz=-1 drop yaw cycles 135 -> 90
-> 0 -> 315 over six air ticks). With Forward held, that rotating yaw
pushes air-control momentum perpendicular to the planned trajectory,
sliding the bot ~0.5 m off the landing onto an adjacent block one tier
below. Lock airborne yaw to the segment's start->end heading for those
descends so air drift stays aligned with the diagonal.

AscendTemplate: add a post-landing completion shortcut for non-FinalStop
exits. Once the jump arc puts the bot back on ground at the target's
elevation with its center inside the target column, hand off to the
next template (which snaps yaw on its first tick). Holding the segment
runs both AscendTemplate's top-level yaw smoothing toward a moving
targetYaw AND GroundedSegmentController's segment/exit-heading rotation
each tick. The competing yaw targets oscillate the bot ~80 ticks until
it walks off the 1-block landing's edge and the segment fails. Mirrors
the existing PrepareJump completion gate. FinalStop is excluded so the
last segment still uses IsSettledAtEnd to detect a true stop.

Live verification on 1.21.11 (round-trip 244<->237 plus the originally
failing 237->244 route): four navigations, 0 replans, all 25/41/24/41
segments completed.

Made-with: Cursor
2026-04-27 13:30:14 +00:00

255 lines
12 KiB
C#

using System;
using MinecraftClient.Mapping;
using MinecraftClient.Pathing.Execution;
using MinecraftClient.Physics;
namespace MinecraftClient.Pathing.Execution.Templates
{
/// <summary>
/// Walk off a ledge and drop 1-N blocks to a landing spot.
/// Walks toward the destination; gravity handles the fall.
/// Sprints when the horizontal distance is large (> 1.5 blocks).
/// Supports solid landings, water landings, and mid-fall vine/ladder grabs.
/// </summary>
public sealed class DescendTemplate : IActionTemplate
{
private const float PreDropYawToleranceDeg = 12f;
public Location ExpectedStart { get; }
public Location ExpectedEnd { get; }
private readonly PathSegment _segment;
private readonly PathSegment? _nextSegment;
private int _tickCount;
private bool _hasFallen;
private readonly bool _needsSprint;
public DescendTemplate(PathSegment segment, PathSegment? nextSegment)
{
_segment = segment;
_nextSegment = nextSegment;
ExpectedStart = segment.Start;
ExpectedEnd = segment.End;
double hdx = segment.End.X - segment.Start.X;
double hdz = segment.End.Z - segment.Start.Z;
_needsSprint = (hdx * hdx + hdz * hdz) > 2.25;
}
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
{
_tickCount++;
double dx = ExpectedEnd.X - pos.X;
double dz = ExpectedEnd.Z - pos.Z;
double dy = ExpectedEnd.Y - pos.Y;
double horizDistSq = dx * dx + dz * dz;
if (!physics.OnGround)
_hasFallen = true;
// Completion: landed in water near destination
if (_hasFallen && physics.InWater && horizDistSq < 0.5 && Math.Abs(dy) < 2.0)
return TemplateState.Complete;
// Fail if climbing up instead of descending
if (pos.Y > ExpectedStart.Y + 2.0)
return TemplateState.Failed;
if (_tickCount > 200)
return TemplateState.Failed;
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
// Snap yaw on the first tick to avoid a few ticks of sideways drift
// when the bot enters this segment with a stale orientation (e.g.
// just after a teleport or after a turn). Ledge-adjacent descends
// cannot tolerate drift without falling off the wrong side.
if (_tickCount == 1)
physics.Yaw = targetYaw;
if (physics.OnGround && Math.Abs(dy) < (_hasFallen ? 1.0 : 0.6))
{
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
bool onOrPastTarget = TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd)
|| TemplateHelper.HasReachedSegmentEndPlane(pos, _segment);
// Fallback: after a diagonal descend landing the bot can end
// up on a support block that is not yet the target block
// (footprint still off the landing column). The braking
// planner reads "remaining <= coastStop + lead" and returns
// Coast, which zeroes every input - if the bot has already
// come to rest this means the segment hangs forever and the
// pathing manager replans. When we are stopped, not inside
// the target block, and not being asked to brake, walk
// toward the target instead of coasting so the landing
// resolves in one tick-window.
if (!decision.HoldBack
&& !TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd)
&& horizDistSq > 0.01
&& TemplateHelper.GetHorizontalSpeed(physics) < 0.03)
{
float walkYaw = TemplateHelper.CalculateYaw(dx, dz);
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, walkYaw);
input.Forward = true;
input.Sprint = _needsSprint;
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
return TemplateState.InProgress;
}
if (horizDistSq > 0.01 && !decision.HoldBack)
{
float groundedYaw = onOrPastTarget
? TemplateHelper.GetExitHeadingYaw(_segment)
: TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment)
? TemplateHelper.GetExitHeadingYaw(_segment)
: targetYaw;
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, groundedYaw);
}
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldBack)
TemplateHelper.FaceSegmentHeading(physics, _segment);
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
}
else if (physics.OnClimbable)
{
if (horizDistSq > 0.25)
{
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
input.Forward = true;
}
}
else if (horizDistSq > 0.01)
{
bool onOrPastTarget = TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd)
|| TemplateHelper.HasReachedSegmentEndPlane(pos, _segment);
// Airborne bias toward the exit heading is only safe when
// the bot has effectively finished the current segment's
// horizontal travel: either the footprint is inside the
// landing block, or the vertical drop is small enough that
// lateral drift cannot miss the 1x1 landing column. For
// multi-block drops the bot is in the air for 8+ ticks;
// rotating yaw mid-fall (e.g. after crossing the end plane
// but still 1-2 blocks above landing) pushes sprint/walk
// momentum perpendicular to the segment and drifts the bot
// off the landing column into the void. Keep yaw pointed
// at the landing center through the whole fall on multi-Y
// descends; GroundedSegmentController rotates yaw once the
// bot is actually standing on the landing column.
double segmentYDrop = _segment.Start.Y - _segment.End.Y;
bool isSingleStepDescend = segmentYDrop <= 1.0;
bool footInsideTarget = TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd);
bool biasTowardExitInAir = footInsideTarget
|| (isSingleStepDescend
&& (onOrPastTarget
|| (_hasFallen
&& TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment, distanceThreshold: 1.5))));
float airborneYaw;
if (biasTowardExitInAir)
{
airborneYaw = TemplateHelper.GetExitHeadingYaw(_segment);
}
else if (!isSingleStepDescend)
{
// Multi-block descend: target-tracking yaw rotates as
// the bot drifts past the landing column mid-fall (e.g.
// a diagonal 3 c2c drop with dx=-1, dz=-1 starts at
// yaw=135, the relative bearing to End flips through
// 90 -> 0 -> 315 in 6 air ticks). With Forward input
// held, the rotating yaw pushes air-control momentum
// perpendicular to the planned trajectory, drifting
// the bot ~0.5 m past the landing column and onto an
// adjacent block one tier below. Lock airborne yaw to
// the segment's start->end heading so air drift stays
// aligned with the planned diagonal; the GroundedSegment
// controller takes over once the bot is on the landing.
airborneYaw = TemplateHelper.CalculateYaw(_segment.HeadingX, _segment.HeadingZ);
}
else
{
airborneYaw = targetYaw;
}
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, airborneYaw);
if (_hasFallen || YawDifference(physics.Yaw, airborneYaw) <= 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
{
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
// Multi-block descend overshoot guard: when the
// fall spans 2+ Y blocks, sprint momentum will
// carry the bot roughly one extra horizontal
// block past the planned landing. If the next
// segment prepares a jump (PrepareJump exit) the
// bot MUST land inside the planned 1x1 landing
// column so the jump takeoff has a valid footing;
// overshooting drops into the void or onto a
// block 1-2 tiers below, breaking the jump.
// Once airborne and past the landing end-plane,
// release forward input so sprint momentum decays
// via air drag over the final 1-2 ticks of fall,
// pulling the bot back into the landing column.
bool riskyOvershoot = _hasFallen
&& segmentYDrop >= 2.0
&& onOrPastTarget
&& _segment.ExitTransition == PathTransitionType.PrepareJump;
if (riskyOvershoot)
{
input.Forward = false;
input.Sprint = false;
input.Back = true;
}
else if (_segment.ExitHints.AllowAirBrake)
{
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldForward && _needsSprint)
input.Sprint = true;
}
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
&& TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, _segment.End);
}
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);
}
}
}