pathing: route Descend->turn handoff through LandingRecovery hints

When a Descend segment is followed by another Descend (or Traverse/
Diagonal) with a different heading, PathSegmentBuilder.Classify
correctly assigned ExitTransition=LandingRecovery, but BuildHints fell
into the `if (turning)` branch first and returned hints with
RequireStableFooting=true. That gate forces GroundedSegmentController to
wait for IsSettledOnTargetBlock (footprint inside, won't leave next tick,
horizontal speed^2 <= 0.0016), so a multi-block diagonal Descend that
landed inside the target block while still carrying ~0.02 m/tick of
residual jump momentum oscillated in place for ~60 ticks (3 seconds)
until the speed decayed.

Move the LandingRecovery branch ahead of the turning branch so the
Descend-carry handoff uses RequireStableFooting=false and the
ShouldComplete shortcut (LandingRecovery + footprint inside target on
the ground) fires the moment the bot reaches the landing column.

Adds two regression tests covering the Descend->turning-Descend handoff
and a sanity guard that ordinary Traverse->turning-Traverse still uses
the turning branch.

Also lifts DiagnosticsTailSize from 64 to 200 and emits an automatic
"slow segment" tick dump from PathSegmentManager whenever a segment
takes >=25 ticks, which is what surfaced this stall.

Made-with: Cursor
This commit is contained in:
BruceChen 2026-04-26 06:10:25 +00:00
parent 1c2e6fba2b
commit b35cdfc40f
3 changed files with 94 additions and 15 deletions

View file

@ -49,6 +49,45 @@ public sealed class PathSegmentBuilderTests
Assert.True(segments[0].PreserveSprint);
}
[Fact]
public void FromPath_DescendIntoTurningDescend_UsesLandingRecoveryHints()
{
// Regression: a Descend that lands and immediately steps into a
// perpendicular Descend (different heading) used to receive the
// turning-branch hints with RequireStableFooting=true. That gate forces
// GroundedSegmentController to wait for IsSettledOnTargetBlock, which
// takes ~3 seconds while residual jump momentum decays. The
// LandingRecovery branch (RequireStableFooting=false) lets the
// ShouldComplete shortcut fire as soon as the bot's footprint is
// inside the landing block.
var nodes = BuildNodes(
(255, 137, 220, MoveType.Traverse),
(256, 134, 219, MoveType.Descend),
(256, 132, 217, MoveType.Descend));
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
Assert.Equal(PathTransitionType.LandingRecovery, segments[0].ExitTransition);
Assert.False(segments[0].ExitHints.RequireStableFooting);
}
[Fact]
public void FromPath_TraverseIntoTurningTraverse_StillUsesTurnHints()
{
// Sanity guard: ordinary Traverse → turning-Traverse must still use the
// turning branch (StableFooting=true) — only Descend/Parkour/Fall
// sources should bypass it.
var nodes = BuildNodes(
(0, 80, 0, MoveType.Traverse),
(1, 80, 0, MoveType.Traverse),
(1, 80, 1, MoveType.Traverse));
List<PathSegment> segments = PathSegmentBuilder.FromPath(nodes);
Assert.Equal(PathTransitionType.Turn, segments[0].ExitTransition);
Assert.True(segments[0].ExitHints.RequireStableFooting);
}
[Fact]
public void FromPath_CopiesParkourProfile_ToRuntimeSegment()
{