Body step 2.6: landing dip, jump, stick crouch, no flip looking down, smooth view

- Look-down flip: body facing from BodyMath.HeadFacing (visor + crown), robust past vertical;
  stick direction uses it too; crouch lean pitch measured against the facing.
- Landing: falling legs touch down only within their reach, so leg strength stops the body
  below its stance (dip scales with impact, big falls fold the legs). Rise eased so gravity
  can stop it at the stance; legs untuck speed-limited. Fixed the vertical play-space
  correction leaking into ground control's feed-forward (a bounce).
- Jump on right A: countermovement dip + leg push over the stroke (crouch = higher).
- Stick crouch on right stick Y (RagdollDriver.StickCrouch: virtual floor offset).
- Menu summon on left X only.
- XR origin drawn with the bodies' physics interpolation (camera/body desync).
- RagdollSpike: landing/fall/jump/look-down/stick-crouch phases, `-- only=` filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 17:25:15 +03:00
1 parent 84df772f38
commit e821728fba
7 files changed
+1379 -1043

No files matched your search

+60 -4
View File
@@ -4,7 +4,8 @@ using Godot;
// Boneworks-style stick locomotion and the play-space reconciliation. Runs everything in one
// physics tick, in this order:
// 1. discrete origin jumps made by other code (spawn, teleport markers) carry the body along;
// 2. stick: left = move (head-relative), right X = turn (snap by default, SmoothTurn for smooth);
// 2. stick: left = move (head-relative), right X = turn (snap by default, SmoothTurn for smooth),
// right Y = crouch (down lowers, up raises — RagdollDriver.StickCrouch); right A = jump;
// 3. the ragdoll's OriginCorrection (push-back at walls, floor follows the ground, falls);
// 4. True Body solve from the corrected camera/controllers, then the ragdoll's muscles.
// Waits for the calibrated BodyProfile like everything else; rebuilds on recalibration.
@@ -18,6 +19,8 @@ public partial class PlayerRig : Node
public const float SmoothTurnSpeed = 2.5f; // rad/s
public const float SnapTurnThreshold = 0.7f;
public const float SnapTurnRearm = 0.4f;
public const float StickCrouchSpeed = 1.2f; // m/s at full stick
public const float StickCrouchHeadroom = 0.5f; // the stick can lower the eyes to this height (a crawl)
public const float JumpDistance = 0.5f; // an origin move this big in one tick = teleport, not motion
public const float JumpAngle = 0.1f; // rad
private const float GhostLagDistance = 0.1f;
@@ -33,9 +36,17 @@ public partial class PlayerRig : Node
private bool _haveLastOrigin;
private Vector3 _moveVelocity;
private bool _snapArmed = true;
private bool _jumpPrev;
private readonly MeshInstance3D[] _ghosts = new MeshInstance3D[2];
private bool _rebuild = true;
private RagdollDriver _driver;
// Render interpolation of the play space (smooth-camera bible, VR edition): physics moves the
// origin every tick, but the body is DRAWN interpolated between the last two ticks. A camera
// stepping with the raw origin while the body glides a tick behind looks like the body is in
// two places at once when you move and look at it. So physics owns _physOrigin and every
// frame the visible origin is the same interpolation of it the bodies get.
private Transform3D _physOrigin, _prevPhysOrigin, _shownOrigin;
private bool _interpolateOrigin;
public PlayerRagdoll Ragdoll => _ragdoll;
public static bool Spectator { get; private set; }
@@ -70,6 +81,8 @@ public partial class PlayerRig : Node
public override void _Ready()
{
_instance = this;
ProcessPriority = -100; // place the interpolated origin before anything reads it this frame
ProcessPhysicsPriority = -100; // restore the physics origin before anything uses it this tick
_origin = GetNode<XROrigin3D>("../XROrigin3D");
_camera = GetNode<XRCamera3D>("../XROrigin3D/XRCamera3D");
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
@@ -114,8 +127,10 @@ public partial class PlayerRig : Node
if (profile == null || Spectator)
{
_haveLastOrigin = false;
_interpolateOrigin = false;
return;
}
RestorePhysicsOrigin();
// 1. Someone else moved the play space discretely: bring the body along.
Transform3D origin = _origin.GlobalTransform;
@@ -134,10 +149,34 @@ public partial class PlayerRig : Node
_origin.GlobalTransform = o;
_lastOrigin = _origin.GlobalTransform;
_haveLastOrigin = true;
_prevPhysOrigin = _interpolateOrigin && !IsJump(_physOrigin, _lastOrigin) ? _physOrigin : _lastOrigin;
_physOrigin = _shownOrigin = _lastOrigin;
_interpolateOrigin = true;
}
// The origin on screen is interpolated; physics must run on the real one. Anyone else who moved
// the origin since (teleport marker, menu) is respected as the new real one.
private void RestorePhysicsOrigin()
{
if (!_interpolateOrigin)
return;
Transform3D current = _origin.GlobalTransform;
if (current.IsEqualApprox(_shownOrigin))
_origin.GlobalTransform = _physOrigin;
else
_physOrigin = _prevPhysOrigin = current;
}
public override void _Process(double delta)
{
if (_interpolateOrigin && !Spectator)
{
Transform3D current = _origin.GlobalTransform;
if (!current.IsEqualApprox(_shownOrigin))
_physOrigin = _prevPhysOrigin = current; // moved by someone else: show it as is
_shownOrigin = _prevPhysOrigin.InterpolateWith(_physOrigin, (float)Engine.GetPhysicsInterpolationFraction());
_origin.GlobalTransform = _shownOrigin;
}
for (int i = 0; i < 2; i++)
{
XRController3D c = i == 0 ? _left : _right;
@@ -178,9 +217,9 @@ public partial class PlayerRig : Node
Vector3 desired = Vector3.Zero;
if (move.Length() > StickDeadzone)
{
Vector3 fwd = -_camera.GlobalTransform.Basis.Z;
fwd.Y = 0f;
fwd = fwd.LengthSquared() > 1e-6f ? fwd.Normalized() : Vector3.Forward;
Vector3 fwd = BodyMath.HeadFacing(_camera.GlobalTransform.Basis.Orthonormalized());
if (fwd == Vector3.Zero)
fwd = -Solver.YawBasis.Z;
Vector3 rightDir = fwd.Cross(Vector3.Up);
float mag = Mathf.Clamp((move.Length() - StickDeadzone) / (1f - StickDeadzone), 0f, 1f);
desired = (fwd * move.Y + rightDir * move.X).Normalized() * (mag * MoveSpeed);
@@ -192,6 +231,23 @@ public partial class PlayerRig : Node
if (_moveVelocity != Vector3.Zero)
_origin.GlobalPosition += _moveVelocity * dt;
// Jump: right A, on the press.
bool jump = _right.IsButtonPressed("ax_button");
if (jump && !_jumpPrev)
_ragdoll.Jump();
_jumpPrev = jump;
// Crouch: right stick held down lowers the body, up raises it back (the stick's dominant
// axis decides between crouch and turn).
bool vertical = Mathf.Abs(turn.Y) > Mathf.Abs(turn.X);
if (vertical && Mathf.Abs(turn.Y) > StickDeadzone)
{
float max = Mathf.Max(0f, BodyProfile.Current.EyeHeight - StickCrouchHeadroom);
_driver.StickCrouch = Mathf.Clamp(_driver.StickCrouch - turn.Y * StickCrouchSpeed * dt, 0f, max);
}
if (vertical)
turn.X = 0f;
// Turn about the head.
float angle = 0f;
if (SmoothTurn)