feat: add corner walk, sprint descend, and parkour descend moves

- MoveDiagonal: allow single-side-blocked diagonals (corner walk) so
  the bot can hug an open side to cut around a wall; both-sides-blocked
  remains impossible. Walk-speed cost when one side is blocked.
- MoveSprintDescend: sprint off a ledge covering 2 horizontal blocks
  while dropping 1-3 blocks. Registered for cardinal and diagonal
  offsets.
- MoveParkour: support negative yDelta (-1, -2) for descending parkour
  where the bot sprint-jumps across a gap and lands on a lower
  platform. Registered cardinal (dist 2-4, y-1/-2) and diagonal
  variants.
- DescendTemplate: sprint when horizontal distance > 1.5 blocks.
- SprintJumpTemplate: increase vertical landing tolerance for descend.

Made-with: Cursor
This commit is contained in:
BruceChen 2026-04-12 01:38:17 +08:00
parent 285c3000c3
commit 9e6b689dd6
6 changed files with 183 additions and 11 deletions

View file

@ -59,6 +59,16 @@ namespace MinecraftClient.Pathing.Core
moves.Add(new MoveFall());
// Sprint descend: sprint off ledge, 2 blocks horizontal + 1-3 drop
foreach (int dx in offsets)
{
moves.Add(new MoveSprintDescend(dx * 2, 0));
moves.Add(new MoveSprintDescend(dx, dx));
moves.Add(new MoveSprintDescend(dx, -dx));
}
foreach (int dz in offsets)
moves.Add(new MoveSprintDescend(0, dz * 2));
// Cardinal parkour: 2-4 block sprint jumps along +-X and +-Z
foreach (int dx in offsets)
{
@ -67,6 +77,13 @@ namespace MinecraftClient.Pathing.Core
// Ascending: +1Y, dist 2-3 (dist 4 ascend not physically reliable)
for (int dist = 2; dist <= 3; dist++)
moves.Add(new MoveParkour(dx * dist, 0, yDelta: 1));
// Descending parkour: sprint-jump, land 1-2 blocks lower
for (int dist = 2; dist <= 4; dist++)
{
moves.Add(new MoveParkour(dx * dist, 0, yDelta: -1));
if (dist <= 3)
moves.Add(new MoveParkour(dx * dist, 0, yDelta: -2));
}
}
foreach (int dz in offsets)
{
@ -74,6 +91,12 @@ namespace MinecraftClient.Pathing.Core
moves.Add(new MoveParkour(0, dz * dist));
for (int dist = 2; dist <= 3; dist++)
moves.Add(new MoveParkour(0, dz * dist, yDelta: 1));
for (int dist = 2; dist <= 4; dist++)
{
moves.Add(new MoveParkour(0, dz * dist, yDelta: -1));
if (dist <= 3)
moves.Add(new MoveParkour(0, dz * dist, yDelta: -2));
}
}
// Diagonal parkour: sprint jumps at angles.
@ -90,6 +113,11 @@ namespace MinecraftClient.Pathing.Core
// (3,1)/(1,3): sqrt(10) ~ 3.16 blocks
moves.Add(new MoveParkour(dx * 3, dz * 1));
moves.Add(new MoveParkour(dx * 1, dz * 3));
// Diagonal descending parkour
moves.Add(new MoveParkour(dx * 2, dz * 1, yDelta: -1));
moves.Add(new MoveParkour(dx * 1, dz * 2, yDelta: -1));
moves.Add(new MoveParkour(dx * 2, dz * 2, yDelta: -1));
}
}

View file

@ -7,6 +7,7 @@ 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
@ -16,11 +17,15 @@ namespace MinecraftClient.Pathing.Execution.Templates
private int _tickCount;
private bool _hasFallen;
private readonly bool _needsSprint;
public DescendTemplate(Location start, Location end)
{
ExpectedStart = start;
ExpectedEnd = end;
double hdx = end.X - start.X;
double hdz = end.Z - start.Z;
_needsSprint = (hdx * hdx + hdz * hdz) > 2.25;
}
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
@ -70,6 +75,8 @@ namespace MinecraftClient.Pathing.Execution.Templates
{
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
input.Forward = true;
if (_needsSprint)
input.Sprint = true;
}
return TemplateState.InProgress;

View file

@ -82,9 +82,9 @@ namespace MinecraftClient.Pathing.Execution.Templates
goto case Phase.Landing;
case Phase.Landing:
// Tolerance scales with jump distance
double horizTolerance = _horizDist >= 3.5 ? 3.0 : 2.0;
if (horizDistSq < horizTolerance && Math.Abs(dy) < 1.0)
double vertTolerance = Math.Abs(ExpectedEnd.Y - ExpectedStart.Y) > 0.5 ? 1.5 : 1.0;
if (horizDistSq < horizTolerance && Math.Abs(dy) < vertTolerance)
return TemplateState.Complete;
return TemplateState.Failed;
}

View file

@ -4,7 +4,9 @@ namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Diagonal walk (1 block in both X and Z, same Y).
/// Checks both intermediate cardinal columns for clearance.
/// Allows corner walks: if one intermediate cardinal is blocked by a wall
/// but the other is clear, the player can hug the open side to cut the
/// corner. Both sides blocked is impossible (player AABB too wide).
/// </summary>
public sealed class MoveDiagonal : IMove
{
@ -36,19 +38,22 @@ namespace MinecraftClient.Pathing.Moves.Impl
return;
}
if (!ctx.CanWalkThrough(x + XOffset, y, z) || !ctx.CanWalkThrough(x + XOffset, y + 1, z))
bool sideX = ctx.CanWalkThrough(x + XOffset, y, z) &&
ctx.CanWalkThrough(x + XOffset, y + 1, z);
bool sideZ = ctx.CanWalkThrough(x, y, z + ZOffset) &&
ctx.CanWalkThrough(x, y + 1, z + ZOffset);
if (!sideX && !sideZ)
{
result.SetImpossible();
return;
}
if (!ctx.CanWalkThrough(x, y, z + ZOffset) || !ctx.CanWalkThrough(x, y + 1, z + ZOffset))
{
result.SetImpossible();
return;
}
double cost = ctx.SprintCost * ActionCosts.DiagonalMultiplier;
if (!sideX || !sideZ)
cost = ctx.WalkCost * ActionCosts.DiagonalMultiplier;
result.Set(destX, y, destZ, ctx.SprintCost * ActionCosts.DiagonalMultiplier);
result.Set(destX, y, destZ, cost);
}
}
}

View file

@ -6,7 +6,8 @@ namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Sprint jump across a gap in cardinal or diagonal direction.
/// Supports horizontal distances of 2-4 blocks and optional +1Y ascent.
/// Supports horizontal distances of 2-4 blocks, optional +1Y ascent,
/// and -1/-2Y descent (land on a lower platform after the jump).
/// Based on Baritone's MovementParkour design with diagonal extensions.
/// </summary>
public sealed class MoveParkour : IMove
@ -44,6 +45,12 @@ namespace MinecraftClient.Pathing.Moves.Impl
return;
}
if (_yDelta < 0 && -_yDelta > ctx.MaxFallHeight)
{
result.SetImpossible();
return;
}
if (!ctx.CanSprint)
{
result.SetImpossible();
@ -172,6 +179,9 @@ namespace MinecraftClient.Pathing.Moves.Impl
double cost;
if (_yDelta > 0)
cost = horizDist * ctx.SprintCost + ctx.JumpPenalty * 2;
else if (_yDelta < 0)
cost = horizDist * ctx.SprintCost + ctx.JumpPenalty
+ ActionCosts.FallCost(-_yDelta);
else if (horizDist >= 3.5)
cost = horizDist * ctx.SprintCost + ctx.JumpPenalty;
else

View file

@ -0,0 +1,122 @@
using System;
using MinecraftClient.Mapping;
using MinecraftClient.Pathing.Core;
namespace MinecraftClient.Pathing.Moves.Impl
{
/// <summary>
/// Sprint off a ledge and land 2 blocks away horizontally while dropping 1-3 blocks.
/// At sprint speed (~5.6 blocks/s), falling 1-3 blocks gives enough airtime to
/// cover 2 horizontal blocks without needing a jump.
/// Supports cardinal (2,0)/(0,2) and diagonal (1,1) offsets.
/// </summary>
public sealed class MoveSprintDescend : IMove
{
public MoveType Type => MoveType.Descend;
public int XOffset { get; }
public int ZOffset { get; }
public bool DynamicY => true;
public MoveSprintDescend(int xOffset, int zOffset)
{
XOffset = xOffset;
ZOffset = zOffset;
}
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
{
if (!ctx.CanSprint)
{
result.SetImpossible();
return;
}
int destX = x + XOffset;
int destZ = z + ZOffset;
Material fromDown = ctx.GetMaterial(x, y - 1, z);
if (fromDown.CanBeClimbedOn())
{
result.SetImpossible();
return;
}
int xSign = Math.Sign(XOffset);
int zSign = Math.Sign(ZOffset);
int xAbs = Math.Abs(XOffset);
int zAbs = Math.Abs(ZOffset);
// The flight path sweeps through intermediate blocks at current Y.
// Check body clearance for all intermediate and destination columns.
for (int i = 0; i <= xAbs; i++)
{
for (int j = 0; j <= zAbs; j++)
{
if (i == 0 && j == 0) continue;
int gx = x + xSign * i;
int gz = z + zSign * j;
if (!ctx.CanWalkThrough(gx, y, gz) || !ctx.CanWalkThrough(gx, y + 1, gz))
{
result.SetImpossible();
return;
}
}
}
// The first step in the primary direction must lack ground (this IS a drop).
if (xAbs > 0 && zAbs == 0)
{
if (ctx.CanWalkOn(x + xSign, y - 1, z))
{
result.SetImpossible();
return;
}
}
else if (xAbs == 0 && zAbs > 0)
{
if (ctx.CanWalkOn(x, y - 1, z + zSign))
{
result.SetImpossible();
return;
}
}
else
{
if (ctx.CanWalkOn(x + xSign, y - 1, z + zSign))
{
result.SetImpossible();
return;
}
}
// Scan downward from destination column for a landing spot.
double horizDist = Math.Sqrt((double)(XOffset * XOffset + ZOffset * ZOffset));
for (int drop = 1; drop <= ctx.MaxFallHeight; drop++)
{
int landY = y - drop - 1;
if (landY < -64) break;
if (!ctx.CanWalkOn(destX, landY, destZ))
continue;
Material landMat = ctx.GetMaterial(destX, landY, destZ);
if (MoveHelper.IsHazardous(landMat))
{
result.SetImpossible();
return;
}
// Body space at landing
if (!ctx.CanWalkThrough(destX, landY + 1, destZ) ||
!ctx.CanWalkThrough(destX, landY + 2, destZ))
continue;
double cost = horizDist * ctx.SprintCost + ActionCosts.FallCost(drop);
result.Set(destX, landY + 1, destZ, cost);
return;
}
result.SetImpossible();
}
}
}