mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
375 lines
18 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|