mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: add transition-aware path execution core
This commit is contained in:
parent
7ad0a57a3e
commit
360883acf3
14 changed files with 755 additions and 94 deletions
|
|
@ -131,6 +131,22 @@ namespace MinecraftClient.Pathing.Core
|
|||
CancellationToken ct,
|
||||
long timeoutMs = 5000)
|
||||
{
|
||||
if (goal.IsInGoal(startX, startY, startZ))
|
||||
{
|
||||
DebugLog?.Invoke($"[A*] Already in goal at ({startX},{startY},{startZ})");
|
||||
return new PathResult(
|
||||
PathStatus.Success,
|
||||
[new PathNode(startX, startY, startZ)],
|
||||
nodesExplored: 0,
|
||||
elapsedMs: 0);
|
||||
}
|
||||
|
||||
if (!IsGoalReachableFootPosition(ctx, goal))
|
||||
{
|
||||
DebugLog?.Invoke($"[A*] Goal {goal} is not a reachable foot position");
|
||||
return PathResult.Fail(nodesExplored: 0, elapsedMs: 0);
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var openSet = new BinaryHeapOpenSet(4096);
|
||||
var nodeMap = new Dictionary<long, PathNode>(4096);
|
||||
|
|
@ -259,5 +275,21 @@ namespace MinecraftClient.Pathing.Core
|
|||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
private static bool IsGoalReachableFootPosition(CalculationContext ctx, IGoal goal)
|
||||
{
|
||||
if (goal is not GoalBlock blockGoal)
|
||||
return true;
|
||||
|
||||
if (!ctx.IsChunkLoaded(blockGoal.X, blockGoal.Z))
|
||||
return true;
|
||||
|
||||
if (blockGoal.Y == int.MinValue)
|
||||
return false;
|
||||
|
||||
return ctx.CanWalkOn(blockGoal.X, blockGoal.Y - 1, blockGoal.Z)
|
||||
&& ctx.CanWalkThrough(blockGoal.X, blockGoal.Y, blockGoal.Z)
|
||||
&& ctx.CanWalkThrough(blockGoal.X, blockGoal.Y + 1, blockGoal.Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
public required Location End { get; init; }
|
||||
public required MoveType MoveType { get; init; }
|
||||
public PathTransitionType ExitTransition { get; init; } = PathTransitionType.FinalStop;
|
||||
public PathTransitionHints ExitHints { get; init; } = PathTransitionHints.Default;
|
||||
public bool PreserveSprint { get; init; }
|
||||
|
||||
public int HeadingX => Math.Sign(End.X - Start.X);
|
||||
|
|
|
|||
|
|
@ -12,27 +12,9 @@ namespace MinecraftClient.Pathing.Execution
|
|||
var segments = new List<PathSegment>(Math.Max(0, nodes.Count - 1));
|
||||
for (int i = 1; i < nodes.Count; i++)
|
||||
{
|
||||
PathSegment? next = null;
|
||||
if (i + 1 < nodes.Count)
|
||||
{
|
||||
var nextNode = nodes[i + 1];
|
||||
var curr = nodes[i];
|
||||
next = new PathSegment
|
||||
{
|
||||
Start = new Location(curr.X + 0.5, curr.Y, curr.Z + 0.5),
|
||||
End = new Location(nextNode.X + 0.5, nextNode.Y, nextNode.Z + 0.5),
|
||||
MoveType = nextNode.MoveUsed
|
||||
};
|
||||
}
|
||||
|
||||
var prev = nodes[i - 1];
|
||||
var currNode = nodes[i];
|
||||
var current = new PathSegment
|
||||
{
|
||||
Start = new Location(prev.X + 0.5, prev.Y, prev.Z + 0.5),
|
||||
End = new Location(currNode.X + 0.5, currNode.Y, currNode.Z + 0.5),
|
||||
MoveType = currNode.MoveUsed
|
||||
};
|
||||
PathSegment current = CreatePreview(nodes[i - 1], nodes[i]);
|
||||
PathSegment? next = i + 1 < nodes.Count ? CreatePreview(nodes[i], nodes[i + 1]) : null;
|
||||
PathSegment? nextNext = i + 2 < nodes.Count ? CreatePreview(nodes[i + 1], nodes[i + 2]) : null;
|
||||
|
||||
PathTransitionType exitTransition = Classify(current, next);
|
||||
segments.Add(new PathSegment
|
||||
|
|
@ -41,6 +23,7 @@ namespace MinecraftClient.Pathing.Execution
|
|||
End = current.End,
|
||||
MoveType = current.MoveType,
|
||||
ExitTransition = exitTransition,
|
||||
ExitHints = BuildHints(current, next, nextNext, exitTransition),
|
||||
PreserveSprint = exitTransition is PathTransitionType.ContinueStraight or PathTransitionType.PrepareJump
|
||||
});
|
||||
}
|
||||
|
|
@ -63,5 +46,90 @@ namespace MinecraftClient.Pathing.Execution
|
|||
|
||||
return PathTransitionType.Turn;
|
||||
}
|
||||
|
||||
private static PathSegment CreatePreview(PathNode start, PathNode end)
|
||||
{
|
||||
return new PathSegment
|
||||
{
|
||||
Start = new Location(start.X + 0.5, start.Y, start.Z + 0.5),
|
||||
End = new Location(end.X + 0.5, end.Y, end.Z + 0.5),
|
||||
MoveType = end.MoveUsed
|
||||
};
|
||||
}
|
||||
|
||||
private static PathTransitionHints BuildHints(PathSegment current, PathSegment? next, PathSegment? nextNext,
|
||||
PathTransitionType exitTransition)
|
||||
{
|
||||
if (next is null)
|
||||
{
|
||||
return new PathTransitionHints(
|
||||
DesiredHeadingX: current.HeadingX,
|
||||
DesiredHeadingZ: current.HeadingZ,
|
||||
MinExitSpeed: 0.0,
|
||||
MaxExitSpeed: 0.02,
|
||||
RequireStableFooting: true,
|
||||
RequireGrounded: true,
|
||||
RequireJumpReady: false,
|
||||
AllowAirBrake: false,
|
||||
HorizonTicks: 12);
|
||||
}
|
||||
|
||||
if (next.MoveType is MoveType.Parkour or MoveType.Ascend)
|
||||
{
|
||||
return new PathTransitionHints(
|
||||
DesiredHeadingX: next.HeadingX,
|
||||
DesiredHeadingZ: next.HeadingZ,
|
||||
MinExitSpeed: next.MoveType == MoveType.Parkour ? 0.10 : 0.0,
|
||||
MaxExitSpeed: double.PositiveInfinity,
|
||||
RequireStableFooting: false,
|
||||
RequireGrounded: true,
|
||||
RequireJumpReady: true,
|
||||
AllowAirBrake: false,
|
||||
HorizonTicks: 10);
|
||||
}
|
||||
|
||||
bool turning = current.HeadingX != next.HeadingX || current.HeadingZ != next.HeadingZ;
|
||||
bool nextImmediatelyJumps = nextNext is not null
|
||||
&& nextNext.MoveType is (MoveType.Parkour or MoveType.Ascend);
|
||||
|
||||
if (turning)
|
||||
{
|
||||
return new PathTransitionHints(
|
||||
DesiredHeadingX: next.HeadingX,
|
||||
DesiredHeadingZ: next.HeadingZ,
|
||||
MinExitSpeed: nextImmediatelyJumps ? 0.05 : 0.0,
|
||||
MaxExitSpeed: nextImmediatelyJumps ? 0.16 : 0.05,
|
||||
RequireStableFooting: !nextImmediatelyJumps,
|
||||
RequireGrounded: true,
|
||||
RequireJumpReady: nextImmediatelyJumps,
|
||||
AllowAirBrake: true,
|
||||
HorizonTicks: 12);
|
||||
}
|
||||
|
||||
if (exitTransition == PathTransitionType.LandingRecovery)
|
||||
{
|
||||
return new PathTransitionHints(
|
||||
DesiredHeadingX: next.HeadingX,
|
||||
DesiredHeadingZ: next.HeadingZ,
|
||||
MinExitSpeed: 0.03,
|
||||
MaxExitSpeed: double.PositiveInfinity,
|
||||
RequireStableFooting: false,
|
||||
RequireGrounded: true,
|
||||
RequireJumpReady: false,
|
||||
AllowAirBrake: true,
|
||||
HorizonTicks: 12);
|
||||
}
|
||||
|
||||
return new PathTransitionHints(
|
||||
DesiredHeadingX: next.HeadingX,
|
||||
DesiredHeadingZ: next.HeadingZ,
|
||||
MinExitSpeed: 0.06,
|
||||
MaxExitSpeed: double.PositiveInfinity,
|
||||
RequireStableFooting: false,
|
||||
RequireGrounded: false,
|
||||
RequireJumpReady: false,
|
||||
AllowAirBrake: false,
|
||||
HorizonTicks: 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,16 @@ namespace MinecraftClient.Pathing.Execution
|
|||
using var cts = new CancellationTokenSource();
|
||||
var result = finder.Calculate(ctx, sx, sy, sz, _goal, cts.Token, 3000);
|
||||
|
||||
bool alreadyInGoal = _goal.IsInGoal(sx, sy, sz)
|
||||
|| (result.Path.Count == 1 && _goal.IsInGoal(result.Path[0].X, result.Path[0].Y, result.Path[0].Z));
|
||||
if (alreadyInGoal)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Navigation complete!");
|
||||
_executor = null;
|
||||
_goal = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Status == PathStatus.Failed || result.Path.Count < 2)
|
||||
{
|
||||
_infoLog?.Invoke("[PathMgr] Replan failed -- no path found.");
|
||||
|
|
|
|||
25
MinecraftClient/Pathing/Execution/PathTransitionHints.cs
Normal file
25
MinecraftClient/Pathing/Execution/PathTransitionHints.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public sealed record PathTransitionHints(
|
||||
int DesiredHeadingX,
|
||||
int DesiredHeadingZ,
|
||||
double MinExitSpeed,
|
||||
double MaxExitSpeed,
|
||||
bool RequireStableFooting,
|
||||
bool RequireGrounded,
|
||||
bool RequireJumpReady,
|
||||
bool AllowAirBrake,
|
||||
int HorizonTicks)
|
||||
{
|
||||
public static PathTransitionHints Default { get; } = new(
|
||||
DesiredHeadingX: 0,
|
||||
DesiredHeadingZ: 0,
|
||||
MinExitSpeed: 0.0,
|
||||
MaxExitSpeed: double.PositiveInfinity,
|
||||
RequireStableFooting: false,
|
||||
RequireGrounded: false,
|
||||
RequireJumpReady: false,
|
||||
AllowAirBrake: false,
|
||||
HorizonTicks: 8);
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,12 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
{
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
|
||||
if (horizDistSq > 0.01 && !decision.HoldBack)
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
{
|
||||
float groundedYaw = TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment)
|
||||
? TemplateHelper.GetExitHeadingYaw(_segment)
|
||||
: targetYaw;
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, groundedYaw);
|
||||
}
|
||||
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldBack)
|
||||
|
|
@ -99,9 +104,19 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
}
|
||||
else
|
||||
{
|
||||
input.Forward = true;
|
||||
if (_needsSprint)
|
||||
input.Sprint = true;
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
|
||||
if (_segment.ExitHints.AllowAirBrake)
|
||||
{
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldForward && _needsSprint)
|
||||
input.Sprint = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
input.Forward = true;
|
||||
if (_needsSprint)
|
||||
input.Sprint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,7 +131,8 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
|
||||
double remaining = (_segment.End.X - pos.X) * _segment.HeadingX
|
||||
+ (_segment.End.Z - pos.Z) * _segment.HeadingZ;
|
||||
return remaining <= 0.55;
|
||||
return remaining <= 0.55
|
||||
&& TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, _segment.End);
|
||||
}
|
||||
|
||||
private static float YawDifference(float current, float target)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,13 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
{
|
||||
internal static class GroundedSegmentController
|
||||
{
|
||||
private const double FinalStopFastCompleteSpeed = 0.08;
|
||||
|
||||
internal static void Apply(PathSegment segment, PathSegment? nextSegment, Location pos, PlayerPhysics physics, MovementInput input, World world)
|
||||
{
|
||||
if (TemplateHelper.ShouldBiasTowardExitHeading(pos, segment))
|
||||
TemplateHelper.FaceExitHeading(physics, segment);
|
||||
|
||||
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(segment, nextSegment, pos, physics, world);
|
||||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldBack)
|
||||
|
|
@ -15,11 +20,69 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
|
||||
internal static bool ShouldComplete(PathSegment segment, Location pos, PlayerPhysics physics)
|
||||
{
|
||||
if (segment.ExitHints.RequireGrounded && !physics.OnGround)
|
||||
return false;
|
||||
|
||||
if (segment.ExitTransition == PathTransitionType.ContinueStraight
|
||||
&& physics.OnGround
|
||||
&& TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, segment.End)
|
||||
&& !TemplateFootingHelper.WillLeaveTargetBlockNextTick(pos, physics, segment.End))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (segment.ExitTransition == PathTransitionType.FinalStop
|
||||
&& physics.OnGround
|
||||
&& TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, segment.End)
|
||||
&& !TemplateFootingHelper.WillLeaveTargetBlockNextTick(pos, physics, segment.End))
|
||||
{
|
||||
return TemplateHelper.GetHorizontalSpeed(physics) <= FinalStopFastCompleteSpeed;
|
||||
}
|
||||
|
||||
double exitSpeed = TemplateHelper.ProjectHorizontalSpeedAlongHint(physics, segment);
|
||||
bool headingReady = TemplateHelper.HeadingPenaltyDegrees(physics.Yaw, segment)
|
||||
<= (segment.ExitHints.RequireJumpReady ? 8.0 : 15.0);
|
||||
|
||||
if (!headingReady)
|
||||
return false;
|
||||
|
||||
if (segment.ExitTransition == PathTransitionType.PrepareJump
|
||||
&& physics.OnGround
|
||||
&& segment.ExitHints.RequireJumpReady
|
||||
&& segment.ExitHints.MinExitSpeed <= 0.0
|
||||
&& TemplateFootingHelper.IsCenterInsideTargetBlock(pos, segment.End)
|
||||
&& TemplateHelper.RemainingDistanceAlongSegment(pos, segment) <= 0.30)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (exitSpeed < segment.ExitHints.MinExitSpeed)
|
||||
return false;
|
||||
|
||||
if (exitSpeed > segment.ExitHints.MaxExitSpeed)
|
||||
return false;
|
||||
|
||||
if (segment.ExitHints.RequireStableFooting)
|
||||
{
|
||||
return physics.OnGround
|
||||
&& (segment.ExitTransition == PathTransitionType.FinalStop
|
||||
? TemplateHelper.IsSettledAtEnd(pos, segment.End, physics)
|
||||
: TemplateHelper.IsSettledOnTargetBlock(pos, segment.End, physics));
|
||||
}
|
||||
|
||||
if (segment.ExitHints.RequireJumpReady)
|
||||
{
|
||||
return physics.OnGround
|
||||
&& TemplateHelper.HasReachedSegmentEndPlane(pos, segment)
|
||||
&& exitSpeed >= segment.ExitHints.MinExitSpeed;
|
||||
}
|
||||
|
||||
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,
|
||||
&& exitSpeed > 0.02,
|
||||
PathTransitionType.FinalStop => physics.OnGround && TemplateHelper.IsSettledAtEnd(pos, segment.End, physics),
|
||||
_ => physics.OnGround && TemplateHelper.IsSettledOnTargetBlock(pos, segment.End, physics)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private readonly double _horizDist;
|
||||
private int _tickCount;
|
||||
private Phase _phase = Phase.Approach;
|
||||
private bool _airReleaseCommitted;
|
||||
private bool _leftGround;
|
||||
|
||||
private const float YawToleranceDeg = 5f;
|
||||
|
|
@ -80,7 +79,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
minApproachSq = 0.64; // 0.8 blocks - 3+ ticks of sprint
|
||||
else if (_horizDist >= 4.0)
|
||||
minApproachSq = 0.36; // 0.6 blocks - 2-3 ticks of sprint
|
||||
else if (_horizDist > 2.5)
|
||||
else if (_horizDist > 3.5)
|
||||
minApproachSq = 0.09; // 0.3 blocks - 1-2 ticks of sprint
|
||||
else
|
||||
minApproachSq = 0.0;
|
||||
|
|
@ -104,17 +103,29 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
_leftGround = true;
|
||||
|
||||
bool pastTarget = IsPastTarget(pos);
|
||||
bool biasTowardExitInAir = _segment.ExitTransition == PathTransitionType.LandingRecovery
|
||||
? TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment, distanceThreshold: 1.5)
|
||||
: TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment);
|
||||
if (biasTowardExitInAir)
|
||||
TemplateHelper.FaceExitHeading(physics, _segment);
|
||||
|
||||
bool lookaheadAirBrake = TransitionBrakingPlanner.ShouldReleaseForwardInAir(
|
||||
_segment, _nextSegment, pos, physics, world);
|
||||
bool releaseInAir = ShouldReleaseInAir(pos, physics, world);
|
||||
if (_segment.ExitTransition == PathTransitionType.LandingRecovery && releaseInAir)
|
||||
_airReleaseCommitted = true;
|
||||
if (_airReleaseCommitted)
|
||||
releaseInAir = true;
|
||||
bool earlySoftBrake = _segment.ExitTransition == PathTransitionType.LandingRecovery
|
||||
&& lookaheadAirBrake
|
||||
&& !releaseInAir;
|
||||
|
||||
if (releaseInAir || pastTarget)
|
||||
{
|
||||
input.Forward = false;
|
||||
input.Sprint = false;
|
||||
}
|
||||
else if (earlySoftBrake)
|
||||
{
|
||||
input.Forward = true;
|
||||
input.Sprint = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
input.Forward = true;
|
||||
|
|
@ -134,6 +145,8 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
TemplateHelper.ApplyDecision(input, decision);
|
||||
if (decision.HoldBack)
|
||||
TemplateHelper.FaceSegmentHeading(physics, _segment);
|
||||
else if (TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment))
|
||||
TemplateHelper.FaceExitHeading(physics, _segment);
|
||||
|
||||
double horizToleranceLinear = _horizDist >= 3.5 ? 1.5 : 1.0;
|
||||
double horizToleranceSq = horizToleranceLinear * horizToleranceLinear;
|
||||
|
|
@ -144,7 +157,8 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
|
||||
if (_segment.ExitTransition != PathTransitionType.ContinueStraight
|
||||
&& physics.OnGround
|
||||
&& TemplateHelper.IsSettledOnTargetBlock(pos, ExpectedEnd, physics))
|
||||
&& (TemplateHelper.IsSettledOnTargetBlock(pos, ExpectedEnd, physics)
|
||||
|| IsSettledOnTurnEntryStrip(pos, physics)))
|
||||
{
|
||||
return TemplateState.Complete;
|
||||
}
|
||||
|
|
@ -177,12 +191,16 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
|
||||
private bool ShouldReleaseInAir(Location pos, PlayerPhysics physics, World world)
|
||||
{
|
||||
if (TransitionBrakingPlanner.ShouldReleaseForwardInAir(_segment, _nextSegment, pos, physics))
|
||||
return true;
|
||||
|
||||
if (_segment.ExitTransition == PathTransitionType.ContinueStraight || physics.OnGround)
|
||||
return false;
|
||||
|
||||
bool plannerWantsRelease = TransitionBrakingPlanner.ShouldReleaseForwardInAir(
|
||||
_segment, _nextSegment, pos, physics, world);
|
||||
double remaining = TemplateHelper.RemainingDistanceAlongSegment(pos, _segment);
|
||||
bool centeredOverLandingBlock = remaining <= 1.2;
|
||||
if (plannerWantsRelease && centeredOverLandingBlock)
|
||||
return true;
|
||||
|
||||
Location? landingIfHolding = PredictLandingPosition(physics, world, holdForward: true, holdSprint: true);
|
||||
Location? landingIfReleased = PredictLandingPosition(physics, world, holdForward: false, holdSprint: false);
|
||||
if (landingIfHolding is null || landingIfReleased is null)
|
||||
|
|
@ -191,15 +209,17 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
bool holdingStaysInside = TemplateFootingHelper.IsFootprintInsideTargetBlock(landingIfHolding.Value, ExpectedEnd);
|
||||
bool releasingStaysInside = TemplateFootingHelper.IsFootprintInsideTargetBlock(landingIfReleased.Value, ExpectedEnd);
|
||||
|
||||
if (_segment.ExitTransition == PathTransitionType.LandingRecovery && !holdingStaysInside)
|
||||
if (plannerWantsRelease && releasingStaysInside)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !holdingStaysInside && releasingStaysInside;
|
||||
}
|
||||
|
||||
private Location? PredictLandingPosition(PlayerPhysics physics, World world, bool holdForward, bool holdSprint)
|
||||
{
|
||||
PlayerPhysics sim = ClonePhysics(physics);
|
||||
PlayerPhysics sim = TemplateHelper.ClonePhysicsForPlanning(physics);
|
||||
var input = new MovementInput
|
||||
{
|
||||
Forward = holdForward,
|
||||
|
|
@ -217,36 +237,19 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return null;
|
||||
}
|
||||
|
||||
private static PlayerPhysics ClonePhysics(PlayerPhysics physics)
|
||||
private bool IsSettledOnTurnEntryStrip(Location pos, PlayerPhysics physics)
|
||||
{
|
||||
return new PlayerPhysics
|
||||
{
|
||||
Position = physics.Position,
|
||||
DeltaMovement = physics.DeltaMovement,
|
||||
Yaw = physics.Yaw,
|
||||
Pitch = physics.Pitch,
|
||||
OnGround = physics.OnGround,
|
||||
HorizontalCollision = physics.HorizontalCollision,
|
||||
VerticalCollision = physics.VerticalCollision,
|
||||
VerticalCollisionBelow = physics.VerticalCollisionBelow,
|
||||
FallDistance = physics.FallDistance,
|
||||
StuckSpeedMultiplier = physics.StuckSpeedMultiplier,
|
||||
Xxa = physics.Xxa,
|
||||
Zza = physics.Zza,
|
||||
Yya = physics.Yya,
|
||||
Jumping = physics.Jumping,
|
||||
Sprinting = physics.Sprinting,
|
||||
Sneaking = physics.Sneaking,
|
||||
CreativeFlying = physics.CreativeFlying,
|
||||
InWater = physics.InWater,
|
||||
IsUnderWater = physics.IsUnderWater,
|
||||
InLava = physics.InLava,
|
||||
OnClimbable = physics.OnClimbable,
|
||||
HasSlowFalling = physics.HasSlowFalling,
|
||||
HasLevitation = physics.HasLevitation,
|
||||
LevitationAmplifier = physics.LevitationAmplifier,
|
||||
MovementSpeed = physics.MovementSpeed
|
||||
};
|
||||
if (_segment.ExitTransition != PathTransitionType.LandingRecovery || _nextSegment is null)
|
||||
return false;
|
||||
|
||||
if (_segment.HeadingX == _nextSegment.HeadingX && _segment.HeadingZ == _nextSegment.HeadingZ)
|
||||
return false;
|
||||
|
||||
double horizontalSpeedSq = physics.DeltaMovement.X * physics.DeltaMovement.X
|
||||
+ physics.DeltaMovement.Z * physics.DeltaMovement.Z;
|
||||
return TemplateFootingHelper.IsCenterInsideSupportStrip(pos, ExpectedEnd, _nextSegment.End)
|
||||
&& !TemplateFootingHelper.WillCenterLeaveSupportStripNextTick(pos, physics, ExpectedEnd, _nextSegment.End)
|
||||
&& horizontalSpeedSq <= 0.0016;
|
||||
}
|
||||
|
||||
private static float YawDifference(float current, float target)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,71 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return !IsFootprintInsideTargetBlock(nextPos, target, epsilon);
|
||||
}
|
||||
|
||||
public static bool IsCenterInsideTargetBlock(Location pos, Location target, double epsilon = 1.0E-4)
|
||||
{
|
||||
double blockMinX = Math.Floor(target.X);
|
||||
double blockMaxX = blockMinX + 1.0;
|
||||
double blockMinZ = Math.Floor(target.Z);
|
||||
double blockMaxZ = blockMinZ + 1.0;
|
||||
|
||||
return pos.X >= blockMinX - epsilon
|
||||
&& pos.X <= blockMaxX + epsilon
|
||||
&& pos.Z >= blockMinZ - epsilon
|
||||
&& pos.Z <= blockMaxZ + epsilon;
|
||||
}
|
||||
|
||||
public static bool WillCenterLeaveTargetBlockNextTick(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 !IsCenterInsideTargetBlock(nextPos, target, epsilon);
|
||||
}
|
||||
|
||||
public static bool IsFootprintInsideSupportStrip(Location pos, Location first, Location second, 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;
|
||||
|
||||
GetSupportStripBounds(first, second, out double stripMinX, out double stripMaxX, out double stripMinZ, out double stripMaxZ);
|
||||
|
||||
return minX >= stripMinX - epsilon
|
||||
&& maxX <= stripMaxX + epsilon
|
||||
&& minZ >= stripMinZ - epsilon
|
||||
&& maxZ <= stripMaxZ + epsilon;
|
||||
}
|
||||
|
||||
public static bool WillLeaveSupportStripNextTick(Location pos, PlayerPhysics physics, Location first, Location second, double epsilon = 1.0E-4)
|
||||
{
|
||||
Location nextPos = new(
|
||||
pos.X + physics.DeltaMovement.X,
|
||||
pos.Y,
|
||||
pos.Z + physics.DeltaMovement.Z);
|
||||
return !IsFootprintInsideSupportStrip(nextPos, first, second, epsilon);
|
||||
}
|
||||
|
||||
public static bool IsCenterInsideSupportStrip(Location pos, Location first, Location second, double epsilon = 1.0E-4)
|
||||
{
|
||||
GetSupportStripBounds(first, second, out double stripMinX, out double stripMaxX, out double stripMinZ, out double stripMaxZ);
|
||||
|
||||
return pos.X >= stripMinX - epsilon
|
||||
&& pos.X <= stripMaxX + epsilon
|
||||
&& pos.Z >= stripMinZ - epsilon
|
||||
&& pos.Z <= stripMaxZ + epsilon;
|
||||
}
|
||||
|
||||
public static bool WillCenterLeaveSupportStripNextTick(Location pos, PlayerPhysics physics, Location first, Location second, double epsilon = 1.0E-4)
|
||||
{
|
||||
Location nextPos = new(
|
||||
pos.X + physics.DeltaMovement.X,
|
||||
pos.Y,
|
||||
pos.Z + physics.DeltaMovement.Z);
|
||||
return !IsCenterInsideSupportStrip(nextPos, first, second, epsilon);
|
||||
}
|
||||
|
||||
public static bool WillCrossSupportExitNextTick(Location pos, PlayerPhysics physics, PathSegment segment, double epsilon = 1.0E-4)
|
||||
{
|
||||
double nextX = pos.X + physics.DeltaMovement.X;
|
||||
|
|
@ -56,5 +121,19 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void GetSupportStripBounds(Location first, Location second,
|
||||
out double minX, out double maxX, out double minZ, out double maxZ)
|
||||
{
|
||||
double firstMinX = Math.Floor(first.X);
|
||||
double secondMinX = Math.Floor(second.X);
|
||||
double firstMinZ = Math.Floor(first.Z);
|
||||
double secondMinZ = Math.Floor(second.Z);
|
||||
|
||||
minX = Math.Min(firstMinX, secondMinX);
|
||||
maxX = Math.Max(firstMinX, secondMinX) + 1.0;
|
||||
minZ = Math.Min(firstMinZ, secondMinZ);
|
||||
maxZ = Math.Max(firstMinZ, secondMinZ) + 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,12 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
|
||||
}
|
||||
|
||||
internal static void FaceExitHeading(PlayerPhysics physics, PathSegment segment)
|
||||
{
|
||||
float headingYaw = GetExitHeadingYaw(segment);
|
||||
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
|
||||
}
|
||||
|
||||
internal static void ApplyDecision(MovementInput input, TransitionBrakingDecision decision)
|
||||
{
|
||||
input.Forward = decision.HoldForward;
|
||||
|
|
@ -104,6 +110,45 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return physics.DeltaMovement.X * dirX + physics.DeltaMovement.Z * dirZ;
|
||||
}
|
||||
|
||||
internal static double ProjectHorizontalSpeedAlongHint(PlayerPhysics physics, PathSegment segment)
|
||||
{
|
||||
GetExitHeading(segment, out int headingX, out int headingZ);
|
||||
return ProjectHorizontalSpeedAlongHeading(physics, headingX, headingZ);
|
||||
}
|
||||
|
||||
internal static double ProjectHorizontalSpeedAlongHeading(PlayerPhysics physics, int headingX, int headingZ)
|
||||
{
|
||||
if (headingX == 0 && headingZ == 0)
|
||||
return GetHorizontalSpeed(physics);
|
||||
|
||||
return physics.DeltaMovement.X * headingX + physics.DeltaMovement.Z * headingZ;
|
||||
}
|
||||
|
||||
internal static double GetHorizontalSpeed(PlayerPhysics physics)
|
||||
{
|
||||
return Math.Sqrt(physics.DeltaMovement.X * physics.DeltaMovement.X
|
||||
+ physics.DeltaMovement.Z * physics.DeltaMovement.Z);
|
||||
}
|
||||
|
||||
internal static double RemainingDistanceAlongSegment(Location pos, PathSegment segment)
|
||||
{
|
||||
double dx = segment.End.X - pos.X;
|
||||
double dz = segment.End.Z - pos.Z;
|
||||
return dx * segment.HeadingX + dz * segment.HeadingZ;
|
||||
}
|
||||
|
||||
internal static bool ShouldBiasTowardExitHeading(Location pos, PathSegment segment, double distanceThreshold = 0.35)
|
||||
{
|
||||
GetExitHeading(segment, out int headingX, out int headingZ);
|
||||
if ((headingX == 0 && headingZ == 0)
|
||||
|| (headingX == segment.HeadingX && headingZ == segment.HeadingZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return RemainingDistanceAlongSegment(pos, segment) <= distanceThreshold;
|
||||
}
|
||||
|
||||
internal static bool IsSettledOnTargetBlock(Location pos, Location target, PlayerPhysics physics,
|
||||
double speedThresholdSq = 0.0016)
|
||||
{
|
||||
|
|
@ -120,11 +165,88 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
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;
|
||||
if (horizontalSpeedSq > speedThresholdSq)
|
||||
return false;
|
||||
|
||||
if (TemplateFootingHelper.IsCenterInsideTargetBlock(pos, target)
|
||||
&& !TemplateFootingHelper.WillCenterLeaveTargetBlockNextTick(pos, physics, target))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
double dx = target.X - pos.X;
|
||||
double dz = target.Z - pos.Z;
|
||||
return dx * dx + dz * dz <= horizThresholdSq;
|
||||
}
|
||||
|
||||
internal static double HeadingPenaltyDegrees(float yaw, PathSegment segment)
|
||||
{
|
||||
GetExitHeading(segment, out int headingX, out int headingZ);
|
||||
return HeadingPenaltyDegrees(yaw, headingX, headingZ);
|
||||
}
|
||||
|
||||
internal static double HeadingPenaltyDegrees(float yaw, int headingX, int headingZ)
|
||||
{
|
||||
if (headingX == 0 && headingZ == 0)
|
||||
return 0.0;
|
||||
|
||||
float targetYaw = CalculateYaw(headingX, headingZ);
|
||||
float delta = targetYaw - yaw;
|
||||
while (delta > 180f) delta -= 360f;
|
||||
while (delta < -180f) delta += 360f;
|
||||
return Math.Abs(delta);
|
||||
}
|
||||
|
||||
internal static float GetExitHeadingYaw(PathSegment segment)
|
||||
{
|
||||
GetExitHeading(segment, out int headingX, out int headingZ);
|
||||
return CalculateYaw(headingX, headingZ);
|
||||
}
|
||||
|
||||
internal static void GetExitHeading(PathSegment segment, out int headingX, out int headingZ)
|
||||
{
|
||||
headingX = segment.ExitHints.DesiredHeadingX;
|
||||
headingZ = segment.ExitHints.DesiredHeadingZ;
|
||||
|
||||
if (headingX == 0 && headingZ == 0)
|
||||
{
|
||||
headingX = segment.HeadingX;
|
||||
headingZ = segment.HeadingZ;
|
||||
}
|
||||
}
|
||||
|
||||
internal static PlayerPhysics ClonePhysicsForPlanning(PlayerPhysics physics)
|
||||
{
|
||||
return new PlayerPhysics
|
||||
{
|
||||
Position = physics.Position,
|
||||
DeltaMovement = physics.DeltaMovement,
|
||||
Yaw = physics.Yaw,
|
||||
Pitch = physics.Pitch,
|
||||
OnGround = physics.OnGround,
|
||||
HorizontalCollision = physics.HorizontalCollision,
|
||||
VerticalCollision = physics.VerticalCollision,
|
||||
VerticalCollisionBelow = physics.VerticalCollisionBelow,
|
||||
FallDistance = physics.FallDistance,
|
||||
StuckSpeedMultiplier = physics.StuckSpeedMultiplier,
|
||||
Xxa = physics.Xxa,
|
||||
Zza = physics.Zza,
|
||||
Yya = physics.Yya,
|
||||
Jumping = physics.Jumping,
|
||||
Sprinting = physics.Sprinting,
|
||||
Sneaking = physics.Sneaking,
|
||||
CreativeFlying = physics.CreativeFlying,
|
||||
InWater = physics.InWater,
|
||||
IsUnderWater = physics.IsUnderWater,
|
||||
InLava = physics.InLava,
|
||||
OnClimbable = physics.OnClimbable,
|
||||
HasSlowFalling = physics.HasSlowFalling,
|
||||
HasLevitation = physics.HasLevitation,
|
||||
LevitationAmplifier = physics.LevitationAmplifier,
|
||||
MovementSpeed = physics.MovementSpeed
|
||||
};
|
||||
}
|
||||
|
||||
private static void GetNormalizedSegmentDirection(PathSegment segment, out double dirX, out double dirZ)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double dy = ExpectedEnd.Y - pos.Y;
|
||||
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
float targetYaw = TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment)
|
||||
? TemplateHelper.GetExitHeadingYaw(_segment)
|
||||
: TemplateHelper.CalculateYaw(dx, dz);
|
||||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Execution.Templates;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
|
|
@ -15,39 +16,61 @@ namespace MinecraftClient.Pathing.Execution
|
|||
|
||||
public static TransitionBrakingDecision Plan(PathSegment current, PathSegment? next, Location pos, PlayerPhysics physics, World world)
|
||||
{
|
||||
if (current.ExitTransition is PathTransitionType.ContinueStraight or PathTransitionType.PrepareJump)
|
||||
return TransitionBrakingDecision.CarryMomentum(current.PreserveSprint);
|
||||
|
||||
double remaining = RemainingDistanceAlongSegment(current, pos);
|
||||
double forwardSpeed = Math.Max(0.0, ProjectHorizontalSpeedAlongHeading(physics, current.HeadingX, current.HeadingZ));
|
||||
double coastStopDistance = EstimateGroundStopDistance(physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: false);
|
||||
double hardBrakeDistance = EstimateGroundStopDistance(physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: true);
|
||||
bool landingNeedsTurnBrake = current.ExitTransition == PathTransitionType.LandingRecovery
|
||||
if (physics.OnGround
|
||||
&& current.ExitTransition == PathTransitionType.LandingRecovery
|
||||
&& next is not null
|
||||
&& !HasSameHeading(current, next);
|
||||
|
||||
if (current.ExitTransition == PathTransitionType.FinalStop)
|
||||
&& !HasSameHeading(current, next))
|
||||
{
|
||||
if (remaining < 0.0)
|
||||
double remaining = RemainingDistanceAlongSegment(current, pos);
|
||||
double forwardSpeed = Math.Max(0.0,
|
||||
ProjectHorizontalSpeedAlongHeading(physics, current.HeadingX, current.HeadingZ));
|
||||
double maxExitSpeed = !double.IsPositiveInfinity(current.ExitHints.MaxExitSpeed)
|
||||
? current.ExitHints.MaxExitSpeed
|
||||
: 0.035;
|
||||
double coastStopDistance = EstimateGroundStopDistance(
|
||||
physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: false);
|
||||
double hardBrakeDistance = EstimateGroundStopDistance(
|
||||
physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: true);
|
||||
|
||||
if (TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, current.End)
|
||||
&& forwardSpeed <= maxExitSpeed)
|
||||
{
|
||||
return TransitionBrakingDecision.Coast;
|
||||
}
|
||||
|
||||
if (remaining < 0.0 && forwardSpeed > maxExitSpeed)
|
||||
return TransitionBrakingDecision.Brake;
|
||||
|
||||
if (forwardSpeed > GroundSpeedThreshold && remaining <= hardBrakeDistance + FinalBrakeLead)
|
||||
if (forwardSpeed > GroundSpeedThreshold && remaining <= hardBrakeDistance + TurnBrakeLead)
|
||||
return TransitionBrakingDecision.Brake;
|
||||
|
||||
if (forwardSpeed <= GroundSpeedThreshold && remaining > 0.0)
|
||||
return TransitionBrakingDecision.CarryMomentum(preserveSprint: false);
|
||||
if (remaining <= coastStopDistance + FinalStopLead)
|
||||
return TransitionBrakingDecision.Coast;
|
||||
}
|
||||
|
||||
if ((current.ExitTransition == PathTransitionType.Turn || landingNeedsTurnBrake)
|
||||
&& remaining <= hardBrakeDistance + TurnBrakeLead)
|
||||
TransitionInputProfile profile;
|
||||
if (physics.OnGround)
|
||||
{
|
||||
return TransitionBrakingDecision.Brake;
|
||||
profile = TransitionLookaheadEvaluator.ChooseGroundProfile(current, pos, physics, world);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!current.ExitHints.AllowAirBrake)
|
||||
return TransitionBrakingDecision.CarryMomentum(current.PreserveSprint);
|
||||
|
||||
profile = TransitionLookaheadEvaluator.ChooseAirProfile(current, pos, physics, world);
|
||||
}
|
||||
|
||||
if (remaining <= coastStopDistance + FinalStopLead)
|
||||
return TransitionBrakingDecision.Coast;
|
||||
|
||||
return TransitionBrakingDecision.CarryMomentum(current.PreserveSprint);
|
||||
return profile switch
|
||||
{
|
||||
TransitionInputProfile.Carry => TransitionBrakingDecision.CarryMomentum(current.PreserveSprint),
|
||||
TransitionInputProfile.Coast => TransitionBrakingDecision.Coast,
|
||||
TransitionInputProfile.Brake => TransitionBrakingDecision.Brake,
|
||||
TransitionInputProfile.AirHoldForward => TransitionBrakingDecision.CarryMomentum(current.PreserveSprint),
|
||||
TransitionInputProfile.AirRelease => TransitionBrakingDecision.Coast,
|
||||
TransitionInputProfile.AirBrake => TransitionBrakingDecision.Brake,
|
||||
_ => TransitionBrakingDecision.Coast
|
||||
};
|
||||
}
|
||||
|
||||
public static bool ShouldReleaseForwardInAir(PathSegment current, PathSegment? next, Location pos, PlayerPhysics physics)
|
||||
|
|
@ -61,6 +84,15 @@ namespace MinecraftClient.Pathing.Execution
|
|||
return remaining <= forwardSpeed + AirReleaseLead;
|
||||
}
|
||||
|
||||
public static bool ShouldReleaseForwardInAir(PathSegment current, PathSegment? next, Location pos, PlayerPhysics physics, World world)
|
||||
{
|
||||
if (!current.ExitHints.AllowAirBrake)
|
||||
return false;
|
||||
|
||||
TransitionInputProfile profile = TransitionLookaheadEvaluator.ChooseAirProfile(current, pos, physics, world);
|
||||
return profile is TransitionInputProfile.AirRelease or TransitionInputProfile.AirBrake;
|
||||
}
|
||||
|
||||
public static double EstimateGroundStopDistance(PlayerPhysics physics, World world, int headingX, int headingZ, bool applyBackBrake)
|
||||
{
|
||||
if (!physics.OnGround)
|
||||
|
|
|
|||
12
MinecraftClient/Pathing/Execution/TransitionInputProfile.cs
Normal file
12
MinecraftClient/Pathing/Execution/TransitionInputProfile.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public enum TransitionInputProfile
|
||||
{
|
||||
Carry,
|
||||
Coast,
|
||||
Brake,
|
||||
AirHoldForward,
|
||||
AirRelease,
|
||||
AirBrake
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Execution.Templates;
|
||||
using MinecraftClient.Physics;
|
||||
|
||||
namespace MinecraftClient.Pathing.Execution
|
||||
{
|
||||
public static class TransitionLookaheadEvaluator
|
||||
{
|
||||
public static TransitionInputProfile ChooseGroundProfile(PathSegment segment, Location pos, PlayerPhysics physics, World world)
|
||||
{
|
||||
double remaining = TemplateHelper.RemainingDistanceAlongSegment(pos, segment);
|
||||
double forwardSpeed = Math.Max(0.0,
|
||||
TemplateHelper.ProjectHorizontalSpeedAlongHeading(physics, segment.HeadingX, segment.HeadingZ));
|
||||
|
||||
bool requiresJumpEntry = segment.ExitHints.RequireJumpReady
|
||||
|| segment.ExitTransition == PathTransitionType.PrepareJump;
|
||||
|
||||
if (segment.ExitTransition == PathTransitionType.ContinueStraight && !requiresJumpEntry)
|
||||
return TransitionInputProfile.Carry;
|
||||
|
||||
if (requiresJumpEntry)
|
||||
return TransitionInputProfile.Carry;
|
||||
|
||||
bool requiresSlowEntry = segment.ExitHints.RequireStableFooting
|
||||
|| segment.ExitTransition is PathTransitionType.FinalStop or PathTransitionType.Turn
|
||||
|| (segment.ExitTransition == PathTransitionType.LandingRecovery
|
||||
&& (segment.ExitHints.AllowAirBrake || IsFiniteSpeedCap(segment)));
|
||||
|
||||
if (!requiresSlowEntry)
|
||||
return TransitionInputProfile.Carry;
|
||||
|
||||
double maxExitSpeed = GetTargetMaxExitSpeed(segment);
|
||||
double hardBrakeDistance = TransitionBrakingPlanner.EstimateGroundStopDistance(
|
||||
physics, world, segment.HeadingX, segment.HeadingZ, applyBackBrake: true);
|
||||
double coastStopDistance = TransitionBrakingPlanner.EstimateGroundStopDistance(
|
||||
physics, world, segment.HeadingX, segment.HeadingZ, applyBackBrake: false);
|
||||
|
||||
if (remaining < 0.0)
|
||||
return TransitionInputProfile.Brake;
|
||||
|
||||
if (forwardSpeed > maxExitSpeed && remaining <= hardBrakeDistance + 0.10)
|
||||
return TransitionInputProfile.Brake;
|
||||
|
||||
if (forwardSpeed <= maxExitSpeed && remaining > 0.0)
|
||||
return TransitionInputProfile.Carry;
|
||||
|
||||
if (remaining <= coastStopDistance + 0.06)
|
||||
return TransitionInputProfile.Coast;
|
||||
|
||||
return TransitionInputProfile.Carry;
|
||||
}
|
||||
|
||||
public static TransitionInputProfile ChooseAirProfile(PathSegment segment, Location pos, PlayerPhysics physics, World world)
|
||||
{
|
||||
if (!segment.ExitHints.AllowAirBrake)
|
||||
return TransitionInputProfile.AirHoldForward;
|
||||
|
||||
TransitionInputProfile[] candidates =
|
||||
[
|
||||
TransitionInputProfile.AirHoldForward,
|
||||
TransitionInputProfile.AirRelease,
|
||||
TransitionInputProfile.AirBrake
|
||||
];
|
||||
|
||||
return ChooseBest(segment, pos, physics, world, candidates);
|
||||
}
|
||||
|
||||
private static TransitionInputProfile ChooseBest(PathSegment segment, Location pos, PlayerPhysics physics, World world,
|
||||
TransitionInputProfile[] candidates)
|
||||
{
|
||||
TransitionInputProfile best = candidates[0];
|
||||
double bestScore = double.PositiveInfinity;
|
||||
|
||||
foreach (TransitionInputProfile candidate in candidates)
|
||||
{
|
||||
double score = Score(segment, pos, physics, world, candidate);
|
||||
if (score < bestScore)
|
||||
{
|
||||
best = candidate;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static double Score(PathSegment segment, Location pos, PlayerPhysics physics, World world, TransitionInputProfile candidate)
|
||||
{
|
||||
PlayerPhysics sim = TemplateHelper.ClonePhysicsForPlanning(physics);
|
||||
sim.Position = new Vec3d(pos.X, pos.Y, pos.Z);
|
||||
|
||||
var input = new MovementInput();
|
||||
Location simPos = pos;
|
||||
|
||||
for (int tick = 0; tick < segment.ExitHints.HorizonTicks; tick++)
|
||||
{
|
||||
if (TemplateHelper.ShouldBiasTowardExitHeading(simPos, segment))
|
||||
TemplateHelper.FaceExitHeading(sim, segment);
|
||||
|
||||
input.Reset();
|
||||
ApplyCandidateInput(input, candidate, segment);
|
||||
sim.ApplyInput(input);
|
||||
sim.Tick(world);
|
||||
simPos = new Location(sim.Position.X, sim.Position.Y, sim.Position.Z);
|
||||
}
|
||||
|
||||
double score = 0.0;
|
||||
double exitSpeed = TemplateHelper.ProjectHorizontalSpeedAlongHint(sim, segment);
|
||||
double horizontalSpeed = TemplateHelper.GetHorizontalSpeed(sim);
|
||||
|
||||
if (segment.ExitHints.RequireGrounded && !sim.OnGround)
|
||||
score += 1000.0;
|
||||
|
||||
if (segment.ExitHints.RequireStableFooting
|
||||
&& !TemplateHelper.IsSettledOnTargetBlock(simPos, segment.End, sim))
|
||||
{
|
||||
score += 1000.0;
|
||||
}
|
||||
|
||||
if (segment.ExitHints.RequireStableFooting && !sim.OnGround)
|
||||
{
|
||||
double remaining = TemplateHelper.RemainingDistanceAlongSegment(simPos, segment);
|
||||
if (remaining > 0.0)
|
||||
score += 2000.0 + remaining * 500.0;
|
||||
|
||||
if (simPos.Y < segment.End.Y)
|
||||
score += 2000.0 + (segment.End.Y - simPos.Y) * 500.0;
|
||||
}
|
||||
|
||||
if (exitSpeed < segment.ExitHints.MinExitSpeed)
|
||||
score += (segment.ExitHints.MinExitSpeed - exitSpeed) * 200.0;
|
||||
|
||||
if (exitSpeed > segment.ExitHints.MaxExitSpeed)
|
||||
score += (exitSpeed - segment.ExitHints.MaxExitSpeed) * 200.0;
|
||||
|
||||
score += TemplateHelper.HeadingPenaltyDegrees(sim.Yaw, segment);
|
||||
|
||||
if (segment.ExitHints.RequireStableFooting)
|
||||
{
|
||||
double dx = segment.End.X - simPos.X;
|
||||
double dz = segment.End.Z - simPos.Z;
|
||||
score += (dx * dx + dz * dz) * 20.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
score += Math.Abs(TemplateHelper.RemainingDistanceAlongSegment(simPos, segment)) * 10.0;
|
||||
}
|
||||
|
||||
if (segment.ExitHints.RequireJumpReady && horizontalSpeed < segment.ExitHints.MinExitSpeed)
|
||||
score += 250.0;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static void ApplyCandidateInput(MovementInput input, TransitionInputProfile candidate, PathSegment segment)
|
||||
{
|
||||
switch (candidate)
|
||||
{
|
||||
case TransitionInputProfile.Carry:
|
||||
case TransitionInputProfile.AirHoldForward:
|
||||
input.Forward = true;
|
||||
input.Sprint = segment.PreserveSprint || segment.ExitHints.RequireJumpReady;
|
||||
break;
|
||||
|
||||
case TransitionInputProfile.Brake:
|
||||
case TransitionInputProfile.AirBrake:
|
||||
input.Back = true;
|
||||
break;
|
||||
|
||||
case TransitionInputProfile.Coast:
|
||||
case TransitionInputProfile.AirRelease:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFiniteSpeedCap(PathSegment segment)
|
||||
{
|
||||
return !double.IsPositiveInfinity(segment.ExitHints.MaxExitSpeed);
|
||||
}
|
||||
|
||||
private static double GetTargetMaxExitSpeed(PathSegment segment)
|
||||
{
|
||||
if (IsFiniteSpeedCap(segment))
|
||||
return segment.ExitHints.MaxExitSpeed;
|
||||
|
||||
return segment.ExitTransition switch
|
||||
{
|
||||
PathTransitionType.FinalStop => 0.03,
|
||||
PathTransitionType.Turn or PathTransitionType.LandingRecovery => 0.035,
|
||||
_ => double.PositiveInfinity
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue