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

+164 -14
View File
@@ -38,6 +38,7 @@ public partial class RagdollSpike : Node3D
private Transform3D Right => _origin * Tracked(2, _rightLocal);
private bool _realInput = true, _noiseOn = true, _holdOn = true;
private string[] _only; // `-- only=jump,fall`: run just the phases whose names contain one of these
private const float TrackingHz = 90f;
private readonly Transform3D[] _held = new Transform3D[3];
private readonly long[] _heldFrame = { -1, -1, -1 };
@@ -71,6 +72,8 @@ public partial class RagdollSpike : Node3D
_noiseOn = false;
if (arg == "nohold")
_holdOn = false;
if (arg.StartsWith("only="))
_only = arg[5..].Split(',');
}
GD.Print($"RAGDOLL: input mode {(_realInput ? "REAL (90 Hz tracking, jitter)" : "ideal")} at {Engine.PhysicsTicksPerSecond} Hz physics");
StaticBody3D floor = MakeStatic(new Vector3(40f, 1f, 40f), new Vector3(0f, -0.5f, 0f), 29);
@@ -106,11 +109,18 @@ public partial class RagdollSpike : Node3D
_phases.Add(("holds poses (stand, crouch, deep crouch, lean, reach, kneel) without view/body drift", Rest, TickPoseHold, 18f, FinishPoseHold));
_phases.Add(("hanging from a bar keeps the view on the body", () => SetupBarHang(), TickHang, 4f, FinishHang));
_phases.Add(("climbs a wall hand over hand (grab, pull, regrab higher)", SetupClimbWall, TickClimbWall, 8f, FinishClimbWall));
_phases.Add(("walks off a 1 m ledge and lands without sinking away from the view", SetupLedgeTop, TickDrop, 4f, FinishDrop));
_phases.Add(("walks off a 1 m ledge and lands: a dip the view follows, no bounce", () => SetupLedgeTop(1f), TickDrop, 4f, () => FinishDrop(1f)));
_phases.Add(("big fall (5 m): legs fold fully, then it recovers", () => SetupLedgeTop(5f), TickDrop, 5f, () => FinishDrop(5f)));
_phases.Add(("standing jump", Rest, t => TickJump(0f), 4f, () => FinishJump(crouched: false)));
_phases.Add(("jump from a crouch goes higher", Rest, t => TickJump(0.45f), 4.5f, () => FinishJump(crouched: true)));
_phases.Add(("looks down past vertical while crouched (body doesn't flip around)", Rest, TickLookDown, 5f, FinishLookDown));
_phases.Add(("stick crouch lowers the body and the view, and stands back up", Rest, TickStickCrouch, 6f, FinishStickCrouch));
_phases.Add(("standing on a seesaw tips it (body weight reaches movable ground)", SetupSeesaw, _ => { }, 3f, FinishSeesaw));
_phases.Add(("playground: swing board hangs, seesaw on its pivot, big plank grabbable", SetupPlayground, _ => { }, 2f, FinishPlayground));
_phases.Add(("walks up an 18° ramp", () => SetupRamp(18f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 4f, () => FinishRamp(true)));
_phases.Add(("can't walk up a 40° ramp", () => SetupRamp(40f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 4f, () => FinishRamp(false)));
if (_only != null)
_phases.RemoveAll(ph => ph.Name != "hinge sign" && !Array.Exists(_only, o => ph.Name.Contains(o)));
Next();
}
@@ -153,6 +163,8 @@ public partial class RagdollSpike : Node3D
_finite = true;
_measureFrom = 1f;
_stickVelocity = Vector3.Zero;
if (_driver != null)
_driver.StickCrouch = 0f;
_phases[_phase].Setup();
_snapsAtStart = _ragdoll.Snaps;
}
@@ -662,38 +674,176 @@ public partial class RagdollSpike : Node3D
}
// Start standing on a 1 m platform whose edge is 0.5 m ahead.
private void SetupLedgeTop()
// Start standing on a platform whose edge is 0.5 m ahead.
private void SetupLedgeTop(float height)
{
Rest();
_origin = new Transform3D(Basis.Identity, new Vector3(0f, 1f, 0f));
_stage.AddChild(MakeStatic(new Vector3(3f, 1f, 3f), new Vector3(0f, 0.5f, 1f), 29));
_origin = new Transform3D(Basis.Identity, new Vector3(0f, height, 0f));
_stage.AddChild(MakeStatic(new Vector3(3f, height, 3f), new Vector3(0f, height * 0.5f, 1f), 29));
_dropEyeErr = _dropEyeErrLate = _dropRebound = 0f;
_dropMinPelvis = float.MaxValue;
_dropStand = 0f;
}
private float _dropEyeErr, _dropMinPelvis = float.MaxValue;
private float _dropEyeErr, _dropEyeErrLate, _dropMinPelvis = float.MaxValue, _dropStand, _dropRebound;
private void TickDrop(float dt)
{
_stickVelocity = _t > 0.5f && _t < 2f ? new Vector3(0f, 0f, -1.2f) : Vector3.Zero;
if (_ragdoll.Built && !_needBuild && _t > 0.5f)
if (!_ragdoll.Built || _needBuild)
return;
float pelvis = _ragdoll.Pelvis.GlobalPosition.Y;
if (_t < 0.5f)
_dropStand = pelvis - _ragdoll.GroundY; // standing hips above the feet
else
{
_dropEyeErr = Mathf.Max(_dropEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
if (_ragdoll.Grounded && _ragdoll.GroundY < 0.5f)
_dropMinPelvis = Mathf.Min(_dropMinPelvis, _ragdoll.Pelvis.GlobalPosition.Y);
{
if (pelvis < _dropMinPelvis)
{
_dropMinPelvis = pelvis;
_dropRebound = pelvis;
}
_dropRebound = Mathf.Max(_dropRebound, pelvis);
}
if (_dropMinPelvis < float.MaxValue && _ragdoll.Pelvis.LinearVelocity.Y > -0.5f)
_dropEyeErrLate = Mathf.Max(_dropEyeErrLate, _ragdoll.Eye.DistanceTo(Camera.Origin));
}
}
private bool FinishDrop()
private bool FinishDrop(float height)
{
GD.Print($"RAGDOLL: landing: eye vs camera max {_dropEyeErr:F3} m (limit 0.15), lowest pelvis after touchdown {_dropMinPelvis:F3} m (standing 0.99, limit > 0.7)");
bool landed = _dropEyeErr < 0.15f && _dropMinPelvis > 0.7f;
_dropEyeErr = 0f;
_dropMinPelvis = float.MaxValue;
float dip = _dropStand - _dropMinPelvis, overshoot = _dropRebound - _dropStand;
// The dip grows with the impact; a big fall folds the legs (hips near the ground). Rebound
// past the stance would be a bounce. The view rides the body throughout.
bool dipOk = height < 2f ? dip > 0.12f && dip < 0.5f : dip > 0.45f;
GD.Print($"RAGDOLL: landing from {height} m: dip {dip:F3} m (stance {_dropStand:F3}), rebound above the stance {overshoot:F3} (limit 0.05), eye vs camera max {_dropEyeErr:F3} / after the impact {_dropEyeErrLate:F3} (limit 0.1)");
bool landed = dipOk && overshoot < 0.05f && _dropEyeErrLate < 0.1f;
if (!landed)
return false;
GD.Print($"RAGDOLL: drop: feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, pelvis {_ragdoll.Pelvis.GlobalPosition}, eye error {_ragdoll.Eye.DistanceTo(Camera.Origin):F3}, snaps {NewSnaps}");
return _finite && NewSnaps == 0 && Mathf.Abs(_ragdoll.GroundY) < 0.05f && Mathf.Abs(_origin.Origin.Y) < 0.1f
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.1f && _ragdoll.Pelvis.GlobalPosition.Z > -3f;
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.1f && _ragdoll.Pelvis.GlobalPosition.Z > -3f
&& Mathf.Abs(_ragdoll.Pelvis.GlobalPosition.Y - _ragdoll.GroundY - _dropStand) < 0.05f;
}
// ---------------------------------------------------------------- jump
private float _jumpStand, _jumpApex, _jumpEyeErr, _standJumpApex;
private bool _jumpPressed;
// Optionally squat (eyes `crouch` lower) first, then press jump at 2 s; the player keeps the pose.
private void TickJump(float crouch)
{
float k = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((_t - 0.8f) / 0.8f, 0f, 1f)) * (crouch > 0f ? 1f : 0f);
_headLocal = new Transform3D(new Basis(Vector3.Right, -0.2f * k), new Vector3(0f, EyeHeight - crouch * k, -0.05f * k));
_leftLocal = new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f - crouch * k, -0.3f));
_rightLocal = new Transform3D(Basis.Identity, new Vector3(0.2f, 1.0f - crouch * k, -0.3f));
_measureFrom = float.MaxValue;
if (!_ragdoll.Built || _needBuild)
return;
float pelvis = _ragdoll.Pelvis.GlobalPosition.Y;
if (_t < 0.7f)
{
_jumpStand = pelvis;
_jumpApex = 0f;
_jumpEyeErr = 0f;
_jumpPressed = false;
}
if (_t >= 2f && !_jumpPressed)
{
_jumpPressed = true;
_ragdoll.Jump();
}
if (_jumpPressed)
{
_jumpApex = Mathf.Max(_jumpApex, pelvis - _jumpStand);
_jumpEyeErr = Mathf.Max(_jumpEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
}
}
private bool FinishJump(bool crouched)
{
GD.Print($"RAGDOLL: {(crouched ? "crouched" : "standing")} jump: hips rose {_jumpApex:F3} m above the standing stance, eye vs camera max {_jumpEyeErr:F3}, grounded {_ragdoll.Grounded}, jumping {_ragdoll.Jumping}, snaps {NewSnaps}");
bool ok = _finite && NewSnaps == 0 && _ragdoll.Grounded && !_ragdoll.Jumping && _jumpEyeErr < 0.12f && _origin.Origin.Length() < 0.15f;
if (!crouched)
{
_standJumpApex = _jumpApex;
return ok && _jumpApex > 0.3f && _jumpApex < 0.65f;
}
return ok && _jumpApex > _standJumpApex + 0.1f;
}
// ---------------------------------------------------------------- looking down past vertical
private float _lookYaw0, _lookYawMax, _lookChestMax, _lookPitchMax;
private static float YawOfBody(RigidBody3D b)
{
Vector3 f = BodyMath.HeadFacing(b.GlobalTransform.Basis.Orthonormalized());
return f == Vector3.Zero ? 0f : BodyMath.YawOf(f);
}
// Crouch (eyes 1.0 m), then look down to 125° below the horizon (chin on the chest, looking
// behind the feet) and back, hands down near the floor — interacting with stuff on the ground.
private void TickLookDown(float dt)
{
float k = Mathf.SmoothStep(0f, 1f, Mathf.Min(_t / 1f, 1f));
float pitch = Mathf.DegToRad(125f) * Mathf.Sin(Mathf.Pi * Mathf.Clamp((_t - 1.5f) / 3f, 0f, 1f));
_lookPitchMax = Mathf.Max(_lookPitchMax, pitch);
Basis head = new(Vector3.Right, -pitch);
Vector3 pivot = new(0f, Mathf.Lerp(EyeHeight, 1.0f, k) + TrueBodySolver.EyeToNeckPivot.Y, -0.1f * k + TrueBodySolver.EyeToNeckPivot.Z);
_headLocal = new Transform3D(head, pivot - head * TrueBodySolver.EyeToNeckPivot);
_leftLocal = new Transform3D(Aim(Vector3.Down + Vector3.Forward), new Vector3(-0.2f, Mathf.Lerp(1.0f, 0.3f, k), -0.4f));
_rightLocal = new Transform3D(Aim(Vector3.Down + Vector3.Forward), new Vector3(0.2f, Mathf.Lerp(1.0f, 0.3f, k), -0.4f));
_measureFrom = float.MaxValue;
if (!_ragdoll.Built || _needBuild)
return;
if (_t < 1.4f)
{
_lookYaw0 = YawOfBody(_ragdoll.Pelvis);
_lookYawMax = _lookChestMax = _lookPitchMax = 0f;
return;
}
_lookYawMax = Mathf.Max(_lookYawMax, Mathf.Abs(Mathf.AngleDifference(_lookYaw0, YawOfBody(_ragdoll.Pelvis))));
_lookChestMax = Mathf.Max(_lookChestMax, Mathf.Abs(Mathf.AngleDifference(_lookYaw0, YawOfBody(_ragdoll.Chest))));
}
private bool FinishLookDown()
{
GD.Print($"RAGDOLL: look down to {Mathf.RadToDeg(_lookPitchMax):F0}°: pelvis yaw change max {Mathf.RadToDeg(_lookYawMax):F1}°, chest {Mathf.RadToDeg(_lookChestMax):F1}° (limit 15), eye error {_ragdoll.Eye.DistanceTo(Camera.Origin):F3}, snaps {NewSnaps}");
return _finite && NewSnaps == 0 && _lookYawMax < Mathf.DegToRad(15f) && _lookChestMax < Mathf.DegToRad(15f)
&& _ragdoll.Eye.DistanceTo(Camera.Origin) < 0.08f;
}
// ---------------------------------------------------------------- stick crouch
private float _stickEyeLow, _stickOriginLow, _stickEyeErr;
private void TickStickCrouch(float dt)
{
_measureFrom = float.MaxValue;
if (_driver == null)
return;
// Hold the stick down for 1 s (1.2 m/s → capped at 0.6 m here), hold, then back up.
float c = _t < 0.5f ? 0f : _t < 3.5f ? Mathf.Min(0.6f, (_t - 0.5f) * 1.2f) : Mathf.Max(0f, 0.6f - (_t - 3.5f) * 1.2f);
_driver.StickCrouch = c;
if (_t > 2.5f && _t < 3.4f && _ragdoll.Built)
{
_stickEyeLow = _ragdoll.Eye.Y;
_stickOriginLow = _origin.Origin.Y;
_stickEyeErr = Mathf.Max(_stickEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin));
}
}
private bool FinishStickCrouch()
{
GD.Print($"RAGDOLL: stick crouch 0.6 m: eye y {_stickEyeLow:F3} (standing {EyeHeight}), origin y {_stickOriginLow:F3}, eye vs camera {_stickEyeErr:F3}; back up: eye y {_ragdoll.Eye.Y:F3}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}");
bool ok = _finite && NewSnaps == 0 && Mathf.Abs(_stickEyeLow - (EyeHeight - 0.6f)) < 0.1f && Mathf.Abs(_stickOriginLow + 0.6f) < 0.08f
&& _stickEyeErr < 0.06f && Mathf.Abs(_ragdoll.Eye.Y - EyeHeight) < 0.06f && Mathf.Abs(_origin.Origin.Y) < 0.06f;
_stickEyeErr = 0f;
return ok;
}
private Vector3 _crouchPelvis;