From 6b449cc72a0f39c7b9387ca7fe7b7a977441c552 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 12 Apr 2026 21:33:24 +0800 Subject: [PATCH] feat: converge grounded path segment completion --- .../Execution/ClimbFallTemplateTests.cs | 99 +++++++++++++++++++ .../Pathing/Execution/FlatWorldTestBuilder.cs | 72 +++++++++++++- .../GroundedTemplateConvergenceTests.cs | 84 ++++++++++++++++ .../Execution/PathExecutorCompletionTests.cs | 3 +- .../Pathing/Execution/TemplateFootingTests.cs | 47 +++++++++ .../Execution/TemplateSimulationRunner.cs | 42 ++++++++ .../Execution/Templates/AscendTemplate.cs | 20 +--- .../Execution/Templates/DescendTemplate.cs | 18 +--- .../Templates/GroundedSegmentController.cs | 27 +++++ .../Templates/TemplateFootingHelper.cs | 60 +++++++++++ .../Execution/Templates/TemplateHelper.cs | 43 ++++++++ .../Execution/Templates/WalkTemplate.cs | 17 ++-- 12 files changed, 489 insertions(+), 43 deletions(-) create mode 100644 MinecraftClient.Tests/Pathing/Execution/ClimbFallTemplateTests.cs create mode 100644 MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs create mode 100644 MinecraftClient.Tests/Pathing/Execution/TemplateFootingTests.cs create mode 100644 MinecraftClient.Tests/Pathing/Execution/TemplateSimulationRunner.cs create mode 100644 MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs create mode 100644 MinecraftClient/Pathing/Execution/Templates/TemplateFootingHelper.cs diff --git a/MinecraftClient.Tests/Pathing/Execution/ClimbFallTemplateTests.cs b/MinecraftClient.Tests/Pathing/Execution/ClimbFallTemplateTests.cs new file mode 100644 index 00000000..9285779b --- /dev/null +++ b/MinecraftClient.Tests/Pathing/Execution/ClimbFallTemplateTests.cs @@ -0,0 +1,99 @@ +using System; +using MinecraftClient.Mapping; +using MinecraftClient.Pathing.Core; +using MinecraftClient.Pathing.Execution; +using MinecraftClient.Pathing.Execution.Templates; +using MinecraftClient.Physics; +using Xunit; + +namespace MinecraftClient.Tests.Pathing.Execution; + +public sealed class ClimbFallTemplateTests +{ + [Fact] + public void ClimbTemplate_AscendsLadderColumn_CompletesOverTarget() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: -2, max: 2); + BuildLadder(world, x: 0, z: 0, bottomY: 80, topY: 84); + + var segment = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(0.5, 84, 0.5), + MoveType = MoveType.Climb, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new ClimbTemplate(segment, null); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 0f); + physics.OnClimbable = true; + + TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 220, out Location finalPos); + + Assert.Equal(TemplateState.Complete, state); + AssertNearTargetBlock(finalPos, segment.End); + } + + [Fact] + public void ClimbTemplate_DescendsLadderColumn_CompletesOverTarget() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: -2, max: 2); + BuildLadder(world, x: 0, z: 0, bottomY: 80, topY: 84); + + var segment = new PathSegment + { + Start = new Location(0.5, 84, 0.5), + End = new Location(0.5, 80, 0.5), + MoveType = MoveType.Climb, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new ClimbTemplate(segment, null); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 180f); + physics.OnClimbable = true; + + TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 220, out Location finalPos); + + Assert.Equal(TemplateState.Complete, state); + AssertNearTargetBlock(finalPos, segment.End); + } + + [Fact] + public void FallTemplate_DropsStraightDown_CompletesOnFloor() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(min: 4, max: 8); + + var segment = new PathSegment + { + Start = new Location(5.5, 85, 5.5), + End = new Location(5.5, 80, 5.5), + MoveType = MoveType.Fall, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new FallTemplate(segment, null); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 90f); + physics.OnGround = false; + physics.DeltaMovement = new Vec3d(0, -0.15, 0); + + TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 260, out Location finalPos); + + Assert.Equal(TemplateState.Complete, state); + AssertNearTargetBlock(finalPos, segment.End); + } + + private static void AssertNearTargetBlock(Location actual, Location target) + { + Assert.True(Math.Abs(actual.Y - target.Y) < 0.6, $"Expected final Y near {target.Y:F2}, got {actual.Y:F2}"); + Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(actual, target), + $"Expected the final footprint to stay within {target}, got {actual}"); + } + + private static void BuildLadder(World world, int x, int z, int bottomY, int topY) + { + for (int y = bottomY; y <= topY; y++) + { + FlatWorldTestBuilder.SetClimbable(world, x, y, z); + } + } +} diff --git a/MinecraftClient.Tests/Pathing/Execution/FlatWorldTestBuilder.cs b/MinecraftClient.Tests/Pathing/Execution/FlatWorldTestBuilder.cs index 5f282b29..70b14a63 100644 --- a/MinecraftClient.Tests/Pathing/Execution/FlatWorldTestBuilder.cs +++ b/MinecraftClient.Tests/Pathing/Execution/FlatWorldTestBuilder.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Threading; using MinecraftClient.Mapping; +using MinecraftClient.Mapping.BlockPalettes; +using MinecraftClient.Physics; namespace MinecraftClient.Tests.Pathing.Execution; @@ -8,6 +11,7 @@ internal static class FlatWorldTestBuilder { private static readonly Lock InitLock = new(); private static bool _defaultsLoaded; + private static readonly Dictionary MaterialIds = new(); public static World CreateStoneFloor(int floorY = 79, int min = -32, int max = 32) { @@ -30,13 +34,56 @@ internal static class FlatWorldTestBuilder { for (int z = min; z <= max; z++) { - world.SetBlock(new Location(x, floorY, z), new Block(1)); + SetSolid(world, x, floorY, z); } } return world; } + public static void SetSolid(World world, int x, int y, int z) + { + SetMaterial(world, x, y, z, Material.Stone); + } + + public static void FillSolid(World world, int x1, int y1, int z1, int x2, int y2, int z2) + { + for (int x = Math.Min(x1, x2); x <= Math.Max(x1, x2); x++) + { + for (int y = Math.Min(y1, y2); y <= Math.Max(y1, y2); y++) + { + for (int z = Math.Min(z1, z2); z <= Math.Max(z1, z2); z++) + { + SetSolid(world, x, y, z); + } + } + } + } + + public static void ClearBox(World world, int x1, int y1, int z1, int x2, int y2, int z2) + { + for (int x = Math.Min(x1, x2); x <= Math.Max(x1, x2); x++) + { + for (int y = Math.Min(y1, y2); y <= Math.Max(y1, y2); y++) + { + for (int z = Math.Min(z1, z2); z <= Math.Max(z1, z2); z++) + { + world.SetBlock(new Location(x, y, z), Block.Air); + } + } + } + } + + public static void SetMaterial(World world, int x, int y, int z, Material material) + { + world.SetBlock(new Location(x, y, z), new Block(ResolveMaterialId(material))); + } + + public static void SetClimbable(World world, int x, int y, int z) + { + SetMaterial(world, x, y, z, Material.Ladder); + } + private static void EnsureDefaultDimensionsLoaded() { lock (InitLock) @@ -44,8 +91,31 @@ internal static class FlatWorldTestBuilder if (_defaultsLoaded) return; + Block.Palette = new Palette1219(); World.LoadDefaultDimensions1206Plus(); + BlockShapes.Initialize(); _defaultsLoaded = true; } } + + private static ushort ResolveMaterialId(Material material) + { + lock (InitLock) + { + if (MaterialIds.TryGetValue(material, out ushort id)) + return id; + + for (int candidate = 0; candidate <= ushort.MaxValue; candidate++) + { + if (Block.Palette.FromId(candidate) == material) + { + ushort resolved = (ushort)candidate; + MaterialIds[material] = resolved; + return resolved; + } + } + + throw new InvalidOperationException($"Could not resolve a block id for material {material}"); + } + } } diff --git a/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs new file mode 100644 index 00000000..f56e9fc3 --- /dev/null +++ b/MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs @@ -0,0 +1,84 @@ +using MinecraftClient.Mapping; +using MinecraftClient.Pathing.Core; +using MinecraftClient.Pathing.Execution; +using MinecraftClient.Pathing.Execution.Templates; +using Xunit; + +namespace MinecraftClient.Tests.Pathing.Execution; + +public sealed class GroundedTemplateConvergenceTests +{ + [Fact] + public void WalkTemplate_FinalStop_Completes_WhenFootprintStaysInsideTargetBlock() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(); + var segment = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(1.5, 80, 0.5), + MoveType = MoveType.Traverse, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new WalkTemplate(segment, null); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 270f); + + TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 160, out Location finalPos); + + Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}"); + Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End)); + } + + [Fact] + public void WalkTemplate_PrepareJump_CompletesWithoutSettlingOnRunUpBlock() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(); + var current = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(1.5, 80, 0.5), + MoveType = MoveType.Traverse, + ExitTransition = PathTransitionType.PrepareJump, + PreserveSprint = true + }; + var next = new PathSegment + { + Start = new Location(1.5, 80, 0.5), + End = new Location(3.5, 80, 0.5), + MoveType = MoveType.Parkour, + ExitTransition = PathTransitionType.FinalStop + }; + + var template = new WalkTemplate(current, next); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(current.Start, yaw: 270f); + + TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 60, out _); + + Assert.Equal(TemplateState.Complete, state); + Assert.True(physics.DeltaMovement.X > 0.02); + } + + [Fact] + public void DescendTemplate_LandingRecovery_CompletesOnLandingBlock() + { + World world = FlatWorldTestBuilder.CreateStoneFloor(); + FlatWorldTestBuilder.ClearBox(world, 1, 79, 0, 1, 79, 0); + FlatWorldTestBuilder.SetSolid(world, 1, 78, 0); + + var segment = new PathSegment + { + Start = new Location(0.5, 80, 0.5), + End = new Location(1.5, 79, 0.5), + MoveType = MoveType.Descend, + ExitTransition = PathTransitionType.LandingRecovery + }; + + var template = new DescendTemplate(segment, null); + var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 270f); + + TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 240, out Location finalPos); + + Assert.True(state == TemplateState.Complete, $"state={state} finalPos={finalPos} vel={physics.DeltaMovement}"); + Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End)); + } +} diff --git a/MinecraftClient.Tests/Pathing/Execution/PathExecutorCompletionTests.cs b/MinecraftClient.Tests/Pathing/Execution/PathExecutorCompletionTests.cs index cf8920df..488e6882 100644 --- a/MinecraftClient.Tests/Pathing/Execution/PathExecutorCompletionTests.cs +++ b/MinecraftClient.Tests/Pathing/Execution/PathExecutorCompletionTests.cs @@ -24,7 +24,8 @@ public sealed class PathExecutorCompletionTests var physics = new PlayerPhysics { Yaw = 270f, - Pitch = 0f + Pitch = 0f, + OnGround = true }; var input = new MovementInput(); var pos = new Location(1.48, 80, 0.5); diff --git a/MinecraftClient.Tests/Pathing/Execution/TemplateFootingTests.cs b/MinecraftClient.Tests/Pathing/Execution/TemplateFootingTests.cs new file mode 100644 index 00000000..15479a12 --- /dev/null +++ b/MinecraftClient.Tests/Pathing/Execution/TemplateFootingTests.cs @@ -0,0 +1,47 @@ +using MinecraftClient.Mapping; +using MinecraftClient.Pathing.Execution.Templates; +using MinecraftClient.Physics; +using Xunit; + +namespace MinecraftClient.Tests.Pathing.Execution; + +public sealed class TemplateFootingTests +{ + [Fact] + public void IsFootprintInsideTargetBlock_ReturnsTrue_WhenPlayerIsNearEdgeButStillInside() + { + bool inside = TemplateFootingHelper.IsFootprintInsideTargetBlock( + new Location(10.69, 80.0, 4.50), + new Location(10.50, 80.0, 4.50)); + + Assert.True(inside); + } + + [Fact] + public void IsFootprintInsideTargetBlock_ReturnsFalse_WhenPlayerCrossesBlockEdge() + { + bool inside = TemplateFootingHelper.IsFootprintInsideTargetBlock( + new Location(10.81, 80.0, 4.50), + new Location(10.50, 80.0, 4.50)); + + Assert.False(inside); + } + + [Fact] + public void WillLeaveTargetBlockNextTick_ReturnsTrue_WhenVelocityWouldCarryPastEdge() + { + var physics = new PlayerPhysics + { + Position = new Vec3d(10.67, 80.0, 4.50), + DeltaMovement = new Vec3d(0.060, 0.0, 0.0), + OnGround = true + }; + + bool exitsNextTick = TemplateFootingHelper.WillLeaveTargetBlockNextTick( + new Location(10.67, 80.0, 4.50), + physics, + new Location(10.50, 80.0, 4.50)); + + Assert.True(exitsNextTick); + } +} diff --git a/MinecraftClient.Tests/Pathing/Execution/TemplateSimulationRunner.cs b/MinecraftClient.Tests/Pathing/Execution/TemplateSimulationRunner.cs new file mode 100644 index 00000000..9f716563 --- /dev/null +++ b/MinecraftClient.Tests/Pathing/Execution/TemplateSimulationRunner.cs @@ -0,0 +1,42 @@ +using MinecraftClient.Mapping; +using MinecraftClient.Pathing.Execution; +using MinecraftClient.Physics; + +namespace MinecraftClient.Tests.Pathing.Execution; + +internal static class TemplateSimulationRunner +{ + internal static PlayerPhysics CreateGroundedPhysics(Location start, float yaw) + { + return new PlayerPhysics + { + Position = new Vec3d(start.X, start.Y, start.Z), + DeltaMovement = Vec3d.Zero, + OnGround = true, + MovementSpeed = 0.1f, + Yaw = yaw, + Pitch = 0f + }; + } + + internal static TemplateState Run(IActionTemplate template, PlayerPhysics physics, World world, int maxTicks, out Location finalPos) + { + var input = new MovementInput(); + TemplateState state = TemplateState.InProgress; + + for (int tick = 0; tick < maxTicks; tick++) + { + input.Reset(); + Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z); + state = template.Tick(pos, physics, input, world); + if (state != TemplateState.InProgress) + break; + + physics.ApplyInput(input); + physics.Tick(world); + } + + finalPos = new Location(physics.Position.X, physics.Position.Y, physics.Position.Z); + return state; + } +} diff --git a/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs index 536fae7b..a9126e8e 100644 --- a/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs +++ b/MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs @@ -47,25 +47,11 @@ namespace MinecraftClient.Pathing.Execution.Templates if (physics.OnGround && dy > 0.1) input.Jump = true; - if (physics.OnGround && Math.Abs(dy) < 0.15) + if (physics.OnGround && Math.Abs(dy) < 0.2) { - TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world); - TemplateHelper.ApplyDecision(input, decision); - if (decision.HoldBack) - TemplateHelper.FaceSegmentHeading(physics, _segment); - - if (_segment.ExitTransition == PathTransitionType.ContinueStraight && horizDistSq < 0.25) + GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world); + if (GroundedSegmentController.ShouldComplete(_segment, pos, physics)) return TemplateState.Complete; - - if (_segment.ExitTransition != PathTransitionType.ContinueStraight - && TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics, horizThresholdSq: 0.0025)) - { - return TemplateState.Complete; - } - } - else if (horizDistSq < 0.25 && Math.Abs(dy) < 0.8) - { - return TemplateState.Complete; } double movedSq = TemplateHelper.HorizontalDistanceSq(pos, _lastPos); diff --git a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs index aa7edfe4..e88e91be 100644 --- a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs +++ b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs @@ -59,26 +59,14 @@ namespace MinecraftClient.Pathing.Execution.Templates float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz); physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch); - if (physics.OnGround && Math.Abs(dy) < (_hasFallen ? 0.8 : 0.5)) + if (physics.OnGround && Math.Abs(dy) < (_hasFallen ? 1.0 : 0.6)) { if (horizDistSq > 0.01) physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw); - TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world); - TemplateHelper.ApplyDecision(input, decision); - if (decision.HoldBack) - TemplateHelper.FaceSegmentHeading(physics, _segment); - - if (_segment.ExitTransition == PathTransitionType.ContinueStraight) - { - double completionThreshold = _hasFallen ? 0.5 : 0.25; - if (horizDistSq < completionThreshold) - return TemplateState.Complete; - } - else if (TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics, horizThresholdSq: 0.0025)) - { + GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world); + if (GroundedSegmentController.ShouldComplete(_segment, pos, physics)) return TemplateState.Complete; - } } else if (physics.OnClimbable) { diff --git a/MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs b/MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs new file mode 100644 index 00000000..23d2fafb --- /dev/null +++ b/MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs @@ -0,0 +1,27 @@ +using MinecraftClient.Mapping; +using MinecraftClient.Physics; + +namespace MinecraftClient.Pathing.Execution.Templates +{ + internal static class GroundedSegmentController + { + internal static void Apply(PathSegment segment, PathSegment? nextSegment, Location pos, PlayerPhysics physics, MovementInput input, World world) + { + TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(segment, nextSegment, pos, physics, world); + TemplateHelper.ApplyDecision(input, decision); + if (decision.HoldBack) + TemplateHelper.FaceSegmentHeading(physics, segment); + } + + internal static bool ShouldComplete(PathSegment segment, Location pos, PlayerPhysics physics) + { + return segment.ExitTransition switch + { + PathTransitionType.ContinueStraight => TemplateHelper.IsNear(pos, segment.End, horizThresholdSq: 0.09), + PathTransitionType.PrepareJump => TemplateHelper.HasReachedSegmentEndPlane(pos, segment) + && TemplateHelper.ProjectHorizontalSpeedAlongSegment(physics, segment) > 0.02, + _ => physics.OnGround && TemplateHelper.IsSettledOnTargetBlock(pos, segment.End, physics) + }; + } + } +} diff --git a/MinecraftClient/Pathing/Execution/Templates/TemplateFootingHelper.cs b/MinecraftClient/Pathing/Execution/Templates/TemplateFootingHelper.cs new file mode 100644 index 00000000..8966e387 --- /dev/null +++ b/MinecraftClient/Pathing/Execution/Templates/TemplateFootingHelper.cs @@ -0,0 +1,60 @@ +using System; +using MinecraftClient.Mapping; +using MinecraftClient.Physics; + +namespace MinecraftClient.Pathing.Execution.Templates +{ + public static class TemplateFootingHelper + { + private const double HalfWidth = PhysicsConsts.PlayerWidth / 2.0; + + public static bool IsFootprintInsideTargetBlock(Location pos, Location target, double epsilon = 1.0E-4) + { + double minX = pos.X - HalfWidth; + double maxX = pos.X + HalfWidth; + double minZ = pos.Z - HalfWidth; + double maxZ = pos.Z + HalfWidth; + + double blockMinX = Math.Floor(target.X); + double blockMaxX = blockMinX + 1.0; + double blockMinZ = Math.Floor(target.Z); + double blockMaxZ = blockMinZ + 1.0; + + return minX >= blockMinX - epsilon + && maxX <= blockMaxX + epsilon + && minZ >= blockMinZ - epsilon + && maxZ <= blockMaxZ + epsilon; + } + + public static bool WillLeaveTargetBlockNextTick(Location pos, PlayerPhysics physics, Location target, double epsilon = 1.0E-4) + { + Location nextPos = new( + pos.X + physics.DeltaMovement.X, + pos.Y, + pos.Z + physics.DeltaMovement.Z); + return !IsFootprintInsideTargetBlock(nextPos, target, epsilon); + } + + public static bool WillCrossSupportExitNextTick(Location pos, PlayerPhysics physics, PathSegment segment, double epsilon = 1.0E-4) + { + double nextX = pos.X + physics.DeltaMovement.X; + double nextZ = pos.Z + physics.DeltaMovement.Z; + + double blockMinX = Math.Floor(segment.End.X); + double blockMaxX = blockMinX + 1.0; + double blockMinZ = Math.Floor(segment.End.Z); + double blockMaxZ = blockMinZ + 1.0; + + if (segment.HeadingX > 0 && nextX > blockMaxX - HalfWidth + epsilon) + return true; + if (segment.HeadingX < 0 && nextX < blockMinX + HalfWidth - epsilon) + return true; + if (segment.HeadingZ > 0 && nextZ > blockMaxZ - HalfWidth + epsilon) + return true; + if (segment.HeadingZ < 0 && nextZ < blockMinZ + HalfWidth - epsilon) + return true; + + return false; + } + } +} diff --git a/MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs b/MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs index 3a07c910..e01fdc05 100644 --- a/MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs +++ b/MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs @@ -90,14 +90,57 @@ namespace MinecraftClient.Pathing.Execution.Templates input.Back = decision.HoldBack; } + internal static bool HasReachedSegmentEndPlane(Location pos, PathSegment segment, double tolerance = 0.05) + { + GetNormalizedSegmentDirection(segment, out double dirX, out double dirZ); + double relX = pos.X - segment.End.X; + double relZ = pos.Z - segment.End.Z; + return relX * dirX + relZ * dirZ >= -tolerance; + } + + internal static double ProjectHorizontalSpeedAlongSegment(PlayerPhysics physics, PathSegment segment) + { + GetNormalizedSegmentDirection(segment, out double dirX, out double dirZ); + return physics.DeltaMovement.X * dirX + physics.DeltaMovement.Z * dirZ; + } + + internal static bool IsSettledOnTargetBlock(Location pos, Location target, PlayerPhysics physics, + double speedThresholdSq = 0.0016) + { + double horizontalSpeedSq = physics.DeltaMovement.X * physics.DeltaMovement.X + + physics.DeltaMovement.Z * physics.DeltaMovement.Z; + return TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, target) + && !TemplateFootingHelper.WillLeaveTargetBlockNextTick(pos, physics, target) + && horizontalSpeedSq <= speedThresholdSq; + } + internal static bool IsSettledAtEnd(Location pos, Location target, PlayerPhysics physics, double horizThresholdSq = 0.0025, double speedThresholdSq = 0.0016) { + if (IsSettledOnTargetBlock(pos, target, physics, speedThresholdSq)) + return true; + double dx = target.X - pos.X; double dz = target.Z - pos.Z; double horizontalSpeedSq = physics.DeltaMovement.X * physics.DeltaMovement.X + physics.DeltaMovement.Z * physics.DeltaMovement.Z; return dx * dx + dz * dz <= horizThresholdSq && horizontalSpeedSq <= speedThresholdSq; } + + private static void GetNormalizedSegmentDirection(PathSegment segment, out double dirX, out double dirZ) + { + dirX = segment.End.X - segment.Start.X; + dirZ = segment.End.Z - segment.Start.Z; + double len = Math.Sqrt(dirX * dirX + dirZ * dirZ); + if (len < 1.0E-6) + { + dirX = 0.0; + dirZ = 0.0; + return; + } + + dirX /= len; + dirZ /= len; + } } } diff --git a/MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs index 79c99e0b..5e8a1d47 100644 --- a/MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs +++ b/MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs @@ -40,22 +40,21 @@ namespace MinecraftClient.Pathing.Execution.Templates physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw); physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch); - TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world); - TemplateHelper.ApplyDecision(input, decision); - if (decision.HoldBack) - TemplateHelper.FaceSegmentHeading(physics, _segment); + GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world); - if (_segment.ExitTransition == PathTransitionType.ContinueStraight && TemplateHelper.IsNear(pos, ExpectedEnd, horizThresholdSq: 0.09)) - return TemplateState.Complete; - - if (_segment.ExitTransition != PathTransitionType.ContinueStraight && TemplateHelper.IsSettledAtEnd(pos, ExpectedEnd, physics)) + if (GroundedSegmentController.ShouldComplete(_segment, pos, physics)) return TemplateState.Complete; double movedSq = TemplateHelper.HorizontalDistanceSq(pos, _lastPos); _stuckTicks = movedSq < 0.0005 ? _stuckTicks + 1 : 0; _lastPos = pos; - int maxTicks = _segment.ExitTransition == PathTransitionType.ContinueStraight ? 100 : 140; + int maxTicks = _segment.ExitTransition switch + { + PathTransitionType.ContinueStraight => 100, + PathTransitionType.PrepareJump => 80, + _ => 140 + }; if (_stuckTicks > 40 || _tickCount > maxTicks) return TemplateState.Failed;