Minecraft-Console-Client/MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs
2026-04-12 18:43:33 +00:00

260 lines
11 KiB
C#

using System;
using MinecraftClient.Mapping;
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 _airReleaseCommitted;
private bool _leftGround;
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;
float targetYaw = 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);
switch (_phase)
{
case Phase.Approach:
input.Forward = true;
input.Sprint = true;
if (physics.OnGround)
{
double fromStartSq = TemplateHelper.HorizontalDistanceSq(pos, ExpectedStart);
float yawDelta = YawDifference(physics.Yaw, targetYaw);
// 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 minApproachSq;
if (_horizDist >= 5.0)
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)
minApproachSq = 0.09; // 0.3 blocks - 1-2 ticks of sprint
else
minApproachSq = 0.0;
bool yawAligned = yawDelta < YawToleranceDeg;
bool posReady = fromStartSq >= minApproachSq;
if (yawAligned && posReady)
{
input.Jump = true;
_phase = Phase.Airborne;
}
}
if (_tickCount > 40)
return TemplateState.Failed;
break;
case Phase.Airborne:
{
if (!physics.OnGround)
_leftGround = true;
bool pastTarget = IsPastTarget(pos);
bool releaseInAir = ShouldReleaseInAir(pos, physics, world);
if (_segment.ExitTransition == PathTransitionType.LandingRecovery && releaseInAir)
_airReleaseCommitted = true;
if (_airReleaseCommitted)
releaseInAir = true;
if (releaseInAir || pastTarget)
{
input.Forward = false;
input.Sprint = false;
}
else
{
input.Forward = true;
input.Sprint = true;
}
if (_leftGround && physics.OnGround)
{
_phase = Phase.Landing;
goto case Phase.Landing;
}
break;
}
case Phase.Landing:
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldBack)
TemplateHelper.FaceSegmentHeading(physics, _segment);
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))
{
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 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;
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 (_segment.ExitTransition == PathTransitionType.LandingRecovery && !holdingStaysInside)
return true;
return !holdingStaysInside && releasingStaysInside;
}
private Location? PredictLandingPosition(PlayerPhysics physics, World world, bool holdForward, bool holdSprint)
{
PlayerPhysics sim = ClonePhysics(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 static PlayerPhysics ClonePhysics(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 float YawDifference(float current, float target)
{
float delta = target - current;
while (delta > 180f) delta -= 360f;
while (delta < -180f) delta += 360f;
return Math.Abs(delta);
}
}
}