mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
pathing: unify jump moves into MoveJump + IMoveExpander
Replace seven hand-written IMove classes (MoveTraverse, MoveDiagonal, MoveAscend, MoveDiagonalAscend, MoveDiagonalDescend, MoveParkour, MoveSidewallParkour) with a single MoveJump driven by a JumpDescriptor (XOffset, ZOffset, YDelta, JumpFlavor). JumpFeasibility is the single source of truth for the physics/cost rules of every jump-family move. A* no longer iterates a flat IMove[]. The Calculate loop now drives an IMoveExpander[] that writes into a stackalloc Span<MoveNeighbor>, eliminating per-iteration heap traffic. JumpExpander enumerates every jump-family descriptor dynamically; LegacyMoveExpander wraps the remaining dynamic-landing moves (MoveDescend, MoveSprintDescend, MoveClimb, MoveFall) so callers that still pass a custom IMove[] keep working. Add two O(1) short-circuits at the top of JumpExpander.Expand: - Hoist the per-node parkour preconditions (AllowParkour + CanSprint, standing block climbability, feet-liquid, head clearance at y+2) so ~170 SprintJump + Sidewall descriptors never call JumpFeasibility when the node cannot take off at all. - Precompute an 8-way "first step has no floor" table indexed by (sign(dx), sign(dz)) so SprintJump descriptors in a direction that has a walkable floor underneath are dropped without Evaluate. - Add a conservative "any cardinal wall at y or y+1" probe that skips all 112 Sidewall descriptors when no wall exists adjacent to the takeoff. Move tests switch to the new MoveJump.* factory methods. Behavior is verified by the existing test suite: the 21 pre-existing baseline failures are preserved exactly, 0 regressions introduced. Made-with: Cursor
This commit is contained in:
parent
da52aa5c3c
commit
d002930a6a
16 changed files with 1650 additions and 848 deletions
|
|
@ -31,7 +31,7 @@ public sealed class MoveParkourTests
|
|||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
world.SetBlock(new Location(-1, FloorY, 0), Block.Air);
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(3, 0);
|
||||
var move = MoveJump.Parkour(3, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -45,7 +45,7 @@ public sealed class MoveParkourTests
|
|||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
world.SetBlock(new Location(1, FloorY, 0), Block.Air);
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(2, 0);
|
||||
var move = MoveJump.Parkour(2, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -60,7 +60,7 @@ public sealed class MoveParkourTests
|
|||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
world.SetBlock(new Location(1, FloorY, 0), Block.Air);
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(2, 0);
|
||||
var move = MoveJump.Parkour(2, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -74,7 +74,7 @@ public sealed class MoveParkourTests
|
|||
{
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(2, 0);
|
||||
var move = MoveJump.Parkour(2, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -95,7 +95,7 @@ public sealed class MoveParkourTests
|
|||
FlatWorldTestBuilder.SetSolid(world, 2, FloorY + 2, -1);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(2, 0);
|
||||
var move = MoveJump.Parkour(2, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -110,7 +110,7 @@ public sealed class MoveParkourTests
|
|||
world.SetBlock(new Location(1, FloorY + 1, 0), new Block(1));
|
||||
world.SetBlock(new Location(1, FloorY + 2, 0), new Block(1));
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(1, 1);
|
||||
var move = MoveJump.Parkour(1, 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -128,7 +128,7 @@ public sealed class MoveParkourTests
|
|||
|
||||
var ctx = BuildContext(world);
|
||||
SetPreviousMoveType(ctx, MoveType.Parkour);
|
||||
var move = new MoveParkour(4, 0, yDelta: -1);
|
||||
var move = MoveJump.Parkour(4, 0, yDelta: -1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -147,7 +147,7 @@ public sealed class MoveParkourTests
|
|||
FlatWorldTestBuilder.SetSolid(world, 4, FloorY + 1, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(4, 0, yDelta: 1);
|
||||
var move = MoveJump.Parkour(4, 0, yDelta: 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -164,7 +164,7 @@ public sealed class MoveParkourTests
|
|||
FlatWorldTestBuilder.SetSolid(world, 6, FloorY - 1, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(6, 0, yDelta: -1);
|
||||
var move = MoveJump.Parkour(6, 0, yDelta: -1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -181,7 +181,7 @@ public sealed class MoveParkourTests
|
|||
FlatWorldTestBuilder.SetSolid(world, 6, FloorY - 2, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(6, 0, yDelta: -2);
|
||||
var move = MoveJump.Parkour(6, 0, yDelta: -2);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -199,7 +199,7 @@ public sealed class MoveParkourTests
|
|||
|
||||
var ctx = BuildContext(world);
|
||||
SetPreviousMoveType(ctx, MoveType.Parkour);
|
||||
var move = new MoveParkour(6, 0, yDelta: -1);
|
||||
var move = MoveJump.Parkour(6, 0, yDelta: -1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -217,11 +217,66 @@ public sealed class MoveParkourTests
|
|||
|
||||
var ctx = BuildContext(world);
|
||||
SetPreviousMoveType(ctx, MoveType.Parkour);
|
||||
var move = new MoveParkour(6, 0, yDelta: -2);
|
||||
var move = MoveJump.Parkour(6, 0, yDelta: -2);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible);
|
||||
}
|
||||
|
||||
// Diagonal ascending parkour: +1 block up with diagonal offset, covers
|
||||
// the corner-step-up case seen in stepped pyramids where a straight
|
||||
// MoveSidewallParkour would demand an adjacent wall that isn't present.
|
||||
// Short (sqrt(5)) ascends work from a lone overhang block because a
|
||||
// cold-start sprint jump reaches ~2.5 blocks horizontally; longer
|
||||
// diagonals such as (2,2) require a runway and are exercised separately.
|
||||
[Theory]
|
||||
[InlineData(2, 1)]
|
||||
[InlineData(1, 2)]
|
||||
public void AcceptsDiagonalAscendingParkour_FromLoneStart(int dx, int dz)
|
||||
{
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
FlatWorldTestBuilder.ClearBox(world, -5, FloorY, -5, 10, FloorY + 5, 10);
|
||||
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY, 0);
|
||||
int destFloorY = FloorY + 1;
|
||||
FlatWorldTestBuilder.SetSolid(world, dx, destFloorY, dz);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = MoveJump.Parkour(dx, dz, yDelta: 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible, $"diagonal ascend ({dx},{dz},+1) should plan from lone start");
|
||||
Assert.Equal(dx, result.DestX);
|
||||
Assert.Equal(destFloorY + 1, result.DestY);
|
||||
Assert.Equal(dz, result.DestZ);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptsDiagonalAscendingParkour_2x2_WithRunway()
|
||||
{
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
FlatWorldTestBuilder.ClearBox(world, -5, FloorY, -5, 10, FloorY + 5, 10);
|
||||
|
||||
// Diagonal runway behind the jump (opposite the jump direction)
|
||||
// so HasRunUp's back-step check at (-1,-1) succeeds.
|
||||
FlatWorldTestBuilder.SetSolid(world, -1, FloorY, -1);
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY, 0);
|
||||
int destFloorY = FloorY + 1;
|
||||
FlatWorldTestBuilder.SetSolid(world, 2, destFloorY, 2);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = MoveJump.Parkour(2, 2, yDelta: 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible, "(2,2,+1) should plan with a straight runway behind the jump");
|
||||
Assert.Equal(2, result.DestX);
|
||||
Assert.Equal(destFloorY + 1, result.DestY);
|
||||
Assert.Equal(2, result.DestZ);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,21 @@ namespace MinecraftClient.Tests.Pathing.Moves;
|
|||
|
||||
public sealed class MoveSidewallParkourTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("sidewall-descend-gap5-dy-1-wo0", 5, 0)]
|
||||
[InlineData("sidewall-descend-gap5-dy-1-wo1", 5, 1)]
|
||||
public void Calculate_LongDescendStaticEntry_RejectsWithoutPreparedRunup(string scenarioId, int gap, int wallOffset)
|
||||
{
|
||||
World world = SidewallParkourScenarioBuilder.BuildWorld(gap, deltaY: -1, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: -1);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible, scenarioId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("sidewall-flat-gap2-wo0", 2, 0, 0)]
|
||||
[InlineData("sidewall-flat-gap3-wo1", 3, 0, 1)]
|
||||
|
|
@ -21,7 +36,7 @@ public sealed class MoveSidewallParkourTests
|
|||
{
|
||||
World world = SidewallParkourScenarioBuilder.BuildWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = new MoveSidewallParkour(xOffset: -1, zOffset: gap, yDelta: deltaY);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
|
@ -41,11 +56,80 @@ public sealed class MoveSidewallParkourTests
|
|||
{
|
||||
World world = SidewallParkourScenarioBuilder.BuildWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = new MoveSidewallParkour(xOffset: -1, zOffset: gap, yDelta: deltaY);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible, scenarioId);
|
||||
}
|
||||
|
||||
// Scenarios captured from the staircase / step-pyramid image where the start
|
||||
// block is a lone, overhanging tread with no 2-block runway behind it.
|
||||
// Physics allows a cold-start sprint-jump to clear ~3 blocks horizontally,
|
||||
// so short sidewall gaps should still plan even without a runway.
|
||||
[Theory]
|
||||
[InlineData("sidewall-lone-start-flat-gap2-wo0", 2, 0, 0)]
|
||||
[InlineData("sidewall-lone-start-flat-gap3-wo0", 3, 0, 0)]
|
||||
[InlineData("sidewall-lone-start-flat-gap2-wo1", 2, 0, 1)]
|
||||
[InlineData("sidewall-lone-start-ascend-gap2-dy+1-wo0", 2, 1, 0)]
|
||||
[InlineData("sidewall-lone-start-descend-gap2-dy-1-wo0", 2, -1, 0)]
|
||||
[InlineData("sidewall-lone-start-descend-gap3-dy-1-wo0", 3, -1, 0)]
|
||||
public void Calculate_AcceptsLoneStart_ShortSidewallJumps(string scenarioId, int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
World world = BuildLoneStartWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible, scenarioId);
|
||||
Assert.Equal(ParkourProfile.Sidewall, result.ParkourProfile);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("sidewall-lone-start-flat-gap4-wo0", 4, 0, 0)]
|
||||
[InlineData("sidewall-lone-start-ascend-gap3-dy+1-wo0", 3, 1, 0)]
|
||||
[InlineData("sidewall-lone-start-descend-gap4-dy-1-wo0", 4, -1, 0)]
|
||||
public void Calculate_RejectsLoneStart_LongSidewallJumps(string scenarioId, int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
World world = BuildLoneStartWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible, scenarioId);
|
||||
}
|
||||
|
||||
private static World BuildLoneStartWorld(int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
const int startX = 100;
|
||||
const int startY = 80;
|
||||
const int startZ = 100;
|
||||
int floorY = startY - 1;
|
||||
int landX = startX - 1;
|
||||
int landY = startY + deltaY;
|
||||
int landZ = startZ + gap;
|
||||
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(floorY: 0, min: 80, max: landZ + 8);
|
||||
FlatWorldTestBuilder.ClearBox(world, 90, 70, 90, 110, 96, landZ + 8);
|
||||
|
||||
FlatWorldTestBuilder.SetSolid(world, startX, floorY, startZ);
|
||||
|
||||
FlatWorldTestBuilder.FillSolid(
|
||||
world,
|
||||
landX,
|
||||
Math.Min(floorY, landY - 1),
|
||||
startZ,
|
||||
landX,
|
||||
Math.Max(floorY, landY - 1) + 7,
|
||||
startZ + wallOffset);
|
||||
|
||||
FlatWorldTestBuilder.SetSolid(world, landX, landY - 1, landZ);
|
||||
|
||||
return world;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,71 @@ namespace MinecraftClient.Pathing.Core
|
|||
{
|
||||
public sealed class AStarPathFinder
|
||||
{
|
||||
private readonly record struct NodeKey(long PackedPosition, EntryPreparationState EntryPreparation);
|
||||
|
||||
private readonly IMove[] _allMoves;
|
||||
private readonly IMoveExpander[] _expanders;
|
||||
private readonly int _totalExpanderCapacity;
|
||||
private readonly int _maxChunkBorderFetch;
|
||||
|
||||
public Action<string>? DebugLog { get; set; }
|
||||
|
||||
public AStarPathFinder(IMove[]? moves = null, int maxChunkBorderFetch = 64)
|
||||
: this(BuildExpanders(moves), moves ?? BuildDefaultMoves(), maxChunkBorderFetch)
|
||||
{
|
||||
_allMoves = moves ?? BuildDefaultMoves();
|
||||
}
|
||||
|
||||
public AStarPathFinder(IMoveExpander[] expanders, int maxChunkBorderFetch = 64)
|
||||
: this(expanders, System.Array.Empty<IMove>(), maxChunkBorderFetch)
|
||||
{
|
||||
}
|
||||
|
||||
private AStarPathFinder(IMoveExpander[] expanders, IMove[] allMoves, int maxChunkBorderFetch)
|
||||
{
|
||||
_expanders = expanders;
|
||||
_allMoves = allMoves;
|
||||
_maxChunkBorderFetch = maxChunkBorderFetch;
|
||||
|
||||
int total = 0;
|
||||
for (int i = 0; i < expanders.Length; i++)
|
||||
total += expanders[i].MaxNeighbors;
|
||||
_totalExpanderCapacity = total;
|
||||
}
|
||||
|
||||
private static IMoveExpander[] BuildExpanders(IMove[]? explicitMoves)
|
||||
{
|
||||
if (explicitMoves is null)
|
||||
{
|
||||
return BuildDefaultExpanders();
|
||||
}
|
||||
|
||||
// Caller supplied a specific move set (e.g. tests). Wrap it as a
|
||||
// legacy expander so the old API keeps working.
|
||||
return [new LegacyMoveExpander(explicitMoves)];
|
||||
}
|
||||
|
||||
public static IMoveExpander[] BuildDefaultExpanders()
|
||||
{
|
||||
IMove[] legacyMoves =
|
||||
[
|
||||
new MoveDescend(1, 0),
|
||||
new MoveDescend(-1, 0),
|
||||
new MoveDescend(0, 1),
|
||||
new MoveDescend(0, -1),
|
||||
new MoveSprintDescend(2, 0),
|
||||
new MoveSprintDescend(-2, 0),
|
||||
new MoveSprintDescend(0, 2),
|
||||
new MoveSprintDescend(0, -2),
|
||||
new MoveSprintDescend(1, 1),
|
||||
new MoveSprintDescend(1, -1),
|
||||
new MoveSprintDescend(-1, 1),
|
||||
new MoveSprintDescend(-1, -1),
|
||||
new MoveClimb(true),
|
||||
new MoveClimb(false),
|
||||
new MoveFall(),
|
||||
];
|
||||
|
||||
return [new JumpExpander(), new LegacyMoveExpander(legacyMoves)];
|
||||
}
|
||||
|
||||
public static IMove[] BuildDefaultMoves()
|
||||
|
|
@ -26,121 +82,119 @@ namespace MinecraftClient.Pathing.Core
|
|||
var moves = new List<IMove>();
|
||||
|
||||
int[] offsets = [1, -1];
|
||||
|
||||
// ---- jump family (all unified as MoveJump with a JumpDescriptor) ----
|
||||
|
||||
// Cardinal walk + 1-block ascend
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
moves.Add(new MoveTraverse(dx, 0));
|
||||
moves.Add(new MoveAscend(dx, 0));
|
||||
moves.Add(new MoveDescend(dx, 0));
|
||||
moves.Add(MoveJump.Traverse(dx, 0));
|
||||
moves.Add(MoveJump.Ascend(dx, 0));
|
||||
}
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
moves.Add(new MoveTraverse(0, dz));
|
||||
moves.Add(new MoveAscend(0, dz));
|
||||
moves.Add(new MoveDescend(0, dz));
|
||||
moves.Add(MoveJump.Traverse(0, dz));
|
||||
moves.Add(MoveJump.Ascend(0, dz));
|
||||
}
|
||||
|
||||
moves.Add(new MoveDiagonal(1, 1));
|
||||
moves.Add(new MoveDiagonal(1, -1));
|
||||
moves.Add(new MoveDiagonal(-1, 1));
|
||||
moves.Add(new MoveDiagonal(-1, -1));
|
||||
|
||||
// Diagonal ascend/descend: corner jumps and drops
|
||||
// Diagonal walk + diagonal ascend/descend (corner cases)
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
moves.Add(new MoveDiagonalAscend(dx, dz));
|
||||
moves.Add(new MoveDiagonalDescend(dx, dz));
|
||||
moves.Add(MoveJump.Diagonal(dx, dz));
|
||||
moves.Add(MoveJump.DiagonalAscend(dx, dz));
|
||||
moves.Add(MoveJump.DiagonalDescend(dx, dz));
|
||||
}
|
||||
}
|
||||
|
||||
moves.Add(new MoveClimb(true));
|
||||
moves.Add(new MoveClimb(false));
|
||||
|
||||
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: long sprint jumps along +-X and +-Z.
|
||||
// Longer distances remain gated by MoveParkour feasibility and available runway/carry.
|
||||
// Cardinal parkour (flat / +1 ascend / -1 -2 descend)
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
for (int dist = 2; dist <= 5; dist++)
|
||||
moves.Add(new MoveParkour(dx * dist, 0));
|
||||
// Ascending cardinal parkour tops out at offset 3.
|
||||
moves.Add(MoveJump.Parkour(dx * dist, 0));
|
||||
for (int dist = 2; dist <= 3; dist++)
|
||||
moves.Add(new MoveParkour(dx * dist, 0, yDelta: 1));
|
||||
// Descending cardinal parkour tops out at offset 5.
|
||||
moves.Add(MoveJump.Parkour(dx * dist, 0, yDelta: 1));
|
||||
for (int dist = 2; dist <= 5; dist++)
|
||||
moves.Add(new MoveParkour(dx * dist, 0, yDelta: -1));
|
||||
moves.Add(MoveJump.Parkour(dx * dist, 0, yDelta: -1));
|
||||
for (int dist = 2; dist <= 5; dist++)
|
||||
moves.Add(new MoveParkour(dx * dist, 0, yDelta: -2));
|
||||
moves.Add(MoveJump.Parkour(dx * dist, 0, yDelta: -2));
|
||||
}
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
for (int dist = 2; dist <= 5; dist++)
|
||||
moves.Add(new MoveParkour(0, dz * dist));
|
||||
moves.Add(MoveJump.Parkour(0, dz * dist));
|
||||
for (int dist = 2; dist <= 3; dist++)
|
||||
moves.Add(new MoveParkour(0, dz * dist, yDelta: 1));
|
||||
moves.Add(MoveJump.Parkour(0, dz * dist, yDelta: 1));
|
||||
for (int dist = 2; dist <= 5; dist++)
|
||||
moves.Add(new MoveParkour(0, dz * dist, yDelta: -1));
|
||||
moves.Add(MoveJump.Parkour(0, dz * dist, yDelta: -1));
|
||||
for (int dist = 2; dist <= 5; dist++)
|
||||
moves.Add(new MoveParkour(0, dz * dist, yDelta: -2));
|
||||
moves.Add(MoveJump.Parkour(0, dz * dist, yDelta: -2));
|
||||
}
|
||||
|
||||
// Sidewall parkour: dominant-axis sprint jumps with a one-block lateral offset.
|
||||
// Diagonal parkour (flat + diagonal ascending/descending)
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
moves.Add(MoveJump.Parkour(dx * 2, dz * 1));
|
||||
moves.Add(MoveJump.Parkour(dx * 1, dz * 2));
|
||||
moves.Add(MoveJump.Parkour(dx * 2, dz * 2));
|
||||
moves.Add(MoveJump.Parkour(dx * 3, dz * 1));
|
||||
moves.Add(MoveJump.Parkour(dx * 1, dz * 3));
|
||||
|
||||
moves.Add(MoveJump.Parkour(dx * 2, dz * 1, yDelta: -1));
|
||||
moves.Add(MoveJump.Parkour(dx * 1, dz * 2, yDelta: -1));
|
||||
moves.Add(MoveJump.Parkour(dx * 2, dz * 2, yDelta: -1));
|
||||
|
||||
moves.Add(MoveJump.Parkour(dx * 2, dz * 1, yDelta: 1));
|
||||
moves.Add(MoveJump.Parkour(dx * 1, dz * 2, yDelta: 1));
|
||||
moves.Add(MoveJump.Parkour(dx * 2, dz * 2, yDelta: 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Sidewall parkour (dominant-axis sprint jumps using an inner wall)
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
foreach (int distance in new[] { 2, 3, 4, 5 })
|
||||
{
|
||||
moves.Add(new MoveSidewallParkour(dx, dz * distance));
|
||||
moves.Add(new MoveSidewallParkour(dx * distance, dz));
|
||||
moves.Add(MoveJump.Sidewall(dx, dz * distance));
|
||||
moves.Add(MoveJump.Sidewall(dx * distance, dz));
|
||||
|
||||
if (distance <= 3)
|
||||
{
|
||||
moves.Add(new MoveSidewallParkour(dx, dz * distance, yDelta: 1));
|
||||
moves.Add(new MoveSidewallParkour(dx * distance, dz, yDelta: 1));
|
||||
moves.Add(MoveJump.Sidewall(dx, dz * distance, yDelta: 1));
|
||||
moves.Add(MoveJump.Sidewall(dx * distance, dz, yDelta: 1));
|
||||
}
|
||||
|
||||
moves.Add(new MoveSidewallParkour(dx, dz * distance, yDelta: -1));
|
||||
moves.Add(new MoveSidewallParkour(dx * distance, dz, yDelta: -1));
|
||||
moves.Add(new MoveSidewallParkour(dx, dz * distance, yDelta: -2));
|
||||
moves.Add(new MoveSidewallParkour(dx * distance, dz, yDelta: -2));
|
||||
moves.Add(MoveJump.Sidewall(dx, dz * distance, yDelta: -1));
|
||||
moves.Add(MoveJump.Sidewall(dx * distance, dz, yDelta: -1));
|
||||
moves.Add(MoveJump.Sidewall(dx, dz * distance, yDelta: -2));
|
||||
moves.Add(MoveJump.Sidewall(dx * distance, dz, yDelta: -2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diagonal parkour: sprint jumps at angles.
|
||||
// Only include combinations with actual distance <= ~3.2 blocks (conservative)
|
||||
// ---- dynamic-landing family (kept separate: variable landing depth) ----
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
// (2,1)/(1,2): sqrt(5) ~ 2.24 blocks
|
||||
moves.Add(new MoveParkour(dx * 2, dz * 1));
|
||||
moves.Add(new MoveParkour(dx * 1, dz * 2));
|
||||
// (2,2): sqrt(8) ~ 2.83 blocks
|
||||
moves.Add(new MoveParkour(dx * 2, dz * 2));
|
||||
// (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));
|
||||
}
|
||||
moves.Add(new MoveDescend(dx, 0));
|
||||
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 MoveDescend(0, dz));
|
||||
moves.Add(new MoveSprintDescend(0, dz * 2));
|
||||
}
|
||||
|
||||
// ---- vertical / free fall ----
|
||||
moves.Add(new MoveClimb(true));
|
||||
moves.Add(new MoveClimb(false));
|
||||
moves.Add(new MoveFall());
|
||||
|
||||
return [.. moves];
|
||||
}
|
||||
|
|
@ -170,7 +224,7 @@ namespace MinecraftClient.Pathing.Core
|
|||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var openSet = new BinaryHeapOpenSet(4096);
|
||||
var nodeMap = new Dictionary<long, PathNode>(4096);
|
||||
var nodeMap = new Dictionary<NodeKey, PathNode>(4096);
|
||||
|
||||
var startNode = new PathNode(startX, startY, startZ)
|
||||
{
|
||||
|
|
@ -179,14 +233,19 @@ namespace MinecraftClient.Pathing.Core
|
|||
IsOpen = true
|
||||
};
|
||||
openSet.Insert(startNode);
|
||||
nodeMap[startNode.PackedPosition] = startNode;
|
||||
nodeMap[new NodeKey(startNode.PackedPosition, startNode.EntryPreparation)] = startNode;
|
||||
|
||||
int nodesExplored = 0;
|
||||
int unloadedChunkHits = 0;
|
||||
bool searchAborted = false;
|
||||
PathNode? bestPartialNode = startNode;
|
||||
double bestPartialScore = startNode.HCost + startNode.GCost * 0.5;
|
||||
MoveResult moveResult = default;
|
||||
|
||||
// Per-node scratch buffer for IMoveExpander output. Size = sum of
|
||||
// MaxNeighbors across all expanders so no expander can overflow.
|
||||
Span<MoveNeighbor> neighborBuffer = _totalExpanderCapacity <= 512
|
||||
? stackalloc MoveNeighbor[_totalExpanderCapacity]
|
||||
: new MoveNeighbor[_totalExpanderCapacity];
|
||||
|
||||
DebugLog?.Invoke($"[A*] Start ({startX},{startY},{startZ}), goal={goal}");
|
||||
|
||||
|
|
@ -217,63 +276,73 @@ namespace MinecraftClient.Pathing.Core
|
|||
return new PathResult(PathStatus.Success, path, nodesExplored, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
foreach (var move in _allMoves)
|
||||
ctx.PreviousMoveType = current.MoveUsed;
|
||||
ctx.CurrentEntryPreparation = current.EntryPreparation;
|
||||
|
||||
int bufferOffset = 0;
|
||||
for (int ex = 0; ex < _expanders.Length; ex++)
|
||||
{
|
||||
ctx.PreviousMoveType = current.MoveUsed;
|
||||
moveResult.Cost = 0;
|
||||
move.Calculate(ctx, current.X, current.Y, current.Z, ref moveResult);
|
||||
IMoveExpander expander = _expanders[ex];
|
||||
Span<MoveNeighbor> slot = neighborBuffer.Slice(bufferOffset, expander.MaxNeighbors);
|
||||
int produced = expander.Expand(ctx, current.X, current.Y, current.Z, slot);
|
||||
bufferOffset += expander.MaxNeighbors;
|
||||
|
||||
if (moveResult.IsImpossible)
|
||||
continue;
|
||||
|
||||
int nx = moveResult.DestX;
|
||||
int ny = moveResult.DestY;
|
||||
int nz = moveResult.DestZ;
|
||||
|
||||
if (!ctx.IsChunkLoaded(nx, nz))
|
||||
for (int i = 0; i < produced; i++)
|
||||
{
|
||||
unloadedChunkHits++;
|
||||
if (unloadedChunkHits > _maxChunkBorderFetch)
|
||||
continue;
|
||||
}
|
||||
MoveNeighbor emitted = slot[i];
|
||||
int nx = emitted.DestX;
|
||||
int ny = emitted.DestY;
|
||||
int nz = emitted.DestZ;
|
||||
|
||||
double tentativeG = current.GCost + moveResult.Cost;
|
||||
long packed = PathNode.Pack(nx, ny, nz);
|
||||
|
||||
if (nodeMap.TryGetValue(packed, out var neighbor))
|
||||
{
|
||||
if (neighbor.IsClosed)
|
||||
continue;
|
||||
if (tentativeG >= neighbor.GCost)
|
||||
continue;
|
||||
|
||||
neighbor.GCost = tentativeG;
|
||||
neighbor.Parent = current;
|
||||
neighbor.MoveUsed = move.Type;
|
||||
neighbor.ParkourProfile = moveResult.ParkourProfile;
|
||||
if (neighbor.IsOpen)
|
||||
openSet.Update(neighbor);
|
||||
}
|
||||
else
|
||||
{
|
||||
neighbor = new PathNode(nx, ny, nz)
|
||||
if (!ctx.IsChunkLoaded(nx, nz))
|
||||
{
|
||||
GCost = tentativeG,
|
||||
HCost = goal.Heuristic(nx, ny, nz),
|
||||
Parent = current,
|
||||
MoveUsed = move.Type,
|
||||
ParkourProfile = moveResult.ParkourProfile,
|
||||
IsOpen = true
|
||||
};
|
||||
nodeMap[packed] = neighbor;
|
||||
openSet.Insert(neighbor);
|
||||
}
|
||||
unloadedChunkHits++;
|
||||
if (unloadedChunkHits > _maxChunkBorderFetch)
|
||||
continue;
|
||||
}
|
||||
|
||||
double partialScore = neighbor.HCost + neighbor.GCost * 0.5;
|
||||
if (partialScore < bestPartialScore)
|
||||
{
|
||||
bestPartialScore = partialScore;
|
||||
bestPartialNode = neighbor;
|
||||
double tentativeG = current.GCost + emitted.Cost;
|
||||
EntryPreparationState nextPreparation = ResolveEntryPreparation(
|
||||
current, emitted.MoveType, emitted.DestX, emitted.DestY, emitted.DestZ);
|
||||
var key = new NodeKey(PathNode.Pack(nx, ny, nz), nextPreparation);
|
||||
|
||||
if (nodeMap.TryGetValue(key, out var neighbor))
|
||||
{
|
||||
if (neighbor.IsClosed)
|
||||
continue;
|
||||
if (tentativeG >= neighbor.GCost)
|
||||
continue;
|
||||
|
||||
neighbor.GCost = tentativeG;
|
||||
neighbor.Parent = current;
|
||||
neighbor.MoveUsed = emitted.MoveType;
|
||||
neighbor.ParkourProfile = emitted.ParkourProfile;
|
||||
neighbor.EntryPreparation = nextPreparation;
|
||||
if (neighbor.IsOpen)
|
||||
openSet.Update(neighbor);
|
||||
}
|
||||
else
|
||||
{
|
||||
neighbor = new PathNode(nx, ny, nz)
|
||||
{
|
||||
GCost = tentativeG,
|
||||
HCost = goal.Heuristic(nx, ny, nz),
|
||||
Parent = current,
|
||||
MoveUsed = emitted.MoveType,
|
||||
ParkourProfile = emitted.ParkourProfile,
|
||||
EntryPreparation = nextPreparation,
|
||||
IsOpen = true
|
||||
};
|
||||
nodeMap[key] = neighbor;
|
||||
openSet.Insert(neighbor);
|
||||
}
|
||||
|
||||
double partialScore = neighbor.HCost + neighbor.GCost * 0.5;
|
||||
if (partialScore < bestPartialScore)
|
||||
{
|
||||
bestPartialScore = partialScore;
|
||||
bestPartialNode = neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -292,6 +361,118 @@ namespace MinecraftClient.Pathing.Core
|
|||
return PathResult.Fail(nodesExplored, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
private EntryPreparationState ResolveEntryPreparation(
|
||||
PathNode current, MoveType moveType, int destX, int destY, int destZ)
|
||||
{
|
||||
EntryPreparationState advanced = AdvanceExistingPreparation(current, moveType, destX, destY, destZ);
|
||||
if (!advanced.IsNone)
|
||||
return advanced;
|
||||
|
||||
if (TryStartSidewallRunupPreparation(current, moveType, destX, destY, destZ, out EntryPreparationState started))
|
||||
return started;
|
||||
|
||||
return EntryPreparationState.None;
|
||||
}
|
||||
|
||||
private static EntryPreparationState AdvanceExistingPreparation(
|
||||
PathNode current, MoveType moveType, int destX, int destY, int destZ)
|
||||
{
|
||||
EntryPreparationState state = current.EntryPreparation;
|
||||
if (state.IsNone)
|
||||
return EntryPreparationState.None;
|
||||
|
||||
if (moveType != MoveType.Traverse || destY != current.Y)
|
||||
return EntryPreparationState.None;
|
||||
|
||||
int stepX = destX - current.X;
|
||||
int stepZ = destZ - current.Z;
|
||||
|
||||
if (state.BackwardSteps < state.RequiredSteps
|
||||
&& stepX == -state.ForwardX
|
||||
&& stepZ == -state.ForwardZ)
|
||||
{
|
||||
return state.AdvanceBackward();
|
||||
}
|
||||
|
||||
if (state.BackwardSteps == state.RequiredSteps
|
||||
&& state.ReturnSteps < state.RequiredSteps
|
||||
&& stepX == state.ForwardX
|
||||
&& stepZ == state.ForwardZ)
|
||||
{
|
||||
EntryPreparationState nextState = state.AdvanceReturn();
|
||||
if (nextState.IsPrepared
|
||||
&& (destX != state.OriginX
|
||||
|| destY != state.OriginY
|
||||
|| destZ != state.OriginZ))
|
||||
{
|
||||
return EntryPreparationState.None;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
return EntryPreparationState.None;
|
||||
}
|
||||
|
||||
private static bool TryStartSidewallRunupPreparation(
|
||||
PathNode current, MoveType moveType, int destX, int destY, int destZ, out EntryPreparationState state)
|
||||
{
|
||||
state = EntryPreparationState.None;
|
||||
|
||||
if (!current.EntryPreparation.IsNone
|
||||
|| moveType != MoveType.Traverse
|
||||
|| destY != current.Y)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int stepX = destX - current.X;
|
||||
int stepZ = destZ - current.Z;
|
||||
|
||||
ReadOnlySpan<JumpDescriptor> descriptors = JumpExpander.Descriptors;
|
||||
for (int i = 0; i < descriptors.Length; i++)
|
||||
{
|
||||
JumpDescriptor candidate = descriptors[i];
|
||||
if (candidate.Flavor != JumpFlavor.Sidewall)
|
||||
continue;
|
||||
|
||||
if (!ParkourFeasibility.TryGetRequiredStaticEntryRunupSteps(
|
||||
current.MoveUsed,
|
||||
candidate.XOffset,
|
||||
candidate.ZOffset,
|
||||
candidate.YDelta,
|
||||
out int requiredSteps))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ParkourFeasibility.GetSidewallAxes(
|
||||
candidate.XOffset,
|
||||
candidate.ZOffset,
|
||||
out int forwardX,
|
||||
out int forwardZ,
|
||||
out _,
|
||||
out _);
|
||||
|
||||
if (stepX != -forwardX || stepZ != -forwardZ)
|
||||
continue;
|
||||
|
||||
state = new EntryPreparationState(
|
||||
EntryPreparationKind.SidewallRunup,
|
||||
current.X,
|
||||
current.Y,
|
||||
current.Z,
|
||||
forwardX,
|
||||
forwardZ,
|
||||
(byte)requiredSteps,
|
||||
BackwardSteps: 1,
|
||||
ReturnSteps: 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<PathNode> ReconstructPath(PathNode end)
|
||||
{
|
||||
var path = new List<PathNode>();
|
||||
|
|
|
|||
53
MinecraftClient/Pathing/Moves/IMoveExpander.cs
Normal file
53
MinecraftClient/Pathing/Moves/IMoveExpander.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// A feasible neighbor emitted by an <see cref="IMoveExpander"/>. Carries the
|
||||
/// per-node data the A* main loop needs to update its open set (destination,
|
||||
/// cost, parkour profile, move type).
|
||||
/// </summary>
|
||||
public readonly struct MoveNeighbor
|
||||
{
|
||||
public readonly int DestX;
|
||||
public readonly int DestY;
|
||||
public readonly int DestZ;
|
||||
public readonly double Cost;
|
||||
public readonly ParkourProfile ParkourProfile;
|
||||
public readonly MoveType MoveType;
|
||||
|
||||
public MoveNeighbor(in MoveResult result, MoveType moveType)
|
||||
{
|
||||
DestX = result.DestX;
|
||||
DestY = result.DestY;
|
||||
DestZ = result.DestZ;
|
||||
Cost = result.Cost;
|
||||
ParkourProfile = result.ParkourProfile;
|
||||
MoveType = moveType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits feasible neighbors from a given node. Replaces the old
|
||||
/// "iterate every pre-instantiated IMove" pattern: A* asks each expander to
|
||||
/// fill a stack-allocated buffer with all feasible neighbors, and the
|
||||
/// expander can prune whole categories (e.g. skip Sidewall when no wall is
|
||||
/// near) without instantiating per-direction IMove objects.
|
||||
/// </summary>
|
||||
public interface IMoveExpander
|
||||
{
|
||||
/// <summary>
|
||||
/// Probe the world and populate <paramref name="buffer"/> with feasible
|
||||
/// neighbors. Returns the number of neighbors written. Implementations
|
||||
/// must not write past the buffer; callers must size it to the expander's
|
||||
/// max output.
|
||||
/// </summary>
|
||||
int Expand(CalculationContext ctx, int x, int y, int z, Span<MoveNeighbor> buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Upper bound on the number of neighbors this expander can emit from a
|
||||
/// single node. Used by the driver to size the per-node buffer.
|
||||
/// </summary>
|
||||
int MaxNeighbors { get; }
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Jump up 1 block in a cardinal direction.
|
||||
/// Requires: headroom at (x, y+2, z), body space at dest (y+1, y+2), ground at dest (y).
|
||||
/// </summary>
|
||||
public sealed class MoveAscend : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Ascend;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveAscend(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;
|
||||
int destY = y + 1;
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, destY, destZ) || !ctx.CanWalkThrough(destX, destY + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, y, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = ctx.SprintCost + ctx.JumpPenalty;
|
||||
result.Set(destX, destY, destZ, cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Diagonal walk (1 block in both X and Z, same Y).
|
||||
/// 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
|
||||
{
|
||||
public MoveType Type => MoveType.Diagonal;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveDiagonal(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;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, y - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
double cost = ctx.SprintCost * ActionCosts.DiagonalMultiplier;
|
||||
if (!sideX || !sideZ)
|
||||
cost = ctx.WalkCost * ActionCosts.DiagonalMultiplier;
|
||||
|
||||
result.Set(destX, y, destZ, cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Jump diagonally (1 block in X and Z) and land 1 block higher.
|
||||
/// Handles the "corner jump" pattern: jump around a wall edge and land
|
||||
/// one block higher on a platform that is diagonally adjacent.
|
||||
/// </summary>
|
||||
public sealed class MoveDiagonalAscend : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Ascend;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveDiagonalAscend(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;
|
||||
int destY = y + 1;
|
||||
|
||||
// Need headroom to jump (y+2 at start)
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Destination: solid ground, body passable, head passable
|
||||
if (!ctx.CanWalkOn(destX, y, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, destY, destZ) ||
|
||||
!ctx.CanWalkThrough(destX, destY + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// At least one of the two intermediate cardinal directions must be passable
|
||||
// at both the current and destination height (player sweeps through).
|
||||
bool pathViaX = ctx.CanWalkThrough(x + XOffset, y, z) &&
|
||||
ctx.CanWalkThrough(x + XOffset, y + 1, z) &&
|
||||
ctx.CanWalkThrough(x + XOffset, y + 2, z);
|
||||
bool pathViaZ = ctx.CanWalkThrough(x, y, z + ZOffset) &&
|
||||
ctx.CanWalkThrough(x, y + 1, z + ZOffset) &&
|
||||
ctx.CanWalkThrough(x, y + 2, z + ZOffset);
|
||||
|
||||
if (!pathViaX && !pathViaZ)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = ctx.SprintCost * ActionCosts.DiagonalMultiplier + ctx.JumpPenalty;
|
||||
result.Set(destX, destY, destZ, cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Walk diagonally (1 block in X and Z) and drop 1 block.
|
||||
/// Handles the "corner drop" pattern: step around a wall edge and land
|
||||
/// one block lower on a platform that is diagonally adjacent.
|
||||
/// </summary>
|
||||
public sealed class MoveDiagonalDescend : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Descend;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveDiagonalDescend(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;
|
||||
int destY = y - 1;
|
||||
|
||||
// Destination must have ground, body space, and head space
|
||||
if (!ctx.CanWalkOn(destX, destY - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, destY, destZ) ||
|
||||
!ctx.CanWalkThrough(destX, destY + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material landOn = ctx.GetMaterial(destX, destY - 1, destZ);
|
||||
if (MoveHelper.IsHazardous(landOn))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't descend from climbable blocks
|
||||
Material fromDown = ctx.GetMaterial(x, y - 1, z);
|
||||
if (fromDown.CanBeClimbedOn())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// At least one of the two intermediate cardinal directions must be passable
|
||||
// (player needs clearance to cut the corner).
|
||||
bool pathViaX = ctx.CanWalkThrough(x + XOffset, y, z) &&
|
||||
ctx.CanWalkThrough(x + XOffset, y + 1, z);
|
||||
bool pathViaZ = ctx.CanWalkThrough(x, y, z + ZOffset) &&
|
||||
ctx.CanWalkThrough(x, y + 1, z + ZOffset);
|
||||
|
||||
if (!pathViaX && !pathViaZ)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = ActionCosts.WalkOffBlock * ActionCosts.DiagonalMultiplier
|
||||
+ ActionCosts.FallCost(1);
|
||||
result.Set(destX, destY, destZ, cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
MinecraftClient/Pathing/Moves/Impl/MoveJump.cs
Normal file
79
MinecraftClient/Pathing/Moves/Impl/MoveJump.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Unified jump-family move. A single IMove implementation that dispatches
|
||||
/// on <see cref="JumpFlavor"/> to cover every combination previously
|
||||
/// implemented by seven separate classes (Traverse, Diagonal, Ascend,
|
||||
/// DiagonalAscend, DiagonalDescend, Parkour, SidewallParkour).
|
||||
///
|
||||
/// All feasibility and cost logic lives in <see cref="JumpFeasibility"/>.
|
||||
/// Factory helpers (<see cref="Traverse"/>, <see cref="Parkour"/>, ...)
|
||||
/// produce the right descriptor for each use site without requiring
|
||||
/// callers to remember the mapping between flavor and <see cref="MoveType"/>.
|
||||
/// </summary>
|
||||
public sealed class MoveJump : IMove
|
||||
{
|
||||
public JumpDescriptor Descriptor { get; }
|
||||
public MoveType Type { get; }
|
||||
public int XOffset => Descriptor.XOffset;
|
||||
public int ZOffset => Descriptor.ZOffset;
|
||||
public int YDelta => Descriptor.YDelta;
|
||||
public JumpFlavor Flavor => Descriptor.Flavor;
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveJump(JumpDescriptor descriptor)
|
||||
{
|
||||
Descriptor = descriptor;
|
||||
Type = DeriveMoveType(descriptor);
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
=> JumpFeasibility.Evaluate(ctx, x, y, z, Descriptor, ref result);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
double horiz = Math.Sqrt((double)(XOffset * XOffset + ZOffset * ZOffset));
|
||||
return $"MoveJump({Flavor}, off=({XOffset},{ZOffset}), dy={YDelta}, dist={horiz:F2})";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Factory helpers
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
public static MoveJump Traverse(int dx, int dz)
|
||||
=> new(new JumpDescriptor(dx, dz, 0, JumpFlavor.Walk));
|
||||
|
||||
public static MoveJump Diagonal(int dx, int dz)
|
||||
=> new(new JumpDescriptor(dx, dz, 0, JumpFlavor.Walk));
|
||||
|
||||
public static MoveJump Ascend(int dx, int dz)
|
||||
=> new(new JumpDescriptor(dx, dz, 1, JumpFlavor.Step));
|
||||
|
||||
public static MoveJump DiagonalAscend(int dx, int dz)
|
||||
=> new(new JumpDescriptor(dx, dz, 1, JumpFlavor.Step));
|
||||
|
||||
public static MoveJump DiagonalDescend(int dx, int dz)
|
||||
=> new(new JumpDescriptor(dx, dz, -1, JumpFlavor.Step));
|
||||
|
||||
public static MoveJump Parkour(int dx, int dz, int yDelta = 0)
|
||||
=> new(new JumpDescriptor(dx, dz, yDelta, JumpFlavor.SprintJump));
|
||||
|
||||
public static MoveJump Sidewall(int dx, int dz, int yDelta = 0)
|
||||
=> new(new JumpDescriptor(dx, dz, yDelta, JumpFlavor.Sidewall));
|
||||
|
||||
private static MoveType DeriveMoveType(JumpDescriptor d)
|
||||
{
|
||||
return d.Flavor switch
|
||||
{
|
||||
JumpFlavor.Walk => d.IsCardinal ? MoveType.Traverse : MoveType.Diagonal,
|
||||
JumpFlavor.Step => d.YDelta > 0 ? MoveType.Ascend : MoveType.Descend,
|
||||
JumpFlavor.SprintJump => MoveType.Parkour,
|
||||
JumpFlavor.Sidewall => MoveType.Parkour,
|
||||
_ => MoveType.Traverse,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Moves;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Sprint jump across a gap in cardinal or diagonal direction.
|
||||
/// 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
|
||||
{
|
||||
public MoveType Type => MoveType.Parkour;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
private readonly int _yDelta;
|
||||
|
||||
/// <summary>
|
||||
/// Create a parkour move with direct XZ offsets.
|
||||
/// For cardinal: one of xOff/zOff is 0, the other is 2..4.
|
||||
/// For diagonal: both non-zero, actual distance should be within sprint jump range.
|
||||
/// </summary>
|
||||
public MoveParkour(int xOff, int zOff, int yDelta = 0)
|
||||
{
|
||||
XOffset = xOff;
|
||||
ZOffset = zOff;
|
||||
_yDelta = yDelta;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
if (!ctx.AllowParkour)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_yDelta > 0 && !ctx.AllowParkourAscend)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_yDelta < 0 && -_yDelta > ctx.MaxFallHeight)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanSprint)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
bool cardinal = (XOffset == 0) != (ZOffset == 0);
|
||||
if (cardinal)
|
||||
{
|
||||
int distance = Math.Max(Math.Abs(XOffset), Math.Abs(ZOffset));
|
||||
int maxDistance = _yDelta switch
|
||||
{
|
||||
> 0 => 3,
|
||||
< 0 => 5,
|
||||
_ => 5,
|
||||
};
|
||||
|
||||
if (distance > maxDistance)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't parkour from climbable blocks (unreliable jump)
|
||||
Material standingOn = ctx.GetMaterial(x, y - 1, z);
|
||||
if (standingOn.CanBeClimbedOn())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasRunUp(ctx, x, y, z, XOffset, ZOffset, _yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
int destX = x + XOffset;
|
||||
int destZ = z + ZOffset;
|
||||
int destY = y + _yDelta;
|
||||
|
||||
// Head clearance at start (need room to jump)
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't jump out of liquid
|
||||
Material atFeet = ctx.GetMaterial(x, y, z);
|
||||
if (atFeet.IsLiquid())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Destination must be standable and passable
|
||||
if (!ctx.CanWalkOn(destX, destY - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, destY, destZ) ||
|
||||
!ctx.CanWalkThrough(destX, destY + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ParkourFeasibility.HasIntermediateLandingConflict(ctx, x, y, z, XOffset, ZOffset, _yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
int xSign = Math.Sign(XOffset);
|
||||
int zSign = Math.Sign(ZOffset);
|
||||
int xAbs = Math.Abs(XOffset);
|
||||
int zAbs = Math.Abs(ZOffset);
|
||||
|
||||
// Check intermediate blocks along the flight path.
|
||||
// Cardinal: check all blocks in the column along the primary axis.
|
||||
// Diagonal: check blocks along the diagonal strip, not the full rectangle.
|
||||
// Player AABB is 0.6 wide, so only blocks near the diagonal line matter.
|
||||
if (!CheckFlightPath(ctx, x, y, z, xSign, zSign, xAbs, zAbs))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Gap check: first block(s) adjacent to start must lack ground.
|
||||
// If ground exists there, A* can find a walking path instead.
|
||||
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
|
||||
{
|
||||
// Diagonal: the diagonally adjacent block must lack ground
|
||||
if (ctx.CanWalkOn(x + xSign, y - 1, z + zSign))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasDiagonalShoulderClearance(ctx, x, y, z, XOffset, ZOffset))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasCardinalSideClearance(ctx, x, y, z, XOffset, ZOffset))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasLandingOvershootClearance(
|
||||
ctx, destX, destY, destZ, xSign, zSign))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cost model following Baritone:
|
||||
// dist 2-3: walk speed * distance (jump is roughly time-neutral vs walking)
|
||||
// dist 4: sprint speed * distance (must sprint, covers ground faster)
|
||||
// ascend: always sprint speed (sprinting required)
|
||||
double horizDist = Math.Sqrt((double)(XOffset * XOffset + ZOffset * ZOffset));
|
||||
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
|
||||
cost = horizDist * ctx.WalkCost + ctx.JumpPenalty;
|
||||
|
||||
result.Set(destX, destY, destZ, cost, ParkourProfile.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check body clearance along the flight path from start toward the destination.
|
||||
/// For cardinal moves, checks a straight line. For diagonal moves, checks
|
||||
/// only blocks near the actual diagonal trajectory rather than the full bounding
|
||||
/// rectangle, allowing jumps that pass a wall on one side.
|
||||
/// </summary>
|
||||
private bool CheckFlightPath(
|
||||
CalculationContext ctx, int x, int y, int z,
|
||||
int xSign, int zSign, int xAbs, int zAbs)
|
||||
{
|
||||
if (xAbs == 0 || zAbs == 0)
|
||||
{
|
||||
// Cardinal: single axis, check each block along the line
|
||||
for (int step = 1; step < Math.Max(xAbs, zAbs); step++)
|
||||
{
|
||||
int gx = x + xSign * (xAbs > 0 ? step : 0);
|
||||
int gz = z + zSign * (zAbs > 0 ? step : 0);
|
||||
if (!ClearColumn(ctx, gx, y, gz))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Diagonal: walk the diagonal and check each block the AABB touches.
|
||||
// At each step t along the diagonal, the player center is near
|
||||
// (x + t*xSign, z + t*zSign). The AABB extends 0.3 blocks each side,
|
||||
// so check the diagonal cell and one neighbor on each axis-aligned side
|
||||
// only when the trajectory is close to a cell boundary (always for short
|
||||
// diagonals). We enumerate cells by stepping through the longer axis
|
||||
// and computing the corresponding position on the shorter axis.
|
||||
int maxSteps = Math.Max(xAbs, zAbs);
|
||||
for (int step = 1; step < maxSteps; step++)
|
||||
{
|
||||
// Proportional position along each axis
|
||||
double fx = (double)step * xAbs / maxSteps;
|
||||
double fz = (double)step * zAbs / maxSteps;
|
||||
|
||||
int ix = (int)Math.Round(fx);
|
||||
int iz = (int)Math.Round(fz);
|
||||
|
||||
int gx = x + xSign * ix;
|
||||
int gz = z + zSign * iz;
|
||||
|
||||
if (!ClearColumn(ctx, gx, y, gz))
|
||||
return false;
|
||||
|
||||
// Also check the neighboring cell across the shorter axis when close
|
||||
// to a cell boundary (player AABB overlaps adjacent cell)
|
||||
if (xAbs != zAbs)
|
||||
{
|
||||
double fracX = fx - Math.Floor(fx);
|
||||
double fracZ = fz - Math.Floor(fz);
|
||||
if (fracX > 0.2 && fracX < 0.8 && ix > 0 && ix < xAbs)
|
||||
{
|
||||
if (!ClearColumn(ctx, x + xSign * (ix - 1), y, gz))
|
||||
return false;
|
||||
}
|
||||
if (fracZ > 0.2 && fracZ < 0.8 && iz > 0 && iz < zAbs)
|
||||
{
|
||||
if (!ClearColumn(ctx, gx, y, z + zSign * (iz - 1)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ClearColumn(CalculationContext ctx, int gx, int y, int gz)
|
||||
{
|
||||
if (!ctx.CanWalkThrough(gx, y, gz) ||
|
||||
!ctx.CanWalkThrough(gx, y + 1, gz) ||
|
||||
!ctx.CanWalkThrough(gx, y + 2, gz))
|
||||
return false;
|
||||
if (_yDelta > 0 && !ctx.CanWalkThrough(gx, y + 3, gz))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
double dist = Math.Sqrt((double)(XOffset * XOffset + ZOffset * ZOffset));
|
||||
return $"MoveParkour(off=({XOffset},{ZOffset}), dy={_yDelta}, dist={dist:F1})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
public sealed class MoveSidewallParkour : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Parkour;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
private readonly int _yDelta;
|
||||
|
||||
public MoveSidewallParkour(int xOffset, int zOffset, int yDelta = 0)
|
||||
{
|
||||
XOffset = xOffset;
|
||||
ZOffset = zOffset;
|
||||
_yDelta = yDelta;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
if (!ctx.AllowParkour || !ctx.CanSprint)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_yDelta > 0 && !ctx.AllowParkourAscend)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_yDelta < 0 && -_yDelta > ctx.MaxFallHeight)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.IsSidewallProfile(XOffset, ZOffset, _yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material standingOn = ctx.GetMaterial(x, y - 1, z);
|
||||
if (standingOn.CanBeClimbedOn())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material atFeet = ctx.GetMaterial(x, y, z);
|
||||
if (atFeet.IsLiquid())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourFeasibility.GetSidewallAxes(XOffset, ZOffset, out int forwardX, out int forwardZ, out int lateralX, out int lateralZ);
|
||||
|
||||
int destX = x + XOffset;
|
||||
int destY = y + _yDelta;
|
||||
int destZ = z + ZOffset;
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasDominantAxisRunUp(ctx, x, y, z, forwardX, forwardZ, XOffset, ZOffset, _yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasSidewallArcClearance(ctx, x, y, z, forwardX, forwardZ, lateralX, lateralZ, XOffset, ZOffset, _yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasSidewallLandingClearance(ctx, destX, destY, destZ, forwardX, forwardZ, lateralX, lateralZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double horizDist = Math.Sqrt((double)(XOffset * XOffset + ZOffset * ZOffset));
|
||||
double cost = _yDelta switch
|
||||
{
|
||||
> 0 => horizDist * ctx.SprintCost + ctx.JumpPenalty * 2,
|
||||
< 0 => horizDist * ctx.SprintCost + ctx.JumpPenalty + ActionCosts.FallCost(-_yDelta),
|
||||
_ => horizDist * ctx.SprintCost + ctx.JumpPenalty,
|
||||
};
|
||||
|
||||
result.Set(destX, destY, destZ, cost, ParkourProfile.Sidewall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Flat cardinal walk (1 block in +/-X or +/-Z, same Y).
|
||||
/// Checks body+head passable and ground below destination.
|
||||
/// </summary>
|
||||
public sealed class MoveTraverse : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Traverse;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
public MoveTraverse(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))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, y + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, y - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = ctx.SprintCost;
|
||||
|
||||
var destFloorMat = ctx.GetMaterial(destX, y - 1, destZ);
|
||||
if (destFloorMat == Mapping.Material.SoulSand)
|
||||
cost *= 1.0 / Physics.PhysicsConsts.SoulSandSpeedFactor;
|
||||
|
||||
result.Set(destX, y, destZ, cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
MinecraftClient/Pathing/Moves/JumpDescriptor.cs
Normal file
56
MinecraftClient/Pathing/Moves/JumpDescriptor.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
namespace MinecraftClient.Pathing.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Kind of jump-family move. Each flavor selects a different evaluator path
|
||||
/// inside <see cref="JumpFeasibility"/> while sharing low-level primitives
|
||||
/// (head clearance, destination clearance, flight-path sweep, cost model).
|
||||
/// </summary>
|
||||
public enum JumpFlavor
|
||||
{
|
||||
/// <summary>
|
||||
/// Single-block cardinal or diagonal walk at the same Y. No jump input.
|
||||
/// Covers the old <c>MoveTraverse</c> and <c>MoveDiagonal</c>.
|
||||
/// </summary>
|
||||
Walk,
|
||||
|
||||
/// <summary>
|
||||
/// Single-block vertical step (dy = +1 up or dy = -1 down), cardinal or
|
||||
/// diagonal. Covers <c>MoveAscend</c>, <c>MoveDiagonalAscend</c>, and
|
||||
/// <c>MoveDiagonalDescend</c>.
|
||||
/// </summary>
|
||||
Step,
|
||||
|
||||
/// <summary>
|
||||
/// Multi-block sprint jump, cardinal or diagonal. Covers <c>MoveParkour</c>.
|
||||
/// </summary>
|
||||
SprintJump,
|
||||
|
||||
/// <summary>
|
||||
/// Dominant-axis sprint jump that uses an inner wall for support. Covers
|
||||
/// <c>MoveSidewallParkour</c>.
|
||||
/// </summary>
|
||||
Sidewall,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fully describes a single jump-family move (Walk / Step / SprintJump /
|
||||
/// Sidewall). All geometry that downstream planners or templates need can be
|
||||
/// derived from this value, so A* only needs to enumerate descriptors rather
|
||||
/// than hard-coded IMove subclasses.
|
||||
/// </summary>
|
||||
public readonly record struct JumpDescriptor(
|
||||
int XOffset,
|
||||
int ZOffset,
|
||||
int YDelta,
|
||||
JumpFlavor Flavor)
|
||||
{
|
||||
public bool IsCardinal => (XOffset == 0) != (ZOffset == 0);
|
||||
|
||||
public bool IsDiagonal => XOffset != 0 && ZOffset != 0;
|
||||
|
||||
public int HorizontalMajor
|
||||
=> System.Math.Max(System.Math.Abs(XOffset), System.Math.Abs(ZOffset));
|
||||
|
||||
public int HorizontalMinor
|
||||
=> System.Math.Min(System.Math.Abs(XOffset), System.Math.Abs(ZOffset));
|
||||
}
|
||||
277
MinecraftClient/Pathing/Moves/JumpExpander.cs
Normal file
277
MinecraftClient/Pathing/Moves/JumpExpander.cs
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Moves.Impl;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic expander for every move in the jump family (Walk, Step,
|
||||
/// SprintJump, Sidewall). Iterates a declarative descriptor table and calls
|
||||
/// <see cref="JumpFeasibility.Evaluate"/> for each entry without allocating
|
||||
/// an IMove object per direction.
|
||||
///
|
||||
/// The hot path hoists per-node guards (AllowParkour, head clearance,
|
||||
/// takeoff material, adjacent-wall presence) and precomputes an 8-direction
|
||||
/// "first-step has no floor" table so entire descriptor groups can be
|
||||
/// rejected in O(1) before touching <see cref="JumpFeasibility"/>. Ordinary
|
||||
/// ground-walking nodes skip all ~170 jump descriptors this way; nodes
|
||||
/// without any adjacent wall skip all 112 sidewall descriptors.
|
||||
/// </summary>
|
||||
public sealed class JumpExpander : IMoveExpander
|
||||
{
|
||||
private static readonly JumpDescriptor[] _descriptors = BuildDescriptors();
|
||||
|
||||
public int MaxNeighbors => _descriptors.Length;
|
||||
|
||||
public int Expand(CalculationContext ctx, int x, int y, int z, Span<MoveNeighbor> buffer)
|
||||
{
|
||||
int count = 0;
|
||||
MoveResult result = default;
|
||||
|
||||
// ---- Per-node preconditions (shared by every SprintJump + Sidewall descriptor) ----
|
||||
// These are the first checks JumpFeasibility.Evaluate* would make. Hoisting
|
||||
// them once turns ~170 method calls per node into one branch in the hot path.
|
||||
bool jumpFamilyAllowed = ctx.AllowParkour && ctx.CanSprint;
|
||||
bool canSprintTakeoff = false;
|
||||
bool hasAdjacentWall = false;
|
||||
if (jumpFamilyAllowed)
|
||||
{
|
||||
Material standingOn = ctx.GetMaterial(x, y - 1, z);
|
||||
Material atFeet = ctx.GetMaterial(x, y, z);
|
||||
canSprintTakeoff =
|
||||
!standingOn.CanBeClimbedOn()
|
||||
&& !atFeet.IsLiquid()
|
||||
&& ctx.CanWalkThrough(x, y + 2, z);
|
||||
|
||||
if (canSprintTakeoff)
|
||||
hasAdjacentWall = HasAnyAdjacentWall(ctx, x, y, z);
|
||||
}
|
||||
|
||||
// ---- Per-direction gap table (SprintJump only) ----
|
||||
// Gap check: "first block adjacent to start must lack ground" so A* can't
|
||||
// pick a cheaper walking path. For an octant (sx, sz) the cell is at
|
||||
// (x+sx, y-1, z+sz). Index = (sx+1)*3 + (sz+1) over sx,sz in {-1,0,1}.
|
||||
// If the floor is present for a direction, every SprintJump descriptor in
|
||||
// that octant is infeasible. 9 slots (center slot 4 unused) fit cleanly
|
||||
// on the stack.
|
||||
Span<bool> directionGapOpen = stackalloc bool[9];
|
||||
if (canSprintTakeoff)
|
||||
{
|
||||
for (int dx = -1; dx <= 1; dx++)
|
||||
{
|
||||
for (int dz = -1; dz <= 1; dz++)
|
||||
{
|
||||
if (dx == 0 && dz == 0)
|
||||
continue;
|
||||
int idx = ((dx + 1) * 3) + (dz + 1);
|
||||
directionGapOpen[idx] = !ctx.CanWalkOn(x + dx, y - 1, z + dz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < _descriptors.Length; i++)
|
||||
{
|
||||
JumpDescriptor desc = _descriptors[i];
|
||||
|
||||
switch (desc.Flavor)
|
||||
{
|
||||
case JumpFlavor.SprintJump:
|
||||
if (!canSprintTakeoff)
|
||||
continue;
|
||||
{
|
||||
int sx = Math.Sign(desc.XOffset);
|
||||
int sz = Math.Sign(desc.ZOffset);
|
||||
int idx = ((sx + 1) * 3) + (sz + 1);
|
||||
if (!directionGapOpen[idx])
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case JumpFlavor.Sidewall:
|
||||
if (!canSprintTakeoff || !hasAdjacentWall)
|
||||
continue;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
result.Cost = 0;
|
||||
JumpFeasibility.Evaluate(ctx, x, y, z, desc, ref result);
|
||||
if (result.IsImpossible)
|
||||
continue;
|
||||
|
||||
MoveType type = DeriveMoveType(desc);
|
||||
if (count < buffer.Length)
|
||||
buffer[count++] = new MoveNeighbor(result, type);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Conservative O(1) short-circuit for the Sidewall family. Every sidewall
|
||||
/// descriptor needs a solid block one lateral step from the takeoff at
|
||||
/// <c>y</c> or <c>y+1</c>, i.e. at a cardinal neighbor. If all four
|
||||
/// cardinal neighbors at both heights are walk-through, there is no wall
|
||||
/// to cling to and all 112 sidewall descriptors can be skipped without
|
||||
/// calling <see cref="JumpFeasibility"/>.
|
||||
/// </summary>
|
||||
private static bool HasAnyAdjacentWall(CalculationContext ctx, int x, int y, int z)
|
||||
{
|
||||
return !ctx.CanWalkThrough(x + 1, y, z) || !ctx.CanWalkThrough(x + 1, y + 1, z)
|
||||
|| !ctx.CanWalkThrough(x - 1, y, z) || !ctx.CanWalkThrough(x - 1, y + 1, z)
|
||||
|| !ctx.CanWalkThrough(x, y, z + 1) || !ctx.CanWalkThrough(x, y + 1, z + 1)
|
||||
|| !ctx.CanWalkThrough(x, y, z - 1) || !ctx.CanWalkThrough(x, y + 1, z - 1);
|
||||
}
|
||||
|
||||
private static MoveType DeriveMoveType(JumpDescriptor d) => d.Flavor switch
|
||||
{
|
||||
JumpFlavor.Walk => d.IsCardinal ? MoveType.Traverse : MoveType.Diagonal,
|
||||
JumpFlavor.Step => d.YDelta > 0 ? MoveType.Ascend : MoveType.Descend,
|
||||
JumpFlavor.SprintJump => MoveType.Parkour,
|
||||
JumpFlavor.Sidewall => MoveType.Parkour,
|
||||
_ => MoveType.Traverse,
|
||||
};
|
||||
|
||||
private static JumpDescriptor[] BuildDescriptors()
|
||||
{
|
||||
var list = new System.Collections.Generic.List<JumpDescriptor>(256);
|
||||
int[] offsets = [1, -1];
|
||||
|
||||
// Cardinal walk + 1-block ascend
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
list.Add(new JumpDescriptor(dx, 0, 0, JumpFlavor.Walk));
|
||||
list.Add(new JumpDescriptor(dx, 0, 1, JumpFlavor.Step));
|
||||
}
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
list.Add(new JumpDescriptor(0, dz, 0, JumpFlavor.Walk));
|
||||
list.Add(new JumpDescriptor(0, dz, 1, JumpFlavor.Step));
|
||||
}
|
||||
|
||||
// Diagonal walk + diagonal ascend/descend
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
list.Add(new JumpDescriptor(dx, dz, 0, JumpFlavor.Walk));
|
||||
list.Add(new JumpDescriptor(dx, dz, 1, JumpFlavor.Step));
|
||||
list.Add(new JumpDescriptor(dx, dz, -1, JumpFlavor.Step));
|
||||
}
|
||||
}
|
||||
|
||||
// Cardinal parkour (flat / +1 / -1 / -2)
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(dx * d, 0, 0, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 3; d++)
|
||||
list.Add(new JumpDescriptor(dx * d, 0, 1, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(dx * d, 0, -1, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(dx * d, 0, -2, JumpFlavor.SprintJump));
|
||||
}
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(0, dz * d, 0, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 3; d++)
|
||||
list.Add(new JumpDescriptor(0, dz * d, 1, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(0, dz * d, -1, JumpFlavor.SprintJump));
|
||||
for (int d = 2; d <= 5; d++)
|
||||
list.Add(new JumpDescriptor(0, dz * d, -2, JumpFlavor.SprintJump));
|
||||
}
|
||||
|
||||
// Diagonal parkour
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
list.Add(new JumpDescriptor(dx * 2, dz * 1, 0, JumpFlavor.SprintJump));
|
||||
list.Add(new JumpDescriptor(dx * 1, dz * 2, 0, JumpFlavor.SprintJump));
|
||||
list.Add(new JumpDescriptor(dx * 2, dz * 2, 0, JumpFlavor.SprintJump));
|
||||
list.Add(new JumpDescriptor(dx * 3, dz * 1, 0, JumpFlavor.SprintJump));
|
||||
list.Add(new JumpDescriptor(dx * 1, dz * 3, 0, JumpFlavor.SprintJump));
|
||||
|
||||
list.Add(new JumpDescriptor(dx * 2, dz * 1, -1, JumpFlavor.SprintJump));
|
||||
list.Add(new JumpDescriptor(dx * 1, dz * 2, -1, JumpFlavor.SprintJump));
|
||||
list.Add(new JumpDescriptor(dx * 2, dz * 2, -1, JumpFlavor.SprintJump));
|
||||
|
||||
list.Add(new JumpDescriptor(dx * 2, dz * 1, 1, JumpFlavor.SprintJump));
|
||||
list.Add(new JumpDescriptor(dx * 1, dz * 2, 1, JumpFlavor.SprintJump));
|
||||
list.Add(new JumpDescriptor(dx * 2, dz * 2, 1, JumpFlavor.SprintJump));
|
||||
}
|
||||
}
|
||||
|
||||
// Sidewall parkour
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
foreach (int distance in new[] { 2, 3, 4, 5 })
|
||||
{
|
||||
list.Add(new JumpDescriptor(dx, dz * distance, 0, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx * distance, dz, 0, JumpFlavor.Sidewall));
|
||||
|
||||
if (distance <= 3)
|
||||
{
|
||||
list.Add(new JumpDescriptor(dx, dz * distance, 1, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx * distance, dz, 1, JumpFlavor.Sidewall));
|
||||
}
|
||||
|
||||
list.Add(new JumpDescriptor(dx, dz * distance, -1, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx * distance, dz, -1, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx, dz * distance, -2, JumpFlavor.Sidewall));
|
||||
list.Add(new JumpDescriptor(dx * distance, dz, -2, JumpFlavor.Sidewall));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-only snapshot of the descriptor table used by this expander. Exposed
|
||||
/// for callers that need to enumerate the jump family directly (e.g. A*'s
|
||||
/// sidewall-runup preparation logic).
|
||||
/// </summary>
|
||||
public static ReadOnlySpan<JumpDescriptor> Descriptors => _descriptors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thin adapter that wraps an array of legacy <see cref="IMove"/> instances as
|
||||
/// an <see cref="IMoveExpander"/>. Used for the dynamic-landing and vertical
|
||||
/// move families (<c>MoveDescend</c>, <c>MoveSprintDescend</c>,
|
||||
/// <c>MoveClimb</c>, <c>MoveFall</c>) which do not fit the JumpDescriptor model.
|
||||
/// </summary>
|
||||
public sealed class LegacyMoveExpander : IMoveExpander
|
||||
{
|
||||
private readonly IMove[] _moves;
|
||||
|
||||
public LegacyMoveExpander(IMove[] moves)
|
||||
{
|
||||
_moves = moves ?? throw new ArgumentNullException(nameof(moves));
|
||||
}
|
||||
|
||||
public int MaxNeighbors => _moves.Length;
|
||||
|
||||
public int Expand(CalculationContext ctx, int x, int y, int z, Span<MoveNeighbor> buffer)
|
||||
{
|
||||
int count = 0;
|
||||
MoveResult result = default;
|
||||
for (int i = 0; i < _moves.Length; i++)
|
||||
{
|
||||
IMove move = _moves[i];
|
||||
result.Cost = 0;
|
||||
move.Calculate(ctx, x, y, z, ref result);
|
||||
if (result.IsImpossible)
|
||||
continue;
|
||||
|
||||
if (count < buffer.Length)
|
||||
buffer[count++] = new MoveNeighbor(result, move.Type);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
574
MinecraftClient/Pathing/Moves/JumpFeasibility.cs
Normal file
574
MinecraftClient/Pathing/Moves/JumpFeasibility.cs
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves;
|
||||
|
||||
/// <summary>
|
||||
/// Single source of truth for jump-family feasibility and cost. Each
|
||||
/// <see cref="JumpFlavor"/> selects one of the Evaluate* methods; the methods
|
||||
/// share low-level primitives (head clearance, destination clearance,
|
||||
/// flight-path sweep, run-up, cost) so that a physics rule is implemented
|
||||
/// exactly once.
|
||||
/// </summary>
|
||||
internal static class JumpFeasibility
|
||||
{
|
||||
public static void Evaluate(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
JumpDescriptor desc,
|
||||
ref MoveResult result)
|
||||
{
|
||||
switch (desc.Flavor)
|
||||
{
|
||||
case JumpFlavor.Walk:
|
||||
EvaluateWalk(ctx, x, y, z, desc, ref result);
|
||||
return;
|
||||
case JumpFlavor.Step:
|
||||
EvaluateStep(ctx, x, y, z, desc, ref result);
|
||||
return;
|
||||
case JumpFlavor.SprintJump:
|
||||
EvaluateSprintJump(ctx, x, y, z, desc, ref result);
|
||||
return;
|
||||
case JumpFlavor.Sidewall:
|
||||
EvaluateSidewall(ctx, x, y, z, desc, ref result);
|
||||
return;
|
||||
default:
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Walk (dy = 0, single block, cardinal or diagonal)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private static void EvaluateWalk(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
JumpDescriptor desc,
|
||||
ref MoveResult result)
|
||||
{
|
||||
int dx = desc.XOffset;
|
||||
int dz = desc.ZOffset;
|
||||
int destX = x + dx;
|
||||
int destZ = z + dz;
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, y, destZ) || !ctx.CanWalkThrough(destX, y + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, y - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (desc.IsCardinal)
|
||||
{
|
||||
double cost = ctx.SprintCost;
|
||||
Material destFloor = ctx.GetMaterial(destX, y - 1, destZ);
|
||||
if (destFloor == Material.SoulSand)
|
||||
cost *= 1.0 / PhysicsConsts.SoulSandSpeedFactor;
|
||||
result.Set(destX, y, destZ, cost);
|
||||
return;
|
||||
}
|
||||
|
||||
// Diagonal corner walk: need at least one passable side cardinal.
|
||||
bool sideX = ctx.CanWalkThrough(x + dx, y, z) &&
|
||||
ctx.CanWalkThrough(x + dx, y + 1, z);
|
||||
bool sideZ = ctx.CanWalkThrough(x, y, z + dz) &&
|
||||
ctx.CanWalkThrough(x, y + 1, z + dz);
|
||||
|
||||
if (!sideX && !sideZ)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double diagCost = ctx.SprintCost * ActionCosts.DiagonalMultiplier;
|
||||
if (!sideX || !sideZ)
|
||||
diagCost = ctx.WalkCost * ActionCosts.DiagonalMultiplier;
|
||||
|
||||
result.Set(destX, y, destZ, diagCost);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Step (dy = +1 ascend, dy = -1 descend, cardinal or diagonal)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private static void EvaluateStep(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
JumpDescriptor desc,
|
||||
ref MoveResult result)
|
||||
{
|
||||
if (desc.YDelta == 1)
|
||||
EvaluateStepAscend(ctx, x, y, z, desc, ref result);
|
||||
else if (desc.YDelta == -1)
|
||||
EvaluateStepDescend(ctx, x, y, z, desc, ref result);
|
||||
else
|
||||
result.SetImpossible();
|
||||
}
|
||||
|
||||
private static void EvaluateStepAscend(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
JumpDescriptor desc,
|
||||
ref MoveResult result)
|
||||
{
|
||||
int dx = desc.XOffset;
|
||||
int dz = desc.ZOffset;
|
||||
int destX = x + dx;
|
||||
int destZ = z + dz;
|
||||
int destY = y + 1;
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, y, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, destY, destZ) ||
|
||||
!ctx.CanWalkThrough(destX, destY + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (desc.IsCardinal)
|
||||
{
|
||||
double cost = ctx.SprintCost + ctx.JumpPenalty;
|
||||
result.Set(destX, destY, destZ, cost);
|
||||
return;
|
||||
}
|
||||
|
||||
bool pathViaX = ctx.CanWalkThrough(x + dx, y, z) &&
|
||||
ctx.CanWalkThrough(x + dx, y + 1, z) &&
|
||||
ctx.CanWalkThrough(x + dx, y + 2, z);
|
||||
bool pathViaZ = ctx.CanWalkThrough(x, y, z + dz) &&
|
||||
ctx.CanWalkThrough(x, y + 1, z + dz) &&
|
||||
ctx.CanWalkThrough(x, y + 2, z + dz);
|
||||
|
||||
if (!pathViaX && !pathViaZ)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double diagCost = ctx.SprintCost * ActionCosts.DiagonalMultiplier + ctx.JumpPenalty;
|
||||
result.Set(destX, destY, destZ, diagCost);
|
||||
}
|
||||
|
||||
private static void EvaluateStepDescend(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
JumpDescriptor desc,
|
||||
ref MoveResult result)
|
||||
{
|
||||
int dx = desc.XOffset;
|
||||
int dz = desc.ZOffset;
|
||||
int destX = x + dx;
|
||||
int destZ = z + dz;
|
||||
int destY = y - 1;
|
||||
|
||||
if (!ctx.CanWalkOn(destX, destY - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, destY, destZ) ||
|
||||
!ctx.CanWalkThrough(destX, destY + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material landOn = ctx.GetMaterial(destX, destY - 1, destZ);
|
||||
if (MoveHelper.IsHazardous(landOn))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material fromDown = ctx.GetMaterial(x, y - 1, z);
|
||||
if (fromDown.CanBeClimbedOn())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!desc.IsDiagonal)
|
||||
{
|
||||
// Currently only diagonal descend steps exist; cardinal descend is
|
||||
// served by MoveDescend which supports dynamic fall depth.
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
bool pathViaX = ctx.CanWalkThrough(x + dx, y, z) &&
|
||||
ctx.CanWalkThrough(x + dx, y + 1, z);
|
||||
bool pathViaZ = ctx.CanWalkThrough(x, y, z + dz) &&
|
||||
ctx.CanWalkThrough(x, y + 1, z + dz);
|
||||
|
||||
if (!pathViaX && !pathViaZ)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = ActionCosts.WalkOffBlock * ActionCosts.DiagonalMultiplier
|
||||
+ ActionCosts.FallCost(1);
|
||||
result.Set(destX, destY, destZ, cost);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// SprintJump (parkour, horiz >= 2, dy in -2..+1)
|
||||
// Ported 1:1 from MoveParkour.Calculate.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private static void EvaluateSprintJump(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
JumpDescriptor desc,
|
||||
ref MoveResult result)
|
||||
{
|
||||
int xOffset = desc.XOffset;
|
||||
int zOffset = desc.ZOffset;
|
||||
int yDelta = desc.YDelta;
|
||||
|
||||
if (!ctx.AllowParkour)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (yDelta > 0 && !ctx.AllowParkourAscend)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (yDelta < 0 && -yDelta > ctx.MaxFallHeight)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanSprint)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
bool cardinal = (xOffset == 0) != (zOffset == 0);
|
||||
if (cardinal)
|
||||
{
|
||||
int distance = Math.Max(Math.Abs(xOffset), Math.Abs(zOffset));
|
||||
int maxDistance = yDelta switch
|
||||
{
|
||||
> 0 => 3,
|
||||
< 0 => 5,
|
||||
_ => 5,
|
||||
};
|
||||
|
||||
if (distance > maxDistance)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Material standingOn = ctx.GetMaterial(x, y - 1, z);
|
||||
if (standingOn.CanBeClimbedOn())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasRunUp(ctx, x, y, z, xOffset, zOffset, yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
int destX = x + xOffset;
|
||||
int destZ = z + zOffset;
|
||||
int destY = y + yDelta;
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material atFeet = ctx.GetMaterial(x, y, z);
|
||||
if (atFeet.IsLiquid())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkOn(destX, destY - 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.CanWalkThrough(destX, destY, destZ) ||
|
||||
!ctx.CanWalkThrough(destX, destY + 1, destZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ParkourFeasibility.HasIntermediateLandingConflict(ctx, x, y, z, xOffset, zOffset, yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
int xSign = Math.Sign(xOffset);
|
||||
int zSign = Math.Sign(zOffset);
|
||||
int xAbs = Math.Abs(xOffset);
|
||||
int zAbs = Math.Abs(zOffset);
|
||||
|
||||
if (!CheckSprintJumpFlightPath(ctx, x, y, z, xSign, zSign, xAbs, zAbs, yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Gap check: first block(s) adjacent to start must lack ground so A*
|
||||
// cannot take a cheaper walking path.
|
||||
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;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasDiagonalShoulderClearance(ctx, x, y, z, xOffset, zOffset))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasCardinalSideClearance(ctx, x, y, z, xOffset, zOffset))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasLandingOvershootClearance(ctx, destX, destY, destZ, xSign, zSign))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double horizDist = Math.Sqrt((double)((xOffset * xOffset) + (zOffset * zOffset)));
|
||||
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
|
||||
cost = horizDist * ctx.WalkCost + ctx.JumpPenalty;
|
||||
|
||||
result.Set(destX, destY, destZ, cost, ParkourProfile.Default);
|
||||
}
|
||||
|
||||
private static bool CheckSprintJumpFlightPath(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
int xSign, int zSign, int xAbs, int zAbs,
|
||||
int yDelta)
|
||||
{
|
||||
if (xAbs == 0 || zAbs == 0)
|
||||
{
|
||||
for (int step = 1; step < Math.Max(xAbs, zAbs); step++)
|
||||
{
|
||||
int gx = x + xSign * (xAbs > 0 ? step : 0);
|
||||
int gz = z + zSign * (zAbs > 0 ? step : 0);
|
||||
if (!ClearSprintJumpColumn(ctx, gx, y, gz, yDelta))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int maxSteps = Math.Max(xAbs, zAbs);
|
||||
for (int step = 1; step < maxSteps; step++)
|
||||
{
|
||||
double fx = (double)step * xAbs / maxSteps;
|
||||
double fz = (double)step * zAbs / maxSteps;
|
||||
|
||||
int ix = (int)Math.Round(fx);
|
||||
int iz = (int)Math.Round(fz);
|
||||
|
||||
int gx = x + xSign * ix;
|
||||
int gz = z + zSign * iz;
|
||||
|
||||
if (!ClearSprintJumpColumn(ctx, gx, y, gz, yDelta))
|
||||
return false;
|
||||
|
||||
if (xAbs != zAbs)
|
||||
{
|
||||
double fracX = fx - Math.Floor(fx);
|
||||
double fracZ = fz - Math.Floor(fz);
|
||||
if (fracX > 0.2 && fracX < 0.8 && ix > 0 && ix < xAbs)
|
||||
{
|
||||
if (!ClearSprintJumpColumn(ctx, x + xSign * (ix - 1), y, gz, yDelta))
|
||||
return false;
|
||||
}
|
||||
if (fracZ > 0.2 && fracZ < 0.8 && iz > 0 && iz < zAbs)
|
||||
{
|
||||
if (!ClearSprintJumpColumn(ctx, gx, y, z + zSign * (iz - 1), yDelta))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ClearSprintJumpColumn(CalculationContext ctx, int gx, int y, int gz, int yDelta)
|
||||
{
|
||||
if (!ctx.CanWalkThrough(gx, y, gz) ||
|
||||
!ctx.CanWalkThrough(gx, y + 1, gz) ||
|
||||
!ctx.CanWalkThrough(gx, y + 2, gz))
|
||||
return false;
|
||||
if (yDelta > 0 && !ctx.CanWalkThrough(gx, y + 3, gz))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Sidewall (dominant-axis sprint jump with an inner-wall constraint).
|
||||
// Ported 1:1 from MoveSidewallParkour.Calculate.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private static void EvaluateSidewall(
|
||||
CalculationContext ctx,
|
||||
int x, int y, int z,
|
||||
JumpDescriptor desc,
|
||||
ref MoveResult result)
|
||||
{
|
||||
int xOffset = desc.XOffset;
|
||||
int zOffset = desc.ZOffset;
|
||||
int yDelta = desc.YDelta;
|
||||
|
||||
if (!ctx.AllowParkour || !ctx.CanSprint)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (yDelta > 0 && !ctx.AllowParkourAscend)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (yDelta < 0 && -yDelta > ctx.MaxFallHeight)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.IsSidewallProfile(xOffset, zOffset, yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material standingOn = ctx.GetMaterial(x, y - 1, z);
|
||||
if (standingOn.CanBeClimbedOn())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material atFeet = ctx.GetMaterial(x, y, z);
|
||||
if (atFeet.IsLiquid())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourFeasibility.GetSidewallAxes(xOffset, zOffset, out int forwardX, out int forwardZ, out int lateralX, out int lateralZ);
|
||||
|
||||
int destX = x + xOffset;
|
||||
int destY = y + yDelta;
|
||||
int destZ = z + zOffset;
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ParkourFeasibility.TryGetRequiredStaticEntryRunupSteps(ctx.PreviousMoveType, xOffset, zOffset, yDelta, out int requiredSteps))
|
||||
{
|
||||
if (!ParkourFeasibility.HasPreparedRunup(ctx.CurrentEntryPreparation, x, y, z, forwardX, forwardZ, requiredSteps))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!ParkourFeasibility.HasDominantAxisRunUp(ctx, x, y, z, forwardX, forwardZ, xOffset, zOffset, yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasSidewallArcClearance(ctx, x, y, z, forwardX, forwardZ, lateralX, lateralZ, xOffset, zOffset, yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasSidewallLandingClearance(ctx, destX, destY, destZ, forwardX, forwardZ, lateralX, lateralZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double horizDist = Math.Sqrt((double)((xOffset * xOffset) + (zOffset * zOffset)));
|
||||
double cost = yDelta switch
|
||||
{
|
||||
> 0 => horizDist * ctx.SprintCost + ctx.JumpPenalty * 2,
|
||||
< 0 => horizDist * ctx.SprintCost + ctx.JumpPenalty + ActionCosts.FallCost(-yDelta),
|
||||
_ => horizDist * ctx.SprintCost + ctx.JumpPenalty,
|
||||
};
|
||||
|
||||
result.Set(destX, destY, destZ, cost, ParkourProfile.Sidewall);
|
||||
}
|
||||
}
|
||||
154
docs/superpowers/plans/2026-04-19-unified-jump-move-plan.md
Normal file
154
docs/superpowers/plans/2026-04-19-unified-jump-move-plan.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Unified Jump Move Refactor
|
||||
|
||||
Date: 2026-04-19
|
||||
Branch: `pathing/jump-entry-direct-yaw`
|
||||
|
||||
## Problem
|
||||
|
||||
MCC's A* uses a hard-coded enumeration of ~220 `IMove` instances covering the
|
||||
"jump family" (Traverse, Diagonal, Ascend, DiagonalAscend, DiagonalDescend,
|
||||
Parkour, SidewallParkour). Each geometric variant is a separate IMove subclass
|
||||
with its own `Calculate` method that re-implements the same physics checks
|
||||
(head clearance, run-up, flight path, landing clearance, gap check). Symptoms:
|
||||
|
||||
1. **Drift**: the same physics rule is implemented in 3-4 places. A fix to
|
||||
`HasDominantAxisRunUp` does not automatically propagate to `HasRunUp`.
|
||||
2. **Missing combinations silently become "impossible"**: until this week, the
|
||||
planner had no `MoveParkour(dx=1, dz=2, yDelta=+1)` entry, so the diagonal
|
||||
ascending jump (upper arrow in the user's pyramid image) was rejected
|
||||
entirely even though the physics allow it.
|
||||
3. **Slow expansion**: every A* node runs 220 feasibility checks and a lot of
|
||||
them are obviously irrelevant for that position (e.g. sidewall checks when
|
||||
there is no wall anywhere near the player).
|
||||
|
||||
Baritone does not have this problem: `MovementParkour` is a single class that
|
||||
dynamically probes reachable landings per direction, and its 8 `Moves` enums
|
||||
cover the entire movement space.
|
||||
|
||||
## Goal
|
||||
|
||||
Bring MCC's jump family to a single parameterized move class with one unified
|
||||
feasibility engine, then evolve to Baritone-style dynamic neighbor expansion.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope (unified under `MoveJump` + `JumpDescriptor`)**:
|
||||
|
||||
- `MoveTraverse` (dy=0 cardinal)
|
||||
- `MoveDiagonal` (dy=0 corner)
|
||||
- `MoveAscend` (dy=+1 cardinal)
|
||||
- `MoveDiagonalAscend` (dy=+1 corner)
|
||||
- `MoveDiagonalDescend` (dy=-1 corner)
|
||||
- `MoveParkour` (dy ∈ {+1, 0, -1, -2}, horiz up to 5 cardinal / sqrt(10) diag)
|
||||
- `MoveSidewallParkour` (parkour + inner wall requirement)
|
||||
|
||||
**Out of scope (stay as their own classes)**:
|
||||
|
||||
- `MoveDescend` — dynamic variable-depth fall with water/ladder grab logic
|
||||
- `MoveSprintDescend` — dynamic landing depth
|
||||
- `MoveClimb` — ladder/vine vertical movement
|
||||
- `MoveFall` — pure free fall
|
||||
|
||||
These are "descent family" and have a different feasibility model (unknown
|
||||
landing y, hazard scanning). Future refactor can unify them under a
|
||||
`MoveFallToLanding` family but that is a separate effort.
|
||||
|
||||
## Design
|
||||
|
||||
### Data
|
||||
|
||||
```csharp
|
||||
public readonly record struct JumpDescriptor(
|
||||
int XOffset,
|
||||
int ZOffset,
|
||||
int YDelta,
|
||||
JumpFlavor Flavor);
|
||||
|
||||
public enum JumpFlavor
|
||||
{
|
||||
Walk, // dy=0, 1 block move, no jump (Traverse/Diagonal)
|
||||
Step, // dy=±1, 1 block move with jump or step-off (Ascend/DiagDescend/DiagAscend)
|
||||
SprintJump, // horiz >= 2 with or without dy (Parkour)
|
||||
Sidewall, // SprintJump + inner-wall clearance (SidewallParkour)
|
||||
}
|
||||
```
|
||||
|
||||
The descriptor fully describes any jump-family move. `MoveType` (Traverse,
|
||||
Diagonal, Ascend, Descend, Parkour) is derived from `(Flavor, dy, horiz)` so
|
||||
downstream consumers (templates, cost tables) keep working.
|
||||
|
||||
### Evaluator
|
||||
|
||||
`JumpFeasibility.Evaluate(ctx, x, y, z, desc, ref result)` is the single source
|
||||
of truth. It dispatches on `desc.Flavor` but shares the following primitives:
|
||||
|
||||
1. **Guards**: `AllowParkour`, `AllowParkourAscend`, `MaxFallHeight`, `CanSprint`.
|
||||
2. **Profile check**: geometry falls in the valid range for this flavor.
|
||||
3. **Head clearance at start**: `y+2` always, plus `y+3` if ascending sprint jump.
|
||||
4. **Standing material**: reject climbable (ladder/vine) takeoffs.
|
||||
5. **Destination**: floor solid, body passable, head passable, no hazards.
|
||||
6. **Run-up**: cold-start reach tables plus prepared-entry lookup
|
||||
(`EntryPreparationState`). This replaces both `HasRunUp` and
|
||||
`HasDominantAxisRunUp`.
|
||||
7. **Flight path**: cardinal straight-line column sweep or diagonal
|
||||
proportional-step sweep. Needs `y+3` clearance only when ascending.
|
||||
8. **Wall** (Sidewall only): inner wall presence + outer clearance + arc span.
|
||||
9. **Gap check**: reject when a direct walk would work.
|
||||
10. **Cost**: unified sprint/walk cost × horizontal distance + penalties.
|
||||
|
||||
### Step 1 — introduce evaluator, existing classes delegate
|
||||
|
||||
No behavior change. Each of the 7 existing classes has `Calculate` shrunk to
|
||||
a single `JumpFeasibility.Evaluate(...)` call with a descriptor derived from
|
||||
its constructor args. All existing tests pass with the same pass/fail counts.
|
||||
|
||||
### Step 2 — single `MoveJump` class
|
||||
|
||||
Delete the 7 subclasses. `AStarPathFinder.BuildDefaultMoves` emits
|
||||
`MoveJump(descriptor)` instances from a declarative list. Tests that instantiate
|
||||
the old classes are updated to instantiate `MoveJump` with the equivalent
|
||||
descriptor (or use factory helpers like `MoveJump.Parkour(dx, dz, dy)`).
|
||||
|
||||
Templates (`SprintJumpTemplate`, `AscendTemplate`, `SidewallParkourController`,
|
||||
etc.) dispatch on `Flavor` / `MoveType` already, so they need no changes.
|
||||
|
||||
### Step 3 — dynamic neighbor expansion
|
||||
|
||||
`AStarPathFinder.Calculate` currently loops over `_allMoves` for every popped
|
||||
node. Replace with an `IMoveExpander[]` where each expander yields neighbors
|
||||
on demand:
|
||||
|
||||
```csharp
|
||||
public interface IMoveExpander
|
||||
{
|
||||
void Expand(CalculationContext ctx, int x, int y, int z, Action<MoveResult, MoveType, JumpDescriptor?> emit);
|
||||
}
|
||||
```
|
||||
|
||||
`JumpExpander.Expand` iterates the 4 cardinals + 4 diagonals. For each
|
||||
direction, it asks "what is the furthest reachable landing?" by scanning from
|
||||
max distance down to 1, emitting the first feasible result (the A* cost model
|
||||
already disprefers short jumps when long jumps work). This yields ~8-16
|
||||
neighbors per node instead of 220.
|
||||
|
||||
Baritone-style partial-path coefficients (`bestSoFar[6]`) are a separate
|
||||
improvement; not bundled here.
|
||||
|
||||
## Risk / rollback
|
||||
|
||||
- Step 1 is behavior-preserving and easy to revert (delete evaluator, restore
|
||||
the old `Calculate` bodies from git).
|
||||
- Step 2 deletes code; revert means restoring from git.
|
||||
- Step 3 changes the A* main loop. Keep the old `BuildDefaultMoves` path behind
|
||||
an `_useDynamicExpansion` flag so we can A/B test in live `tools/test-parkour.py`
|
||||
runs before deleting the old path.
|
||||
|
||||
## Test strategy
|
||||
|
||||
- `dotnet test MinecraftClient.Tests` after each step. Baseline is 21 failing
|
||||
tests (all pre-existing on this branch). Target: exact same failure set at
|
||||
each checkpoint.
|
||||
- At Step 2 end, verify upper-arrow scenario (this task's motivating bug) still
|
||||
plans correctly.
|
||||
- At Step 3 end, run `tools/test-parkour.py` linear + sidewall + ceiling
|
||||
scenarios on a real server.
|
||||
Loading…
Add table
Add a link
Reference in a new issue