pathing: add side-wall theory, full-coverage parkour test suite, and live test fixes

Theory simulator:
- Add 2D side-wall jump physics with yaw sweep for worst-case margin
- Generate sidewall theory cases (flat/ascend/descend, wall_offset 0/1)
- Add momentum-capabilities.json with band compression and max_reach
- Extend models, capabilities, canonical, and renderers for sidewall

Full-coverage parkour test suite (tools/test-parkour.py):
- Derive test matrix from momentum-capabilities.json
- Build linear/neo/ceiling courses via RCON with 7-block clear margin
- Use /goto for pathfinding, parse A* and PathMgr log output
- Stop-at-first-failure per (family, subfamily, dy, ceil, wo) group
- Hierarchical --filter (e.g. linear/flat, ceiling/headhitter/ceil2.5)
- Exclude sidewall from default matrix (identical max_reach to linear)

Pathing execution fixes:
- Align parkour contracts and timing budgets with live test results
- Fix jump-entry yaw snapping for grounded handoffs
- Template helper and sprint jump template refinements

Made-with: Cursor
This commit is contained in:
BruceChen 2026-04-15 17:52:47 +00:00
parent cf8bf349db
commit 418f17b4a1
42 changed files with 56087 additions and 4322 deletions

View file

@ -46,11 +46,8 @@ namespace MinecraftClient.Pathing.Execution.Templates
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
bool snapYawForJumpCommit = !_initiatedJump && !groundedPrepareJumpHandoff;
physics.Yaw = TemplateHelper.AlignYaw(
physics.Yaw,
targetYaw,
snapYawForJumpCommit ? YawAlignmentMode.Snap : YawAlignmentMode.Smooth);
if (!groundedPrepareJumpHandoff)
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
float headingPenalty = YawDifference(physics.Yaw, targetYaw);
bool headingReady = headingPenalty <= 8.0;

View file

@ -20,7 +20,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
input.Forward = false;
input.Sprint = false;
input.Back = false;
TemplateHelper.FaceExitHeading(physics, segment, YawAlignmentMode.Snap);
TemplateHelper.FaceExitHeading(physics, segment);
return;
}

View file

@ -31,7 +31,6 @@ namespace MinecraftClient.Pathing.Execution.Templates
private Phase _phase = Phase.Approach;
private bool _leftGround;
private bool _carriedGroundEntry;
private bool _airBrakeLatched;
private const float YawToleranceDeg = 5f;
@ -57,10 +56,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
YawAlignmentMode yawMode = _phase == Phase.Approach
? YawAlignmentMode.Snap
: YawAlignmentMode.Smooth;
physics.Yaw = TemplateHelper.AlignYaw(physics.Yaw, targetYaw, yawMode);
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
switch (_phase)
@ -139,14 +135,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
&& lookaheadAirBrake
&& !releaseInAir;
if (_segment.ExitTransition == PathTransitionType.FinalStop
&& _horizDist <= 2.5
&& (releaseInAir || pastTarget))
{
_airBrakeLatched = true;
}
if (_airBrakeLatched || releaseInAir || pastTarget)
if (releaseInAir || pastTarget)
{
input.Forward = false;
input.Sprint = false;

View file

@ -4,12 +4,6 @@ using MinecraftClient.Physics;
namespace MinecraftClient.Pathing.Execution.Templates
{
internal enum YawAlignmentMode
{
Smooth,
Snap
}
internal static class TemplateHelper
{
private const double EyeHeight = 1.62;
@ -56,14 +50,6 @@ namespace MinecraftClient.Pathing.Execution.Templates
return result;
}
internal static float AlignYaw(float current, float target, YawAlignmentMode mode, float maxStep = MaxYawStepPerTick)
{
target = NormalizeYaw(target);
return mode == YawAlignmentMode.Snap
? target
: SmoothYaw(current, target, maxStep);
}
/// <summary>
/// Smoothly interpolate pitch toward a target.
/// </summary>
@ -91,18 +77,16 @@ namespace MinecraftClient.Pathing.Execution.Templates
return dx * dx + dz * dz < horizThresholdSq && Math.Abs(dy) < vertThreshold;
}
internal static void FaceSegmentHeading(PlayerPhysics physics, PathSegment segment,
YawAlignmentMode mode = YawAlignmentMode.Smooth)
internal static void FaceSegmentHeading(PlayerPhysics physics, PathSegment segment)
{
float headingYaw = CalculateYaw(segment.HeadingX, segment.HeadingZ);
physics.Yaw = AlignYaw(physics.Yaw, headingYaw, mode);
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
}
internal static void FaceExitHeading(PlayerPhysics physics, PathSegment segment,
YawAlignmentMode mode = YawAlignmentMode.Smooth)
internal static void FaceExitHeading(PlayerPhysics physics, PathSegment segment)
{
float headingYaw = GetExitHeadingYaw(segment);
physics.Yaw = AlignYaw(physics.Yaw, headingYaw, mode);
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
}
internal static void ApplyDecision(MovementInput input, TransitionBrakingDecision decision)
@ -238,13 +222,6 @@ namespace MinecraftClient.Pathing.Execution.Templates
}
}
private static float NormalizeYaw(float yaw)
{
while (yaw < 0f) yaw += 360f;
while (yaw >= 360f) yaw -= 360f;
return yaw;
}
internal static PlayerPhysics ClonePhysicsForPlanning(PlayerPhysics physics)
{
return new PlayerPhysics

View file

@ -35,17 +35,11 @@ namespace MinecraftClient.Pathing.Execution.Templates
double dx = ExpectedEnd.X - pos.X;
double dz = ExpectedEnd.Z - pos.Z;
double dy = ExpectedEnd.Y - pos.Y;
bool snapYawForJumpEntry = physics.OnGround
&& _segment.ExitTransition == PathTransitionType.PrepareJump
&& _segment.ExitHints.RequireJumpReady;
float targetYaw = TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment)
? TemplateHelper.GetExitHeadingYaw(_segment)
: TemplateHelper.CalculateYaw(dx, dz);
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
physics.Yaw = TemplateHelper.AlignYaw(
physics.Yaw,
targetYaw,
snapYawForJumpEntry ? YawAlignmentMode.Snap : YawAlignmentMode.Smooth);
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);

View file

@ -15,10 +15,7 @@ internal static class ParkourFeasibility
int yDelta)
{
double horiz = Math.Sqrt(xOffset * xOffset + zOffset * zOffset);
if (yDelta <= 0)
return true;
double threshold = 2.5;
double threshold = yDelta > 0 ? 2.5 : 3.5;
if (horiz < threshold)
return true;