Minecraft-Console-Client/MinecraftClient/Pathing/Moves/Impl/MoveDescend.cs
BruceChen 5de169db64 pathing: stabilize 0-replan round-trip on ledge/descend runs
Fix a cluster of execution-layer issues that caused replans and void
falls when traversing narrow ledges and multi-block descents between
(251.5,141,210.5) and (252.5,138,220.5):

- WalkTemplate / GroundedSegmentController: suppress the pre-rotation
  bias toward the next segment's exit heading on stable-footing Turn
  exits where the next segment is not a jump.  The next template
  snaps yaw on its first tick anyway, and pre-rotating mid-stride on
  a 1-block walkway pushes sprint drift perpendicular to the path and
  walks the bot off the edge.  Turn exits into a jump still get the
  bias so the takeoff direction stays aligned.

- GroundedSegmentController.ShouldComplete: relax the headingReady
  gate for Turn exits with stable footing so the segment can complete
  once yaw is aligned with either the current or the next segment
  heading (within 25/15 deg).  Without this the removed bias would
  leave the bot stuck at the end of a walkway waiting for a rotation
  that never happens.

- DescendTemplate: restrict the airborne exit-heading bias so it only
  kicks in when the footprint is inside the landing block, or on
  single-step drops where the fall is too short for lateral drift to
  miss the landing column.  On 2+ block drops the bot now keeps yaw
  pointed at the landing center for the whole fall.

- DescendTemplate: add a multi-block overshoot guard on PrepareJump
  exits.  Once airborne and past the landing end-plane on a 2+ Y
  drop, release forward/sprint and press back briefly so air drag
  pulls the bot back into the 1x1 landing column instead of sailing
  one block past it into the neighbouring void.

Live round-trip between the two goal coordinates now completes with
zero replans in three consecutive runs in each direction.  Full unit
test suite is unchanged from the pre-existing baseline (22 failing
tests, all orthogonal to this change).

Made-with: Cursor
2026-04-22 16:43:43 +00:00

167 lines
6.2 KiB
C#

using MinecraftClient.Mapping;
using MinecraftClient.Pathing.Core;
namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Walk off a ledge and drop 1-N blocks in a cardinal direction.
/// For short drops (1-MaxFallHeight), uses simple scan.
/// For longer drops, delegates to DynamicFallCost which supports:
/// - Water/liquid safe landing
/// - Mid-fall ladder/vine grabbing (resets effective fall height if ≤ 11 blocks)
/// Based on Baritone's MovementDescend.dynamicFallCost design.
/// </summary>
public sealed class MoveDescend : IMove
{
public MoveType Type => MoveType.Descend;
public int XOffset { get; }
public int ZOffset { get; }
public bool DynamicY => true;
public MoveDescend(int xOffset, int zOffset)
{
XOffset = xOffset;
ZOffset = zOffset;
}
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
{
int destX = x + XOffset;
int destZ = z + ZOffset;
if (!ctx.CanWalkThrough(destX, y, destZ) || !ctx.CanWalkThrough(destX, y + 1, destZ))
{
result.SetImpossible();
return;
}
// The landing feet column must also be passable. Without this, a descend
// into a column whose y-1 block is solid (e.g. a 2-block thick platform top
// where (destX,y-1) is stone and (destX,y-2) is also stone) would be
// accepted: the solid y-2 floor satisfies CanWalkOn, the y/y+1 body space
// satisfies the step, but physically the bot just walks onto the solid
// y-1 block at the same feet level and the Descend template waits forever
// for a drop that can never happen -- producing an infinite replan loop.
if (!ctx.CanWalkThrough(destX, y - 1, destZ))
{
result.SetImpossible();
return;
}
// Don't descend from ladder/vine (unreliable)
Material fromDown = ctx.GetMaterial(x, y - 1, z);
if (fromDown.CanBeClimbedOn())
{
result.SetImpossible();
return;
}
// Check for simple 1-block descend first (most common case)
if (ctx.CanWalkOn(destX, y - 2, destZ))
{
Material landOn = ctx.GetMaterial(destX, y - 2, destZ);
if (MoveHelper.IsHazardous(landOn))
{
result.SetImpossible();
return;
}
if (ctx.GetMaterial(destX, y - 1, destZ).CanBeClimbedOn())
{
result.SetImpossible();
return;
}
double cost = ActionCosts.WalkOffBlock + ActionCosts.FallCost(1);
result.Set(destX, y - 1, destZ, cost);
return;
}
// Not a simple 1-block drop, try dynamic fall
DynamicFallCost(ctx, x, y, z, destX, destZ, ref result);
}
/// <summary>
/// Scan downward for a safe landing, supporting water, ladder grabs, and
/// configurable max heights. Based on Baritone's dynamicFallCost.
/// </summary>
private static void DynamicFallCost(
CalculationContext ctx, int x, int y, int z,
int destX, int destZ, ref MoveResult result)
{
if (!ctx.CanWalkThrough(destX, y - 2, destZ))
{
result.SetImpossible();
return;
}
double costSoFar = 0;
int effectiveStartHeight = y;
// Scan starts from fallHeight=3 (2 blocks below the ledge)
// because fallHeight=1 and =2 were already checked above
int maxScan = ctx.MaxFallHeightWater > ctx.MaxFallHeight
? ctx.MaxFallHeightWater
: ctx.MaxFallHeight;
for (int fallHeight = 3; fallHeight <= maxScan; fallHeight++)
{
int newY = y - fallHeight;
if (newY < -64) break;
Material ontoMat = ctx.GetMaterial(destX, newY, destZ);
int unprotectedFallHeight = fallHeight - (y - effectiveStartHeight);
double tentativeCost = ActionCosts.WalkOffBlock
+ ActionCosts.FallCost(unprotectedFallHeight) + costSoFar;
// Water landing: safe regardless of height (water absorbs all fall damage)
if (MoveHelper.IsWater(ontoMat))
{
result.Set(destX, newY, destZ, tentativeCost);
return;
}
// Mid-fall ladder/vine grab: resets effective fall height.
// Vanilla: player grabs ladders/vines if falling speed is low enough
// (roughly ≤ 11 blocks of unprotected free fall).
if (ctx.AllowLadderGrabDuringFall && unprotectedFallHeight <= 11
&& ontoMat.CanBeClimbedOn())
{
costSoFar += ActionCosts.FallCost(unprotectedFallHeight - 1);
costSoFar += ActionCosts.LadderDownOne;
effectiveStartHeight = newY;
continue;
}
// Air or passable: continue falling
if (ctx.CanWalkThrough(destX, newY, destZ))
continue;
// Hit something solid
if (MoveHelper.IsHazardous(ontoMat))
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkOn(destX, newY, destZ))
{
result.SetImpossible();
return;
}
// Solid landing: allowed if within safe fall height
if (unprotectedFallHeight <= ctx.MaxFallHeight + 1)
{
result.Set(destX, newY + 1, destZ, tentativeCost);
return;
}
result.SetImpossible();
return;
}
result.SetImpossible();
}
}
}