diff --git a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs
index cabc5d8e..f8f4b69c 100644
--- a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs
+++ b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs
@@ -886,4 +886,121 @@ public sealed class GroundedTemplateConvergenceTests
TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, ascend.End),
$"state={state} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}");
}
+
+ ///
+ /// Live regression: B->A route segment 9/41 Ascend (243.5,97,181.5)->
+ /// (244.5,98,182.5) exit=Turn, fed by a Descend->PrepareJump handoff.
+ /// The Ascend starts mid-air with cardinal +Z momentum and yaw aimed
+ /// at the diagonal target. Without a post-landing completion shortcut
+ /// the bot lands hot (footprint partially outside target) and the
+ /// AscendTemplate top-level yaw smoothing toward targetYaw fights the
+ /// GroundedSegmentController exit-heading rotation, oscillating yaw
+ /// each tick until the bot drifts off the 1-block landing's edge and
+ /// the segment fails. With the shortcut the segment completes the
+ /// instant the bot lands with center inside the target column, which
+ /// hands off cleanly to the next template (Traverse) that snaps yaw
+ /// on its first tick.
+ ///
+ [Fact]
+ public void AscendTemplate_TurnExit_FromCarriedAirMomentum_CompletesOnTargetColumnLanding()
+ {
+ World world = FlatWorldTestBuilder.CreateStoneFloor(min: -4, max: 6);
+ FlatWorldTestBuilder.ClearBox(world, -4, 80, -4, 6, 86, 6);
+ // Source block (0,79,0) is the Descend's landing column; the bot
+ // arrives mid-jump above it.
+ FlatWorldTestBuilder.SetSolid(world, 0, 79, 0);
+ // Target block (1,80,1) (NE diagonal +1y) is the Ascend's landing.
+ FlatWorldTestBuilder.SetSolid(world, 1, 80, 1);
+ // Next-segment landing column (Traverse east from target).
+ FlatWorldTestBuilder.SetSolid(world, 2, 80, 1);
+ // Walkable shoulder block beneath the bot's overshooting +Z
+ // footprint so the bot doesn't drop into a 2-block hole on
+ // landing (matches the live world geometry where a wide platform
+ // existed at the target's elevation).
+ FlatWorldTestBuilder.SetSolid(world, 1, 80, 2);
+
+ var ascend = new PathSegment
+ {
+ Start = new Location(0.5, 80, 0.5),
+ End = new Location(1.5, 81, 1.5),
+ MoveType = MoveType.Ascend,
+ ExitTransition = PathTransitionType.Turn,
+ ExitHints = new PathTransitionHints(
+ DesiredHeadingX: 1,
+ DesiredHeadingZ: 0,
+ MinExitSpeed: 0.0,
+ MaxExitSpeed: 0.05,
+ RequireStableFooting: true,
+ RequireGrounded: true,
+ RequireJumpReady: false,
+ AllowAirBrake: true,
+ HorizonTicks: 12),
+ PreserveSprint = true
+ };
+ var next = new PathSegment
+ {
+ Start = new Location(1.5, 81, 1.5),
+ End = new Location(2.5, 81, 1.5),
+ MoveType = MoveType.Traverse,
+ ExitTransition = PathTransitionType.FinalStop
+ };
+
+ var template = new AscendTemplate(ascend, next);
+
+ // Seed mid-arc state mirroring the live PathDiag tick-trace at the
+ // first observed tick of seg9/41: bot already airborne with rising
+ // vy, cardinal +Z momentum from the preceding Descend, and yaw
+ // pointing roughly NE (the AscendTemplate snapped yaw on the
+ // takeoff tick).
+ var physics = new PlayerPhysics
+ {
+ Position = new Vec3d(0.7, 80.42, 0.92),
+ DeltaMovement = new Vec3d(0.0, 0.333, 0.145),
+ OnGround = false,
+ Sprinting = true,
+ MovementSpeed = 0.1f,
+ Yaw = 311f,
+ Pitch = 0f
+ };
+
+ var input = new MovementInput();
+ TemplateState state = TemplateState.InProgress;
+ Location finalPos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
+ var trace = new List();
+ int elapsedTicks = 0;
+ for (; elapsedTicks < 80; elapsedTicks++)
+ {
+ input.Reset();
+ Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
+ state = template.Tick(pos, physics, input, world);
+ if (elapsedTicks < 30 || state != TemplateState.InProgress)
+ {
+ trace.Add(
+ $"tick={elapsedTicks} state={state} pos={pos} yaw={physics.Yaw:F1} vel={physics.DeltaMovement} " +
+ $"onGround={physics.OnGround} input(F={input.Forward},B={input.Back},J={input.Jump},S={input.Sprint})");
+ }
+ if (state != TemplateState.InProgress)
+ {
+ finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z);
+ break;
+ }
+
+ physics.ApplyInput(input);
+ physics.Tick(world);
+ finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z);
+ }
+
+ Assert.True(
+ state == TemplateState.Complete,
+ $"state={state} elapsed={elapsedTicks} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}");
+ Assert.True(
+ physics.OnGround,
+ $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}");
+ Assert.True(
+ TemplateFootingHelper.IsCenterInsideTargetBlock(finalPos, ascend.End),
+ $"finalPos={finalPos} target={ascend.End}\n{string.Join('\n', trace)}");
+ Assert.True(
+ elapsedTicks <= 30,
+ $"completion took {elapsedTicks} ticks; expected post-landing shortcut to fire promptly\n{string.Join('\n', trace)}");
+ }
}
diff --git a/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs
index dedb7818..5cede71f 100644
--- a/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs
+++ b/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs
@@ -43,6 +43,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
private Location _lastPos;
private int _stuckTicks;
private bool _initiatedJump;
+ private bool _hasBeenAirborne;
private int _diagonalBrakeTicks;
public AscendTemplate(PathSegment segment, PathSegment? nextSegment)
@@ -182,8 +183,38 @@ namespace MinecraftClient.Pathing.Execution.Templates
}
}
+ if (!physics.OnGround)
+ _hasBeenAirborne = true;
+
if (physics.OnGround && Math.Abs(dy) < 0.2)
{
+ // Post-landing shortcut: once the Ascend's jump arc has put the
+ // bot back on ground at the target's elevation with its center
+ // inside the target column, the segment has done its job. Hand
+ // off to the next template (which snaps yaw on its first tick)
+ // instead of trying to brake or settle to stable footing.
+ //
+ // Holding onto the segment here re-runs both the AscendTemplate
+ // top-level yaw smoothing toward targetYaw (a moving bearing as
+ // the bot drifts past End) AND GroundedSegmentController's
+ // segment/exit-heading rotation each tick. The two competing
+ // yaw targets (e.g. yaw=233 anti-velocity vs yaw=315 segment
+ // heading vs yaw=270 exit heading on a Descend->PrepareJump->
+ // Ascend->Turn chain) oscillate the bot ~80 ticks until it
+ // walks off the 1-block landing's edge and the segment fails.
+ // Mirrors the existing "Ascend completes on PrepareJump as
+ // soon as center is inside the target block" gate in
+ // GroundedSegmentController.ShouldComplete. FinalStop is
+ // excluded because the last segment must come to rest at
+ // the goal: hand it back to GroundedSegmentController,
+ // which uses IsSettledAtEnd to detect a true stop.
+ if (_hasBeenAirborne
+ && _segment.ExitTransition != PathTransitionType.FinalStop
+ && TemplateFootingHelper.IsCenterInsideTargetBlock(pos, _segment.End))
+ {
+ return TemplateState.Complete;
+ }
+
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
diff --git a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs
index 3cee5861..e481e361 100644
--- a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs
+++ b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs
@@ -150,9 +150,31 @@ namespace MinecraftClient.Pathing.Execution.Templates
&& (onOrPastTarget
|| (_hasFallen
&& TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment, distanceThreshold: 1.5))));
- float airborneYaw = biasTowardExitInAir
- ? TemplateHelper.GetExitHeadingYaw(_segment)
- : targetYaw;
+ float airborneYaw;
+ if (biasTowardExitInAir)
+ {
+ airborneYaw = TemplateHelper.GetExitHeadingYaw(_segment);
+ }
+ else if (!isSingleStepDescend)
+ {
+ // Multi-block descend: target-tracking yaw rotates as
+ // the bot drifts past the landing column mid-fall (e.g.
+ // a diagonal 3 c2c drop with dx=-1, dz=-1 starts at
+ // yaw=135, the relative bearing to End flips through
+ // 90 -> 0 -> 315 in 6 air ticks). With Forward input
+ // held, the rotating yaw pushes air-control momentum
+ // perpendicular to the planned trajectory, drifting
+ // the bot ~0.5 m past the landing column and onto an
+ // adjacent block one tier below. Lock airborne yaw to
+ // the segment's start->end heading so air drift stays
+ // aligned with the planned diagonal; the GroundedSegment
+ // controller takes over once the bot is on the landing.
+ airborneYaw = TemplateHelper.CalculateYaw(_segment.HeadingX, _segment.HeadingZ);
+ }
+ else
+ {
+ airborneYaw = targetYaw;
+ }
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, airborneYaw);
if (_hasFallen || YawDifference(physics.Yaw, airborneYaw) <= PreDropYawToleranceDeg)