mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: add parkour moves and template-based path execution system
Phase 2.2: MoveParkour for sprint-jump across 1-2 block gaps (distance 2-3) and ascending parkour (distance 2, +1Y). Registered in BuildDefaultMoves with CalculationContext.AllowParkour gating. Phase 3.1-3.2: Template execution engine replacing the waypoint queue system. - IActionTemplate interface with per-tick state machine pattern - Templates: Walk, Ascend, Descend, Climb, Fall, SprintJump - ActionTemplateFactory maps MoveType to the correct template - PathExecutor drives sequential template execution with logging - PathSegmentManager handles replanning on failure (up to 5 retries) - McClient integration: MoveToAStar now creates PathSegmentManager, UpdatePathfindingInput delegates to it, CancelMovement/ClientIsMoving updated for both old and new systems. Tested on 1.21.11: straight walk, zigzag maze, stair ascent, 1-gap and 2-gap sprint jumps all pass. Made-with: Cursor
This commit is contained in:
parent
2d4f1fa677
commit
4b49135107
15 changed files with 833 additions and 26 deletions
|
|
@ -80,6 +80,7 @@ namespace MinecraftClient
|
|||
private readonly MovementInput physicsInput = new();
|
||||
private bool physicsInitialized = false;
|
||||
private Location? pathTarget; // Current waypoint for physics-driven pathfinding
|
||||
private Pathing.Execution.PathSegmentManager? pathSegmentManager;
|
||||
public enum MovementType { Sneak, Walk, Sprint }
|
||||
private int sequenceId; // User for player block synchronization (Aka. digging, placing blocks, etc..)
|
||||
private bool CanSendMessage = false;
|
||||
|
|
@ -1723,7 +1724,8 @@ namespace MinecraftClient
|
|||
{
|
||||
lock (locationLock)
|
||||
{
|
||||
var ctx = new Pathing.Core.CalculationContext(world);
|
||||
var ctx = new Pathing.Core.CalculationContext(world,
|
||||
allowParkour: true, allowParkourAscend: true);
|
||||
var finder = new Pathing.Core.AStarPathFinder();
|
||||
finder.DebugLog = msg => Log.Debug(msg);
|
||||
|
||||
|
|
@ -1731,7 +1733,6 @@ namespace MinecraftClient
|
|||
int sy = (int)Math.Floor(location.Y);
|
||||
int sz = (int)Math.Floor(location.Z);
|
||||
|
||||
// If floored Y lands inside a solid block (e.g. player on top of it), step up
|
||||
if (!ctx.CanWalkThrough(sx, sy, sz) && ctx.CanWalkThrough(sx, sy + 1, sz))
|
||||
sy++;
|
||||
|
||||
|
|
@ -1746,8 +1747,8 @@ namespace MinecraftClient
|
|||
$"[raw pos=({location.X:F2},{location.Y:F2},{location.Z:F2})]");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var result = finder.Calculate(ctx, sx, sy, sz,
|
||||
new Pathing.Goals.GoalBlock(gx, gy, gz), cts.Token, timeoutMs);
|
||||
var pathGoal = new Pathing.Goals.GoalBlock(gx, gy, gz);
|
||||
var result = finder.Calculate(ctx, sx, sy, sz, pathGoal, cts.Token, timeoutMs);
|
||||
|
||||
Log.Info($"[Goto] A* result: {result.Status}, nodes={result.NodesExplored}, " +
|
||||
$"time={result.ElapsedMs}ms, path length={result.Path.Count}");
|
||||
|
|
@ -1758,27 +1759,23 @@ namespace MinecraftClient
|
|||
result.NodesExplored, result.ElapsedMs));
|
||||
}
|
||||
|
||||
var queue = new Queue<Location>();
|
||||
for (int i = 1; i < result.Path.Count; i++)
|
||||
{
|
||||
var node = result.Path[i];
|
||||
queue.Enqueue(new Location(node.X + 0.5, node.Y, node.Z + 0.5));
|
||||
}
|
||||
|
||||
Log.Info($"[Goto] Path waypoints: {queue.Count}");
|
||||
int logCount = 0;
|
||||
foreach (var wp in queue)
|
||||
{
|
||||
Log.Debug($"[Goto] wp[{logCount}] = ({wp.X:F1},{wp.Y:F1},{wp.Z:F1})");
|
||||
logCount++;
|
||||
Log.Debug($"[Goto] seg[{i - 1}] = {node.MoveUsed}: ({node.X},{node.Y},{node.Z})");
|
||||
}
|
||||
|
||||
pathTarget = null;
|
||||
path = queue;
|
||||
path = null;
|
||||
|
||||
pathSegmentManager = new Pathing.Execution.PathSegmentManager(
|
||||
debugLog: msg => Log.Debug(msg),
|
||||
infoLog: msg => Log.Info(msg));
|
||||
pathSegmentManager.StartNavigation(pathGoal, result);
|
||||
|
||||
string statusStr = result.Status == Pathing.Core.PathStatus.Partial ? " (partial)" : "";
|
||||
return (true, string.Format(Translations.cmd_goto_success,
|
||||
queue.Count, result.NodesExplored, result.ElapsedMs, statusStr));
|
||||
result.Path.Count - 1, result.NodesExplored, result.ElapsedMs, statusStr));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3261,26 +3258,30 @@ namespace MinecraftClient
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drive the physics engine input based on the current A* path.
|
||||
/// Converts discrete waypoint pathfinding into continuous movement input.
|
||||
/// Drive the physics engine input based on the current path.
|
||||
/// Uses template-based PathSegmentManager when available, falls back to legacy waypoints.
|
||||
/// </summary>
|
||||
private void UpdatePathfindingInput()
|
||||
{
|
||||
physicsInput.Reset();
|
||||
|
||||
// Advance waypoints when reached
|
||||
// Template-based execution (new system)
|
||||
if (pathSegmentManager is not null && pathSegmentManager.IsNavigating)
|
||||
{
|
||||
pathSegmentManager.Tick(location, playerPhysics, physicsInput, world);
|
||||
playerYaw = playerPhysics.Yaw;
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy waypoint-based execution
|
||||
if (pathTarget is not null && ReachedWaypoint(pathTarget.Value))
|
||||
AdvanceWaypoint();
|
||||
|
||||
// First target from a fresh path
|
||||
if (pathTarget is null && path is not null && path.Count > 0)
|
||||
AdvanceWaypoint();
|
||||
|
||||
if (pathTarget is not null)
|
||||
{
|
||||
// Look-ahead: if this is a vertical-only waypoint and the next requires
|
||||
// horizontal movement, merge them once we're close enough vertically.
|
||||
// This handles the ladder-to-platform transition.
|
||||
if (path is not null && path.Count > 0)
|
||||
{
|
||||
var target = pathTarget.Value;
|
||||
|
|
@ -3297,8 +3298,6 @@ namespace MinecraftClient
|
|||
double ndz = next.Z - target.Z;
|
||||
bool nextIsHorizontal = ndx * ndx + ndz * ndz > 0.3;
|
||||
|
||||
// Skip to next waypoint early if we're within 1 block of the target Y
|
||||
// and the next move requires horizontal movement
|
||||
if (nextIsHorizontal && Math.Abs(dy) < 1.0)
|
||||
{
|
||||
AdvanceWaypoint();
|
||||
|
|
@ -3421,7 +3420,14 @@ namespace MinecraftClient
|
|||
/// <returns>true if a movement is currently handled</returns>
|
||||
public bool ClientIsMoving()
|
||||
{
|
||||
return terrainAndMovementsEnabled && locationReceived && path is not null && path.Count > 0;
|
||||
if (terrainAndMovementsEnabled && locationReceived)
|
||||
{
|
||||
if (pathSegmentManager is not null && pathSegmentManager.IsNavigating)
|
||||
return true;
|
||||
if (path is not null && path.Count > 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -3441,6 +3447,9 @@ namespace MinecraftClient
|
|||
{
|
||||
bool success = ClientIsMoving();
|
||||
path = null;
|
||||
pathTarget = null;
|
||||
pathSegmentManager?.Cancel();
|
||||
pathSegmentManager = null;
|
||||
return success;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,19 @@ namespace MinecraftClient.Pathing.Core
|
|||
|
||||
moves.Add(new MoveFall());
|
||||
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
for (int dist = 2; dist <= 3; dist++)
|
||||
moves.Add(new MoveParkour(dx, 0, dist));
|
||||
moves.Add(new MoveParkour(dx, 0, 2, yDelta: 1));
|
||||
}
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
for (int dist = 2; dist <= 3; dist++)
|
||||
moves.Add(new MoveParkour(0, dz, dist));
|
||||
moves.Add(new MoveParkour(0, dz, 2, yDelta: 1));
|
||||
}
|
||||
|
||||
return [.. moves];
|
||||
}
|
||||
|
||||
|
|
|
|||
27
MinecraftClient/Pathing/Execution/ActionTemplateFactory.cs
Normal file
27
MinecraftClient/Pathing/Execution/ActionTemplateFactory.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Execution.Templates;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a PathSegment (MoveType + start/end) to the appropriate IActionTemplate.
|
||||
/// </summary>
|
||||
public static class ActionTemplateFactory
|
||||
{
|
||||
public static IActionTemplate Create(PathSegment segment)
|
||||
{
|
||||
return segment.MoveType switch
|
||||
{
|
||||
MoveType.Traverse => new WalkTemplate(segment.Start, segment.End),
|
||||
MoveType.Diagonal => new WalkTemplate(segment.Start, segment.End),
|
||||
MoveType.Ascend => new AscendTemplate(segment.Start, segment.End),
|
||||
MoveType.Descend => new DescendTemplate(segment.Start, segment.End),
|
||||
MoveType.Fall => new FallTemplate(segment.Start, segment.End),
|
||||
MoveType.Climb => new ClimbTemplate(segment.Start, segment.End),
|
||||
MoveType.Parkour => new SprintJumpTemplate(segment.Start, segment.End),
|
||||
_ => throw new ArgumentException($"Unknown MoveType: {segment.MoveType}")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
25
MinecraftClient/Pathing/Execution/IActionTemplate.cs
Normal file
25
MinecraftClient/Pathing/Execution/IActionTemplate.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public enum TemplateState
|
||||
{
|
||||
InProgress,
|
||||
Complete,
|
||||
Failed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-tick movement controller for one path segment.
|
||||
/// Reads player state from physics, writes desired input to MovementInput,
|
||||
/// and reports completion or failure.
|
||||
/// </summary>
|
||||
public interface IActionTemplate
|
||||
{
|
||||
Location ExpectedStart { get; }
|
||||
Location ExpectedEnd { get; }
|
||||
|
||||
TemplateState Tick(Location currentPos, PlayerPhysics physics, MovementInput input);
|
||||
}
|
||||
}
|
||||
87
MinecraftClient/Pathing/Execution/PathExecutor.cs
Normal file
87
MinecraftClient/Pathing/Execution/PathExecutor.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public enum PathExecutorState
|
||||
{
|
||||
InProgress,
|
||||
Failed,
|
||||
Complete
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drives a sequence of PathSegments by instantiating the correct IActionTemplate
|
||||
/// for each segment and ticking it every game tick.
|
||||
/// </summary>
|
||||
public sealed class PathExecutor
|
||||
{
|
||||
private readonly List<PathSegment> _segments;
|
||||
private int _currentIndex;
|
||||
private IActionTemplate? _currentTemplate;
|
||||
private readonly Action<string>? _debugLog;
|
||||
|
||||
public bool IsComplete => _currentIndex >= _segments.Count && _currentTemplate is null;
|
||||
public int CurrentIndex => _currentIndex;
|
||||
public int TotalSegments => _segments.Count;
|
||||
public PathSegment? CurrentSegment =>
|
||||
_currentIndex < _segments.Count ? _segments[_currentIndex] : null;
|
||||
|
||||
public PathExecutor(List<PathSegment> segments, Action<string>? debugLog = null)
|
||||
{
|
||||
_segments = segments;
|
||||
_currentIndex = 0;
|
||||
_debugLog = debugLog;
|
||||
AdvanceToNextSegment();
|
||||
}
|
||||
|
||||
public PathExecutorState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
{
|
||||
if (_currentTemplate is null)
|
||||
return PathExecutorState.Complete;
|
||||
|
||||
var state = _currentTemplate.Tick(pos, physics, input);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case TemplateState.Complete:
|
||||
_debugLog?.Invoke($"[PathExec] Segment {_currentIndex} complete " +
|
||||
$"({_segments[_currentIndex].MoveType}) at ({pos.X:F2},{pos.Y:F2},{pos.Z:F2})");
|
||||
_currentIndex++;
|
||||
if (_currentIndex >= _segments.Count)
|
||||
{
|
||||
_currentTemplate = null;
|
||||
_debugLog?.Invoke("[PathExec] All segments complete!");
|
||||
return PathExecutorState.Complete;
|
||||
}
|
||||
AdvanceToNextSegment();
|
||||
return PathExecutorState.InProgress;
|
||||
|
||||
case TemplateState.Failed:
|
||||
_debugLog?.Invoke($"[PathExec] Segment {_currentIndex} FAILED " +
|
||||
$"({_segments[_currentIndex].MoveType}) at ({pos.X:F2},{pos.Y:F2},{pos.Z:F2}), " +
|
||||
$"target was ({_currentTemplate.ExpectedEnd.X:F2},{_currentTemplate.ExpectedEnd.Y:F2},{_currentTemplate.ExpectedEnd.Z:F2})");
|
||||
return PathExecutorState.Failed;
|
||||
|
||||
default:
|
||||
return PathExecutorState.InProgress;
|
||||
}
|
||||
}
|
||||
|
||||
private void AdvanceToNextSegment()
|
||||
{
|
||||
if (_currentIndex < _segments.Count)
|
||||
{
|
||||
var seg = _segments[_currentIndex];
|
||||
_currentTemplate = ActionTemplateFactory.Create(seg);
|
||||
_debugLog?.Invoke($"[PathExec] Starting segment {_currentIndex}/{_segments.Count}: {seg}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentTemplate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
MinecraftClient/Pathing/Execution/PathSegment.cs
Normal file
33
MinecraftClient/Pathing/Execution/PathSegment.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System.Collections.Generic;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public sealed class PathSegment
|
||||
{
|
||||
public required Location Start { get; init; }
|
||||
public required Location End { get; init; }
|
||||
public required MoveType MoveType { get; init; }
|
||||
|
||||
public static List<PathSegment> FromPath(IReadOnlyList<PathNode> nodes)
|
||||
{
|
||||
var segments = new List<PathSegment>(nodes.Count - 1);
|
||||
for (int i = 1; i < nodes.Count; i++)
|
||||
{
|
||||
var prev = nodes[i - 1];
|
||||
var curr = nodes[i];
|
||||
segments.Add(new PathSegment
|
||||
{
|
||||
Start = new Location(prev.X + 0.5, prev.Y, prev.Z + 0.5),
|
||||
End = new Location(curr.X + 0.5, curr.Y, curr.Z + 0.5),
|
||||
MoveType = curr.MoveUsed
|
||||
});
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"{MoveType}: ({Start.X:F1},{Start.Y:F1},{Start.Z:F1})->({End.X:F1},{End.Y:F1},{End.Z:F1})";
|
||||
}
|
||||
}
|
||||
120
MinecraftClient/Pathing/Execution/PathSegmentManager.cs
Normal file
120
MinecraftClient/Pathing/Execution/PathSegmentManager.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Goals;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Top-level navigation controller. Holds a PathExecutor, monitors its progress,
|
||||
/// and triggers replanning on failure or deviation.
|
||||
/// </summary>
|
||||
public sealed class PathSegmentManager
|
||||
{
|
||||
private PathExecutor? _executor;
|
||||
private IGoal? _goal;
|
||||
private int _replanCount;
|
||||
private const int MaxReplans = 5;
|
||||
|
||||
private readonly Action<string>? _debugLog;
|
||||
private readonly Action<string>? _infoLog;
|
||||
|
||||
public bool IsNavigating => _executor is not null && !_executor.IsComplete;
|
||||
public int ReplanCount => _replanCount;
|
||||
|
||||
public PathSegmentManager(Action<string>? debugLog = null, Action<string>? infoLog = null)
|
||||
{
|
||||
_debugLog = debugLog;
|
||||
_infoLog = infoLog;
|
||||
}
|
||||
|
||||
public void StartNavigation(IGoal goal, PathResult result)
|
||||
{
|
||||
_goal = goal;
|
||||
_replanCount = 0;
|
||||
var segments = PathSegment.FromPath(result.Path);
|
||||
_executor = new PathExecutor(segments, _debugLog);
|
||||
_infoLog?.Invoke($"[PathMgr] Navigation started: {segments.Count} segments");
|
||||
}
|
||||
|
||||
public void Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
if (_executor is null)
|
||||
return;
|
||||
|
||||
var state = _executor.Tick(pos, physics, input);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case PathExecutorState.Complete:
|
||||
_infoLog?.Invoke("[PathMgr] Navigation complete!");
|
||||
_executor = null;
|
||||
_goal = null;
|
||||
break;
|
||||
|
||||
case PathExecutorState.Failed:
|
||||
_infoLog?.Invoke("[PathMgr] Segment failed, replanning...");
|
||||
Replan(pos, world);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
if (_executor is not null)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Navigation cancelled.");
|
||||
_executor = null;
|
||||
_goal = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Replan(Location pos, World world)
|
||||
{
|
||||
_replanCount++;
|
||||
if (_replanCount > MaxReplans)
|
||||
{
|
||||
_infoLog?.Invoke($"[PathMgr] Giving up after {MaxReplans} replans.");
|
||||
_executor = null;
|
||||
_goal = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_goal is null)
|
||||
{
|
||||
_executor = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_debugLog?.Invoke($"[PathMgr] Replan #{_replanCount} from ({pos.X:F2},{pos.Y:F2},{pos.Z:F2})");
|
||||
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var finder = new AStarPathFinder();
|
||||
finder.DebugLog = _debugLog;
|
||||
|
||||
int sx = (int)Math.Floor(pos.X);
|
||||
int sy = (int)Math.Floor(pos.Y);
|
||||
int sz = (int)Math.Floor(pos.Z);
|
||||
|
||||
if (!ctx.CanWalkThrough(sx, sy, sz) && ctx.CanWalkThrough(sx, sy + 1, sz))
|
||||
sy++;
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var result = finder.Calculate(ctx, sx, sy, sz, _goal, cts.Token, 3000);
|
||||
|
||||
if (result.Status == PathStatus.Failed || result.Path.Count < 2)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Replan failed -- no path found.");
|
||||
_executor = null;
|
||||
_goal = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var segments = PathSegment.FromPath(result.Path);
|
||||
_executor = new PathExecutor(segments, _debugLog);
|
||||
_infoLog?.Invoke($"[PathMgr] Replanned: {segments.Count} segments (replan #{_replanCount})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
/// <summary>
|
||||
/// Jump up 1 block while moving 1 block in a cardinal direction.
|
||||
/// Faces destination, sprints forward, and jumps when on ground.
|
||||
/// </summary>
|
||||
public sealed class AscendTemplate : IActionTemplate
|
||||
{
|
||||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private int _tickCount;
|
||||
private Location _lastPos;
|
||||
private int _stuckTicks;
|
||||
|
||||
public AscendTemplate(Location start, Location end)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
_lastPos = start;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double dy = ExpectedEnd.Y - pos.Y;
|
||||
double horizDistSq = dx * dx + dz * dz;
|
||||
|
||||
// Complete when close to destination. Sprint bouncing can leave the player
|
||||
// slightly above ground, so we don't require OnGround here.
|
||||
if (horizDistSq < 0.25 && Math.Abs(dy) < 0.8)
|
||||
return TemplateState.Complete;
|
||||
|
||||
double movedSq = TemplateHelper.HorizontalDistanceSq(pos, _lastPos);
|
||||
double movedY = Math.Abs(pos.Y - _lastPos.Y);
|
||||
_stuckTicks = (movedSq < 0.0005 && movedY < 0.001) ? _stuckTicks + 1 : 0;
|
||||
_lastPos = pos;
|
||||
|
||||
if (_stuckTicks > 40 || _tickCount > 80)
|
||||
return TemplateState.Failed;
|
||||
|
||||
physics.Yaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
input.Forward = true;
|
||||
input.Sprint = true;
|
||||
|
||||
if (physics.OnGround && dy > 0.1)
|
||||
input.Jump = true;
|
||||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
MinecraftClient/Pathing/Execution/Templates/ClimbTemplate.cs
Normal file
63
MinecraftClient/Pathing/Execution/Templates/ClimbTemplate.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
/// <summary>
|
||||
/// Climb up or down a ladder/vine by 1 block.
|
||||
/// Pushes against the wall (Forward + face center) and jumps for upward movement.
|
||||
/// </summary>
|
||||
public sealed class ClimbTemplate : IActionTemplate
|
||||
{
|
||||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private int _tickCount;
|
||||
|
||||
public ClimbTemplate(Location start, Location end)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
double dy = ExpectedEnd.Y - pos.Y;
|
||||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double horizDistSq = dx * dx + dz * dz;
|
||||
|
||||
if (Math.Abs(dy) < 0.3 && horizDistSq < 0.5)
|
||||
return TemplateState.Complete;
|
||||
|
||||
if (_tickCount > 100)
|
||||
return TemplateState.Failed;
|
||||
|
||||
if (physics.OnClimbable)
|
||||
{
|
||||
if (dy > 0)
|
||||
{
|
||||
input.Jump = true;
|
||||
input.Forward = true;
|
||||
if (horizDistSq > 0.01)
|
||||
physics.Yaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
}
|
||||
// Going down: don't press anything, gravity + climbable friction handles it
|
||||
}
|
||||
else
|
||||
{
|
||||
// Left the climbable area -- walk toward destination
|
||||
if (horizDistSq > 0.01)
|
||||
{
|
||||
physics.Yaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
input.Forward = true;
|
||||
}
|
||||
}
|
||||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
/// <summary>
|
||||
/// Walk off a ledge and drop 1-N blocks to a landing spot.
|
||||
/// Walks toward the destination; gravity handles the fall.
|
||||
/// </summary>
|
||||
public sealed class DescendTemplate : IActionTemplate
|
||||
{
|
||||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private int _tickCount;
|
||||
private bool _hasFallen;
|
||||
|
||||
public DescendTemplate(Location start, Location end)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double dy = ExpectedEnd.Y - pos.Y;
|
||||
double horizDistSq = dx * dx + dz * dz;
|
||||
|
||||
if (!physics.OnGround)
|
||||
_hasFallen = true;
|
||||
|
||||
if (_hasFallen && physics.OnGround && horizDistSq < 0.5 && Math.Abs(dy) < 0.8)
|
||||
return TemplateState.Complete;
|
||||
|
||||
if (horizDistSq < 0.25 && Math.Abs(dy) < 0.5 && physics.OnGround)
|
||||
return TemplateState.Complete;
|
||||
|
||||
if (_tickCount > 120)
|
||||
return TemplateState.Failed;
|
||||
|
||||
if (horizDistSq > 0.01)
|
||||
{
|
||||
physics.Yaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
input.Forward = true;
|
||||
}
|
||||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
MinecraftClient/Pathing/Execution/Templates/FallTemplate.cs
Normal file
42
MinecraftClient/Pathing/Execution/Templates/FallTemplate.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
/// <summary>
|
||||
/// Vertical free fall at the same X,Z. Waits for the player to land at the target Y.
|
||||
/// </summary>
|
||||
public sealed class FallTemplate : IActionTemplate
|
||||
{
|
||||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private int _tickCount;
|
||||
private bool _hasFallen;
|
||||
|
||||
public FallTemplate(Location start, Location end)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
double dy = pos.Y - ExpectedEnd.Y;
|
||||
|
||||
if (!physics.OnGround)
|
||||
_hasFallen = true;
|
||||
|
||||
if (_hasFallen && physics.OnGround && Math.Abs(dy) < 1.0)
|
||||
return TemplateState.Complete;
|
||||
|
||||
if (_tickCount > 200)
|
||||
return TemplateState.Failed;
|
||||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
/// <summary>
|
||||
/// Sprint-jump across a gap. Uses a phase-based state machine:
|
||||
/// Approach -> jump on first available ground tick -> Airborne -> Landing check.
|
||||
/// </summary>
|
||||
public sealed class SprintJumpTemplate : IActionTemplate
|
||||
{
|
||||
private enum Phase { Approach, Airborne, Landing }
|
||||
|
||||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private int _tickCount;
|
||||
private Phase _phase = Phase.Approach;
|
||||
private readonly int _distance;
|
||||
|
||||
public SprintJumpTemplate(Location start, Location end)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
|
||||
double dx = Math.Abs(end.X - start.X);
|
||||
double dz = Math.Abs(end.Z - start.Z);
|
||||
_distance = (int)Math.Round(Math.Max(dx, dz));
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double dy = ExpectedEnd.Y - pos.Y;
|
||||
double horizDistSq = dx * dx + dz * dz;
|
||||
|
||||
physics.Yaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
input.Forward = true;
|
||||
input.Sprint = true;
|
||||
|
||||
switch (_phase)
|
||||
{
|
||||
case Phase.Approach:
|
||||
if (physics.OnGround)
|
||||
{
|
||||
input.Jump = true;
|
||||
_phase = Phase.Airborne;
|
||||
}
|
||||
if (_tickCount > 20)
|
||||
return TemplateState.Failed;
|
||||
break;
|
||||
|
||||
case Phase.Airborne:
|
||||
if (!physics.OnGround)
|
||||
break;
|
||||
// Landed
|
||||
_phase = Phase.Landing;
|
||||
goto case Phase.Landing;
|
||||
|
||||
case Phase.Landing:
|
||||
if (horizDistSq < 2.0 && Math.Abs(dy) < 1.0)
|
||||
return TemplateState.Complete;
|
||||
return TemplateState.Failed;
|
||||
}
|
||||
|
||||
if (pos.Y < ExpectedEnd.Y - 4.0)
|
||||
return TemplateState.Failed;
|
||||
|
||||
if (_tickCount > 60)
|
||||
return TemplateState.Failed;
|
||||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
internal static class TemplateHelper
|
||||
{
|
||||
internal static float CalculateYaw(double dx, double dz)
|
||||
{
|
||||
float yaw = (float)(-Math.Atan2(dx, dz) / Math.PI * 180.0);
|
||||
if (yaw < 0) yaw += 360;
|
||||
return yaw;
|
||||
}
|
||||
|
||||
internal static double HorizontalDistanceSq(Location a, Location b)
|
||||
{
|
||||
double dx = a.X - b.X;
|
||||
double dz = a.Z - b.Z;
|
||||
return dx * dx + dz * dz;
|
||||
}
|
||||
|
||||
internal static bool IsNear(Location pos, Location target,
|
||||
double horizThresholdSq = 0.25, double vertThreshold = 0.8)
|
||||
{
|
||||
double dx = target.X - pos.X;
|
||||
double dz = target.Z - pos.Z;
|
||||
double dy = target.Y - pos.Y;
|
||||
return dx * dx + dz * dz < horizThresholdSq && Math.Abs(dy) < vertThreshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs
Normal file
51
MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
/// <summary>
|
||||
/// Walk/sprint toward a destination on the same Y level.
|
||||
/// Used for Traverse and Diagonal moves.
|
||||
/// </summary>
|
||||
public sealed class WalkTemplate : IActionTemplate
|
||||
{
|
||||
public Location ExpectedStart { get; }
|
||||
public Location ExpectedEnd { get; }
|
||||
|
||||
private int _tickCount;
|
||||
private Location _lastPos;
|
||||
private int _stuckTicks;
|
||||
|
||||
public WalkTemplate(Location start, Location end)
|
||||
{
|
||||
ExpectedStart = start;
|
||||
ExpectedEnd = end;
|
||||
_lastPos = start;
|
||||
}
|
||||
|
||||
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input)
|
||||
{
|
||||
_tickCount++;
|
||||
|
||||
if (TemplateHelper.IsNear(pos, ExpectedEnd, horizThresholdSq: 0.20))
|
||||
return TemplateState.Complete;
|
||||
|
||||
double movedSq = TemplateHelper.HorizontalDistanceSq(pos, _lastPos);
|
||||
_stuckTicks = movedSq < 0.0005 ? _stuckTicks + 1 : 0;
|
||||
_lastPos = pos;
|
||||
|
||||
if (_stuckTicks > 40 || _tickCount > 100)
|
||||
return TemplateState.Failed;
|
||||
|
||||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
physics.Yaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
|
||||
input.Forward = true;
|
||||
input.Sprint = true;
|
||||
|
||||
return TemplateState.InProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
MinecraftClient/Pathing/Moves/Impl/MoveParkour.cs
Normal file
113
MinecraftClient/Pathing/Moves/Impl/MoveParkour.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// Sprint jump across a gap of 1-3 blocks (total distance 2-4 blocks forward).
|
||||
/// Optionally ascends 1 block during the jump (distance 2 only).
|
||||
/// Requires AllowParkour in context; the first block forward must lack ground.
|
||||
/// </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 _distance;
|
||||
private readonly int _yDelta;
|
||||
private readonly int _xDir;
|
||||
private readonly int _zDir;
|
||||
|
||||
public MoveParkour(int xDir, int zDir, int distance, int yDelta = 0)
|
||||
{
|
||||
_xDir = xDir;
|
||||
_zDir = zDir;
|
||||
_distance = distance;
|
||||
_yDelta = yDelta;
|
||||
XOffset = xDir * distance;
|
||||
ZOffset = zDir * distance;
|
||||
}
|
||||
|
||||
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 (!ctx.CanSprint)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
int destX = x + _xDir * _distance;
|
||||
int destZ = z + _zDir * _distance;
|
||||
int destY = y + _yDelta;
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
for (int i = 1; i < _distance; i++)
|
||||
{
|
||||
int gx = x + _xDir * i;
|
||||
int gz = z + _zDir * i;
|
||||
|
||||
if (!ctx.CanWalkThrough(gx, y, gz) ||
|
||||
!ctx.CanWalkThrough(gx, y + 1, gz) ||
|
||||
!ctx.CanWalkThrough(gx, y + 2, gz))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_yDelta > 0 && !ctx.CanWalkThrough(gx, y + 3, gz))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int firstGapX = x + _xDir;
|
||||
int firstGapZ = z + _zDir;
|
||||
if (ctx.CanWalkOn(firstGapX, y - 1, firstGapZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double cost = _distance * ctx.SprintCost + ctx.JumpPenalty;
|
||||
if (_yDelta > 0)
|
||||
cost += ctx.JumpPenalty;
|
||||
|
||||
result.Set(destX, destY, destZ, cost);
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"MoveParkour(dir=({_xDir},{_zDir}), dist={_distance}, dy={_yDelta})";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue