Minecraft-Console-Client/MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs
2026-04-18 06:14:45 +00:00

375 lines
18 KiB
C#

using System;
using MinecraftClient.Mapping;
using MinecraftClient.Pathing.Core;
using MinecraftClient.Physics;
namespace MinecraftClient.Pathing.Execution.Templates
{
/// <summary>
/// Jump across a gap. Uses a phase-based state machine:
/// Approach -> Jump -> Airborne -> Landing.
///
/// All parkour jumps use sprint-jumping (vanilla optimal horizontal distance).
/// The key to landing on small platforms is releasing forward/sprint input mid-air
/// once the player is close to or past the target, letting drag decelerate them
/// onto the block.
///
/// During Approach, the template waits for the yaw to be within 5 degrees of
/// the target direction before jumping. For medium/long jumps, it also builds
/// momentum by sprinting toward the block edge.
/// </summary>
public sealed class SprintJumpTemplate : IActionTemplate
{
private enum Phase { Approach, Airborne, Landing }
public Location ExpectedStart { get; }
public Location ExpectedEnd { get; }
private readonly PathSegment _segment;
private readonly PathSegment? _nextSegment;
private readonly double _horizDist;
private int _tickCount;
private Phase _phase = Phase.Approach;
private bool _leftGround;
private bool _carriedGroundEntry;
private bool _releaseForwardLatched;
private const float YawToleranceDeg = 5f;
public SprintJumpTemplate(PathSegment segment, PathSegment? nextSegment)
{
_segment = segment;
_nextSegment = nextSegment;
ExpectedStart = segment.Start;
ExpectedEnd = segment.End;
double dx = segment.End.X - segment.Start.X;
double dz = segment.End.Z - segment.Start.Z;
_horizDist = Math.Sqrt(dx * dx + dz * dz);
}
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
{
_tickCount++;
double dx = ExpectedEnd.X - pos.X;
double dz = ExpectedEnd.Z - pos.Z;
double dy = ExpectedEnd.Y - pos.Y;
double horizDistSq = dx * dx + dz * dz;
bool prepareJumpTouchdown = _phase == Phase.Airborne && _leftGround && physics.OnGround;
bool groundedPrepareJumpHandoff = (_phase == Phase.Landing || prepareJumpTouchdown)
&& physics.OnGround
&& _segment.ExitTransition == PathTransitionType.PrepareJump
&& _segment.ExitHints.RequireJumpReady
&& (TemplateFootingHelper.IsCenterInsideTargetBlock(pos, _segment.End)
|| TemplateHelper.HasReachedSegmentEndPlane(pos, _segment));
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
physics.Yaw = groundedPrepareJumpHandoff
? TemplateHelper.SmoothYaw(physics.Yaw, TemplateHelper.GetExitHeadingYaw(_segment))
: TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
switch (_phase)
{
case Phase.Approach:
if (physics.OnGround)
{
if (_tickCount == 1 && TemplateHelper.GetHorizontalSpeed(physics) > 0.02)
_carriedGroundEntry = true;
double approachProgress = ((pos.X - ExpectedStart.X) * _segment.HeadingX)
+ ((pos.Z - ExpectedStart.Z) * _segment.HeadingZ);
float yawDelta = YawDifference(physics.Yaw, targetYaw);
bool turnInPlace = yawDelta > 35f;
input.Forward = !turnInPlace;
input.Sprint = !turnInPlace;
bool carriedShortFinalStopJump = _carriedGroundEntry
&& _segment.ExitTransition == PathTransitionType.FinalStop
&& _horizDist <= 2.5;
bool carriedDescendingFinalStopJump = _carriedGroundEntry
&& _segment.ExitTransition == PathTransitionType.FinalStop
&& ExpectedEnd.Y < ExpectedStart.Y
&& _horizDist <= 3.5;
bool carriedDescendingParkourJump = _carriedGroundEntry
&& _segment.ExitTransition == PathTransitionType.PrepareJump
&& ExpectedEnd.Y < ExpectedStart.Y
&& _horizDist <= 3.5;
if (carriedShortFinalStopJump || carriedDescendingFinalStopJump || carriedDescendingParkourJump)
input.Sprint = false;
// Build momentum before jumping. Sprint speed is ~5.6 m/s
// (0.28 blocks/tick). More run-up = more airtime distance.
// Standing sprint jump (0t): ~3.6 blocks horizontal
// 2-tick sprint (0.56m): ~4.3 blocks horizontal
// 4-tick sprint (1.1m): ~5.0 blocks horizontal
double minApproachDistance;
bool carriedLongDescendingJump = _carriedGroundEntry
&& ExpectedEnd.Y < ExpectedStart.Y
&& _horizDist >= 5.0;
if (carriedLongDescendingJump)
minApproachDistance = 0.8; // use nearly the full landing block to preserve long-jump carry
else if (_horizDist >= 5.0)
minApproachDistance = 0.8; // 3+ ticks of sprint
else if (_horizDist >= 4.0)
minApproachDistance = 0.6; // 2-3 ticks of sprint
else if (_horizDist > 3.5)
minApproachDistance = 0.3; // 1-2 ticks of sprint
else
minApproachDistance = 0.0;
bool yawAligned = yawDelta < YawToleranceDeg;
bool posReady = approachProgress >= minApproachDistance;
if (yawAligned && posReady)
{
input.Jump = true;
_phase = Phase.Airborne;
}
}
else
{
float yawDelta = YawDifference(physics.Yaw, targetYaw);
bool turnInPlace = yawDelta > 35f;
input.Forward = !turnInPlace;
input.Sprint = !turnInPlace;
}
if (_tickCount > 40)
return TemplateState.Failed;
break;
case Phase.Airborne:
{
if (!physics.OnGround)
_leftGround = true;
bool pastTarget = IsPastTarget(pos);
bool parkourOnOrPastTarget = _segment.MoveType == MoveType.Parkour
&& (TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd)
|| TemplateHelper.HasReachedSegmentEndPlane(pos, _segment)
|| pastTarget);
bool biasTowardExitInAir = _segment.ExitTransition == PathTransitionType.LandingRecovery
? TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment, distanceThreshold: 1.5)
: TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment);
if (parkourOnOrPastTarget || biasTowardExitInAir)
TemplateHelper.FaceExitHeading(physics, _segment);
bool lookaheadAirBrake = TransitionBrakingPlanner.ShouldReleaseForwardInAir(
_segment, _nextSegment, pos, physics, world);
bool releaseInAir = ShouldReleaseInAir(pos, physics, world);
_releaseForwardLatched |= releaseInAir;
bool earlySoftBrake = _segment.ExitTransition == PathTransitionType.LandingRecovery
&& lookaheadAirBrake
&& !releaseInAir;
if (_releaseForwardLatched || pastTarget)
{
input.Forward = false;
input.Sprint = false;
}
else if (earlySoftBrake)
{
input.Forward = true;
input.Sprint = false;
}
else
{
input.Forward = true;
input.Sprint = !(_segment.ExitTransition == PathTransitionType.FinalStop
&& ExpectedEnd.Y < ExpectedStart.Y);
}
if (_leftGround && physics.OnGround)
{
_phase = Phase.Landing;
goto case Phase.Landing;
}
break;
}
case Phase.Landing:
bool descendingPrepareJump = _segment.ExitTransition == PathTransitionType.PrepareJump
&& ExpectedEnd.Y < ExpectedStart.Y;
bool descendingPrepareJumpOnSupport = descendingPrepareJump
&& physics.OnGround
&& TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, _segment.End);
bool descendingPrepareJumpPastSupport = descendingPrepareJump
&& physics.OnGround
&& TemplateHelper.HasReachedSegmentEndPlane(pos, _segment)
&& !descendingPrepareJumpOnSupport;
if (descendingPrepareJumpPastSupport)
{
input.Forward = false;
input.Sprint = false;
input.Back = true;
TemplateHelper.FaceExitHeading(physics, _segment);
}
else
{
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldBack)
TemplateHelper.FaceSegmentHeading(physics, _segment);
else if (TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment))
TemplateHelper.FaceExitHeading(physics, _segment);
}
if (_segment.ExitTransition == PathTransitionType.PrepareJump
&& physics.OnGround
&& (!descendingPrepareJump || descendingPrepareJumpOnSupport)
&& GroundedSegmentController.ShouldComplete(_segment, pos, physics))
{
return TemplateState.Complete;
}
double horizToleranceLinear = _horizDist >= 3.5 ? 1.5 : 1.0;
double horizToleranceSq = horizToleranceLinear * horizToleranceLinear;
double vertTolerance = Math.Abs(ExpectedEnd.Y - ExpectedStart.Y) > 0.5 ? 1.5 : 1.0;
if (_segment.ExitTransition == PathTransitionType.ContinueStraight
&& horizDistSq < horizToleranceSq && Math.Abs(dy) < vertTolerance)
return TemplateState.Complete;
if (_segment.ExitTransition != PathTransitionType.ContinueStraight
&& physics.OnGround
&& (TemplateHelper.IsSettledOnTargetBlock(pos, ExpectedEnd, physics)
|| IsSettledOnTurnEntryStrip(pos, physics)))
{
return TemplateState.Complete;
}
break;
}
if (pos.Y < ExpectedEnd.Y - 4.0)
return TemplateState.Failed;
if (_tickCount > 60)
return TemplateState.Failed;
return TemplateState.InProgress;
}
private bool IsPastTarget(Location pos)
{
double dirX = ExpectedEnd.X - ExpectedStart.X;
double dirZ = ExpectedEnd.Z - ExpectedStart.Z;
double len = Math.Sqrt(dirX * dirX + dirZ * dirZ);
if (len < 0.001) return false;
dirX /= len;
dirZ /= len;
double relX = pos.X - ExpectedEnd.X;
double relZ = pos.Z - ExpectedEnd.Z;
double dot = relX * dirX + relZ * dirZ;
return dot > 0.0;
}
private double GetLateralOffsetFromSegmentLine(Location pos)
{
double dirX = ExpectedEnd.X - ExpectedStart.X;
double dirZ = ExpectedEnd.Z - ExpectedStart.Z;
double len = Math.Sqrt(dirX * dirX + dirZ * dirZ);
if (len < 0.001)
return 0.0;
dirX /= len;
dirZ /= len;
double relX = pos.X - ExpectedStart.X;
double relZ = pos.Z - ExpectedStart.Z;
return Math.Abs((-dirZ * relX) + (dirX * relZ));
}
private bool ShouldReleaseInAir(Location pos, PlayerPhysics physics, World world)
{
if (_segment.ExitTransition == PathTransitionType.ContinueStraight || physics.OnGround)
return false;
bool heuristicFinalStopRelease = _segment.ExitTransition == PathTransitionType.FinalStop
&& ShouldReleaseByRemainingLead(pos, physics);
if (heuristicFinalStopRelease)
return true;
bool heuristicDescendingPrepareJumpRelease = _segment.ExitTransition == PathTransitionType.PrepareJump
&& ExpectedEnd.Y < ExpectedStart.Y
&& ShouldReleaseByRemainingLead(pos, physics);
if (heuristicDescendingPrepareJumpRelease)
return true;
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)
return false;
bool holdingStaysInside = TemplateFootingHelper.IsFootprintInsideTargetBlock(landingIfHolding.Value, ExpectedEnd);
bool releasingStaysInside = TemplateFootingHelper.IsFootprintInsideTargetBlock(landingIfReleased.Value, ExpectedEnd);
if (plannerWantsRelease && releasingStaysInside)
{
return true;
}
return !holdingStaysInside && releasingStaysInside;
}
private bool ShouldReleaseByRemainingLead(Location pos, PlayerPhysics physics)
{
double remaining = TemplateHelper.RemainingDistanceAlongSegment(pos, _segment);
double forwardSpeed = Math.Max(0.0,
TemplateHelper.ProjectHorizontalSpeedAlongHeading(physics, _segment.HeadingX, _segment.HeadingZ));
double dropHeight = Math.Max(0.0, ExpectedStart.Y - ExpectedEnd.Y);
double releaseLead = 0.14 + (Math.Max(0.0, dropHeight - 1.0) * 0.20);
return remaining <= forwardSpeed + releaseLead;
}
private Location? PredictLandingPosition(PlayerPhysics physics, World world, bool holdForward, bool holdSprint)
{
PlayerPhysics sim = TemplateHelper.ClonePhysicsForPlanning(physics);
var input = new MovementInput
{
Forward = holdForward,
Sprint = holdSprint
};
for (int tick = 0; tick < 16; tick++)
{
sim.ApplyInput(input);
sim.Tick(world);
if (sim.OnGround)
return new Location(sim.Position.X, sim.Position.Y, sim.Position.Z);
}
return null;
}
private bool IsSettledOnTurnEntryStrip(Location pos, PlayerPhysics physics)
{
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)
{
float delta = target - current;
while (delta > 180f) delta -= 360f;
while (delta < -180f) delta += 360f;
return Math.Abs(delta);
}
}
}