Ground under the body: sphere footprint, clamber by pressure (Nimso), physics objects climbable, arms keep colliding

- Ground = centre ray + stance-wide sphere cast (face normal from a ray inside the contact):
  planks and lips keep support.
- Higher ground is stood on when the hips are over it and the body presses onto it (hands
  braced, a just-finished press, legs tucked over it, or airborne) - no hips-well-above rule.
- Bracing counts heavy/frozen physics objects (>= 15 kg), not what the hand carries.
- Knee up: braced + hips held back tucks the legs collider.
- Support goal capped at straight legs (tiptoe/over-tall head floated the body).
- Arms never get collision exceptions with held objects (they disabled whole crag rocks).
- Spike: heavy-box mantle, 15 cm plank, chest-high ledge two hands yes / one hand no.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 20:14:11 +03:00
1 parent 3e300c7f04
commit 2d986d08ac
2 files changed
+172 -52

No files matched your search

+97 -42
View File
@@ -187,7 +187,7 @@ public partial class PlayerRagdoll : Node3D
private float _supportGoalY; private float _supportGoalY;
private RigidBody3D _groundBody; private RigidBody3D _groundBody;
private Vector3 _groundPoint; private Vector3 _groundPoint;
private float _tuckFree, _dt; private float _tuckFree, _dt, _sinceBraced = float.MaxValue;
private PhysicsShapeQueryParameters3D _legsQuery; private PhysicsShapeQueryParameters3D _legsQuery;
private bool LegsSpaceFree(Vector3 center, Basis basis) private bool LegsSpaceFree(Vector3 center, Basis basis)
@@ -599,6 +599,7 @@ public partial class PlayerRagdoll : Node3D
HandGuide(a, _targets[a.Hand], side == 0 ? leftVelocity : rightVelocity); HandGuide(a, _targets[a.Hand], side == 0 ? leftVelocity : rightVelocity);
Braced |= IsBraced(a); Braced |= IsBraced(a);
} }
_sinceBraced = Braced ? 0f : _sinceBraced + dt;
GroundControl(pelvisTarget, floorY, dt); GroundControl(pelvisTarget, floorY, dt);
HeadGuide(s, dt); HeadGuide(s, dt);
LegsDrive(s); LegsDrive(s);
@@ -720,15 +721,24 @@ public partial class PlayerRagdoll : Node3D
j.SetParamZ(Generic6DofJoint3D.Param.LinearMotorForceLimit, cap); j.SetParamZ(Generic6DofJoint3D.Param.LinearMotorForceLimit, cap);
} }
// A hand pressing on or holding static world (a ledge, a wall, a grabbed bar) — not the floor: // A hand pressing on or holding something solid enough to push yourself up on (a ledge, a wall,
// hands on the floor while crawling don't lift the body. // a grabbed bar, a heavy crate) — not the floor: hands on the floor while crawling don't lift the
// body. Nimso: physics objects are climbed "the same" as the world. A light prop just gets pushed,
// and something you're carrying isn't support.
public const float BraceMinMass = 15f;
public const float ClamberMemory = 0.6f; // s a hand press still counts after the hands leave
private static bool IsBraced(Arm a) private static bool IsBraced(Arm a)
{ {
if (a.Held is StaticBody3D) if (a.Held is StaticBody3D || a.Held is RigidBody3D { Freeze: true })
return true; return true;
foreach (Node3D n in a.Hand.GetCollidingBodies()) foreach (Node3D n in a.Hand.GetCollidingBodies())
if (n is StaticBody3D && !n.IsInGroup(PropGrabber.NoGrabGroup)) {
if (n == a.Held || n.IsInGroup(PropGrabber.NoGrabGroup))
continue;
if (n is StaticBody3D || n is RigidBody3D { Freeze: true } || n is RigidBody3D { Mass: >= BraceMinMass })
return true; return true;
}
return false; return false;
} }
@@ -748,7 +758,10 @@ public partial class PlayerRagdoll : Node3D
bool climbingHold = _arms[0].Held is StaticBody3D || _arms[1].Held is StaticBody3D; bool climbingHold = _arms[0].Held is StaticBody3D || _arms[1].Held is StaticBody3D;
_lifted = Braced && (Pelvis.GlobalPosition.Y > _supportGoalY + MantleLift || climbingHold); _lifted = Braced && (Pelvis.GlobalPosition.Y > _supportGoalY + MantleLift || climbingHold);
bool deep = center + _legsHeight * 0.5f >= hip.Y + LegsOffAbove; bool deep = center + _legsHeight * 0.5f >= hip.Y + LegsOffAbove;
bool wantTuck = deep || _lifted || (!_legsSolid && Braced); // Hands braced while the hips are held back by an obstacle = getting a knee up onto it (a
// thigh-high crate or ledge the straight arms can't press down on while standing).
bool kneeUp = Braced && HeldBack != Vector3.Zero;
bool wantTuck = deep || _lifted || kneeUp || (!_legsSolid && Braced);
_tuckFree = wantTuck ? 0f : _tuckFree + _dt; _tuckFree = wantTuck ? 0f : _tuckFree + _dt;
Vector3 untucked = new(hip.X, center, hip.Z); Vector3 untucked = new(hip.X, center, hip.Z);
bool solid = _legsSolid ? !wantTuck : _tuckFree > UntuckDelay && LegsSpaceFree(untucked, s.YawBasis); bool solid = _legsSolid ? !wantTuck : _tuckFree > UntuckDelay && LegsSpaceFree(untucked, s.YawBasis);
@@ -845,16 +858,12 @@ public partial class PlayerRagdoll : Node3D
{ {
Vector3 pos = Pelvis.GlobalPosition; Vector3 pos = Pelvis.GlobalPosition;
float hipAboveFloor = target.Origin.Y - floorY; float hipAboveFloor = target.Origin.Y - floorY;
var query = PhysicsRayQueryParameters3D.Create(pos, pos + Vector3.Down * (hipAboveFloor + GroundProbeExtra + 0.5f), WorldMask);
Godot.Collections.Dictionary hit = GetWorld3D().DirectSpaceState.IntersectRay(query);
bool wasGrounded = Grounded; bool wasGrounded = Grounded;
Grounded = false; Grounded = false;
_groundBody = null; _groundBody = null;
if (hit.Count > 0) if (ProbeGround(pos, hipAboveFloor + GroundProbeExtra + 0.5f, wasGrounded, out Vector3 point, out Vector3 normal, out GodotObject groundObject, out bool underHips))
{ {
Vector3 point = (Vector3)hit["position"]; _groundBody = groundObject as RigidBody3D;
Vector3 normal = (Vector3)hit["normal"];
_groundBody = GodotObject.InstanceFromId((ulong)hit["collider_id"]) as RigidBody3D; // Box3D: "collider" is null
_groundPoint = point; _groundPoint = point;
// Standing (or walking down a step) the legs keep contact over GroundProbeExtra; falling, // Standing (or walking down a step) the legs keep contact over GroundProbeExtra; falling,
// they touch down only within their reach — the rest of the fall is stopped by the legs. // they touch down only within their reach — the rest of the fall is stopped by the legs.
@@ -873,14 +882,17 @@ public partial class PlayerRagdoll : Node3D
} }
else if (inReach && !steep) else if (inReach && !steep)
{ {
// Step rule: ground more than a step above the feet is only stood on once the // Step rule (Nimso: higher ground "doesn't move my body up … until I push on it with enough
// hips are already well above it (climbed/mantled/landed there) — otherwise it's an // pressure, at which point I actually clamber over"): ground more than a step above the
// obstacle the legs collider holds us back from, and the feet stay where they were. // feet is stood on once the hips are over it AND the body is pressing onto it — hands
// Higher ground climbed onto must be under the hips' back edge too (not just a // braced on anything solid (ledge, crate), or already airborne
// pelvis centre hanging over the lip). // (landing, hanging). Otherwise it's an obstacle the legs collider holds us back from.
bool clear = point.Y <= _footY + MaxStepHeight || GroundBehindAt(pos, point.Y); // No "hips well above it" requirement: that made a ledge need a full press-up.
bool reachable = point.Y <= _footY + MaxStepHeight bool step = point.Y <= _footY + MaxStepHeight;
|| ((!wasGrounded || _lifted || pos.Y - point.Y > 0.6f * hipAboveFloor) && clear); // The press lasts a moment after the hands leave (you push up, then move over it), and legs
// tucked up with the hips over it are already on top of it.
bool pressing = !wasGrounded || Braced || _lifted || _sinceBraced < ClamberMemory || !_legsSolid;
bool reachable = step || (underHips && pressing);
Grounded = true; Grounded = true;
if (reachable) if (reachable)
_footY = point.Y; _footY = point.Y;
@@ -915,6 +927,9 @@ public partial class PlayerRagdoll : Node3D
{ {
// Pose heights are relative to the tracking floor; stand on the ground actually under us. // Pose heights are relative to the tracking floor; stand on the ground actually under us.
Vector3 goal = target.Origin + Vector3.Up * (GroundY - floorY); Vector3 goal = target.Origin + Vector3.Up * (GroundY - floorY);
// Legs can't hold the hips higher than straight legs (head above the calibrated height —
// tiptoes, a reach up — would otherwise float the body off the ground).
goal.Y = Mathf.Min(goal.Y, _footY + _standingHip + JumpExtension);
_supportGoalY = goal.Y; _supportGoalY = goal.Y;
if (_jump == JumpPhase.Dip) if (_jump == JumpPhase.Dip)
goal.Y = Mathf.Min(goal.Y, _jumpFromY); goal.Y = Mathf.Min(goal.Y, _jumpFromY);
@@ -1070,16 +1085,64 @@ public partial class PlayerRagdoll : Node3D
private PhysicsShapeQueryParameters3D _cameraQuery; private PhysicsShapeQueryParameters3D _cameraQuery;
// Second ground ray 12 cm behind the hips (toward the body's back): ground at least `groundY`. // Ground under the body (Nimso: "ground detection uses the objects underneath the body"), not
private bool GroundBehindAt(Vector3 pelvisPos, float groundY) // under one point: a sphere the width of the stance is cast down from the hips, plus the
// centre ray. One ray lost the ground as soon as the hips swayed off a plank or past a ledge lip.
// The sphere's contact can be a corner, whose normal is useless — the surface normal comes from a
// short ray just inside the contact. Same-level ground (within a step) from either probe is
// stood on; higher ground only from the centre ray (the hips must really be over it), and it's
// reported via underHips for the pressure rule.
public const float FootprintRadius = 0.12f;
private bool ProbeGround(Vector3 pos, float length, bool wasGrounded, out Vector3 point, out Vector3 normal, out GodotObject collider, out bool underHips)
{ {
Vector3 back = Pelvis.GlobalTransform.Basis.Z; PhysicsDirectSpaceState3D space = GetWorld3D().DirectSpaceState;
back.Y = 0f; point = normal = Vector3.Zero;
back = back.LengthSquared() > 1e-6f ? back.Normalized() * 0.12f : Vector3.Zero; collider = null;
var query = PhysicsRayQueryParameters3D.Create(pelvisPos + back, pelvisPos + back + Vector3.Down * 3f, WorldMask); underHips = false;
Godot.Collections.Dictionary hit = GetWorld3D().DirectSpaceState.IntersectRay(query); Godot.Collections.Dictionary ray = space.IntersectRay(PhysicsRayQueryParameters3D.Create(pos, pos + Vector3.Down * length, WorldMask));
return hit.Count > 0 && ((Vector3)hit["position"]).Y > groundY - 0.05f; bool haveRay = ray.Count > 0;
if (haveRay)
{
point = (Vector3)ray["position"];
normal = (Vector3)ray["normal"];
collider = GodotObject.InstanceFromId((ulong)ray["collider_id"]); // Box3D: "collider" is null
underHips = true;
} }
_footprintQuery ??= new PhysicsShapeQueryParameters3D { Shape = new SphereShape3D { Radius = FootprintRadius }, CollisionMask = WorldMask };
_footprintQuery.Transform = new Transform3D(Basis.Identity, pos);
_footprintQuery.Motion = Vector3.Down * length;
Godot.Collections.Dictionary rest = space.GetRestInfo(_footprintQuery);
if (rest.Count == 0)
return haveRay;
Vector3 contact = (Vector3)rest["point"];
if (contact.Y >= pos.Y - 0.05f)
return haveRay; // touching a wall beside the hips, not ground
Vector3 inward = new(contact.X - pos.X, 0f, contact.Z - pos.Z);
inward = inward.LengthSquared() > 1e-6f ? inward.Normalized() * 0.02f : Vector3.Zero;
Vector3 probe = contact + inward;
Godot.Collections.Dictionary face = space.IntersectRay(PhysicsRayQueryParameters3D.Create(
new Vector3(probe.X, contact.Y + 0.05f, probe.Z), new Vector3(probe.X, contact.Y - 0.05f, probe.Z), WorldMask));
if (face.Count == 0)
return haveRay;
Vector3 sPoint = (Vector3)face["position"], sNormal = (Vector3)face["normal"];
if (sNormal.Y <= Mathf.Cos(Mathf.DegToRad(MaxSlopeDegrees)))
return haveRay;
// The sphere wins where the centre ray found nothing, nothing walkable, or only lower ground
// within the stance (a plank's edge under the hips) — but never higher ground than a step.
bool sameLevel = sPoint.Y <= _footY + MaxStepHeight || !wasGrounded;
bool better = !haveRay || normal.Y <= Mathf.Cos(Mathf.DegToRad(MaxSlopeDegrees)) || sPoint.Y > point.Y + 0.01f;
if (sameLevel && better)
{
point = sPoint;
normal = sNormal;
collider = GodotObject.InstanceFromId((ulong)face["collider_id"]);
underHips = true; // under the footprint
}
return true;
}
private PhysicsShapeQueryParameters3D _footprintQuery;
public void TeleportBy(Transform3D delta) public void TeleportBy(Transform3D delta)
{ {
@@ -1118,23 +1181,15 @@ public partial class PlayerRagdoll : Node3D
b.ResetPhysicsInterpolation(); b.ResetPhysicsInterpolation();
} }
// Collision exceptions between a held prop and that arm's segments (its hand is excluded by // What each hand holds (bracing, climbing). Only the hand itself is excluded from the held object
// the grab weld itself) — a forearm wrapped around a held object must not fight it. // (by the grab weld).
public void SetHeld(int side, PhysicsBody3D prop) public void SetHeld(int side, PhysicsBody3D prop)
{ {
if (!Built) if (!Built)
return; return;
Arm a = _arms[side]; // The arm keeps colliding with what the hand holds (owner: real arms don't pass through things).
if (a.Held != null && IsInstanceValid(a.Held) && a.Held != _arms[1 - side].Held) // The old per-object exception switched the arm off against a whole crag rock while
{ // gripping one of its holds — arms "randomly" phased through the world mid-climb.
a.Upper.RemoveCollisionExceptionWith(a.Held); _arms[side].Held = prop;
a.Fore.RemoveCollisionExceptionWith(a.Held);
}
a.Held = prop;
if (prop != null)
{
a.Upper.AddCollisionExceptionWith(prop);
a.Fore.AddCollisionExceptionWith(prop);
}
} }
} }
+75 -10
View File
@@ -100,7 +100,10 @@ public partial class RagdollSpike : Node3D
_phases.Add(("a table can't be walked over", SetupTableObstacle, _ => _stickVelocity = new Vector3(0f, 0f, -1.5f), 3f, FinishBlockedLow)); _phases.Add(("a table can't be walked over", SetupTableObstacle, _ => _stickVelocity = new Vector3(0f, 0f, -1.5f), 3f, FinishBlockedLow));
_phases.Add(("walks up a 0.25 m step", () => SetupObstacle(0.25f, 8f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 3f, FinishStep)); _phases.Add(("walks up a 0.25 m step", () => SetupObstacle(0.25f, 8f), _ => _stickVelocity = new Vector3(0f, 0f, -1.2f), 3f, FinishStep));
_phases.Add(("two-handed mantle onto a 0.9 m ledge (crouch, hands on the edge, stand and press)", () => SetupObstacle(0.9f), _ => TickHumanMantle(both: true), 7f, () => FinishMantle(true))); _phases.Add(("two-handed mantle onto a 0.9 m ledge (crouch, hands on the edge, stand and press)", () => SetupObstacle(0.9f), _ => TickHumanMantle(both: true), 7f, () => FinishMantle(true)));
_phases.Add(("one hand can't mantle", () => SetupObstacle(0.9f), _ => TickHumanMantle(both: false), 7f, () => FinishMantle(false))); _phases.Add(("two-handed mantle onto a chest-high 1.2 m ledge", () => SetupObstacle(1.2f), _ => TickHumanMantle(both: true), 7f, () => FinishMantle(true)));
_phases.Add(("one hand can't mantle the 1.2 m ledge (the hips must be lifted)", () => SetupObstacle(1.2f), _ => TickHumanMantle(both: false), 7f, () => FinishMantle(false)));
_phases.Add(("mantles onto the playground's heavy box (25 kg physics object, 0.7 m)", SetupCrateLedge, _ => TickHumanMantle(both: true), 8.5f, FinishCrateMantle));
_phases.Add(("walks a 15 cm plank swaying the hips without falling off", SetupPlank, TickPlank, 6f, FinishPlank));
_phases.Add(("two-handed pull-up on a grabbed bar", () => SetupBarHang(), t => TickPullUp(both: true), 3.5f, () => FinishPullUp(true))); _phases.Add(("two-handed pull-up on a grabbed bar", () => SetupBarHang(), t => TickPullUp(both: true), 3.5f, () => FinishPullUp(true)));
_phases.Add(("one-handed pull-up is weaker", () => SetupBarHang(), t => TickPullUp(both: false), 3.5f, () => FinishPullUp(false))); _phases.Add(("one-handed pull-up is weaker", () => SetupBarHang(), t => TickPullUp(both: false), 3.5f, () => FinishPullUp(false)));
_phases.Add(("crawls through a 0.6 m tunnel", SetupTunnel, TickCrawl, 7f, FinishCrawl)); _phases.Add(("crawls through a 0.6 m tunnel", SetupTunnel, TickCrawl, 7f, FinishCrawl));
@@ -531,9 +534,59 @@ public partial class RagdollSpike : Node3D
private void SetupObstacle(float height, float depth = 2f) private void SetupObstacle(float height, float depth = 2f)
{ {
Rest(); Rest();
_ledgeBox = null;
_ledgeTop = height;
if (height >= 1f)
_origin = new Transform3D(Basis.Identity, new Vector3(0f, 0f, ObstacleFront + 0.4f));
_stage.AddChild(MakeStatic(new Vector3(3f, height, depth), new Vector3(0f, height * 0.5f, ObstacleFront - depth * 0.5f), 29)); _stage.AddChild(MakeStatic(new Vector3(3f, height, depth), new Vector3(0f, height * 0.5f, ObstacleFront - depth * 0.5f), 29));
} }
// Playground.HeavyBox (25 kg, 0.7 m cube) as the ledge, same human mantle as onto the static ledge.
private void SetupCrateLedge()
{
Rest();
_ledgeBox = MakeBox(Vector3.One * 0.7f, 25f, new Vector3(0f, 0.35f, ObstacleFront - 0.35f));
_ledgeTop = 0.7f;
}
private RigidBody3D _ledgeBox;
private float _ledgeTop = 0.9f;
private bool FinishCrateMantle()
{
GD.Print($"RAGDOLL: crate mantle: pelvis rose up to {_maxPelvisRise:F3} m, feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}");
_maxPelvisRise = 0f;
return _finite && NewSnaps == 0 && _ragdoll.GroundY > 0.6f && _origin.Origin.Y > 0.55f;
}
private const float PlankY = 0.3f;
private float _plankMinFoot;
private void SetupPlank()
{
Rest();
_origin = new Transform3D(Basis.Identity, new Vector3(0f, PlankY, 0f));
_stage.AddChild(MakeStatic(new Vector3(0.15f, PlankY, 8f), new Vector3(0f, PlankY * 0.5f, -3f), 29));
_plankMinFoot = float.MaxValue;
}
// Walk along it with the head (and so the hips) swaying ±9 cm side to side.
private void TickPlank(float dt)
{
float sway = _t > 1f ? 0.09f * Mathf.Sin(Mathf.Tau * 0.7f * (_t - 1f)) : 0f;
_headLocal = new Transform3D(Basis.Identity, new Vector3(sway, EyeHeight, 0f));
_stickVelocity = _t > 1f && _t < 5f ? new Vector3(0f, 0f, -0.6f) : Vector3.Zero;
_measureFrom = float.MaxValue;
if (_ragdoll.Built && !_needBuild && _t > 0.5f)
_plankMinFoot = Mathf.Min(_plankMinFoot, _ragdoll.Grounded ? _ragdoll.GroundY : -1f);
}
private bool FinishPlank()
{
GD.Print($"RAGDOLL: plank: lowest supported foot level {_plankMinFoot:F3} (plank top {PlankY}), pelvis {_ragdoll.Pelvis.GlobalPosition}, eye error {_ragdoll.Eye.DistanceTo(Camera.Origin):F3}, snaps {NewSnaps}");
return _finite && NewSnaps == 0 && _plankMinFoot > PlankY - 0.03f && _ragdoll.Pelvis.GlobalPosition.Z < -1.8f;
}
private void SetupTableObstacle() private void SetupTableObstacle()
{ {
Rest(); Rest();
@@ -562,7 +615,7 @@ public partial class RagdollSpike : Node3D
private bool FinishMantle(bool both) private bool FinishMantle(bool both)
{ {
GD.Print($"RAGDOLL: mantle ({(both ? "two hands" : "one hand")}): pelvis rose up to {_maxPelvisRise:F3} m, feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}"); GD.Print($"RAGDOLL: mantle ({(both ? "two hands" : "one hand")}): pelvis rose up to {_maxPelvisRise:F3} m, feet y {_ragdoll.GroundY:F3}, origin y {_origin.Origin.Y:F3}, snaps {NewSnaps}");
bool onTop = _ragdoll.GroundY > 0.85f && _origin.Origin.Y > 0.8f; bool onTop = _ragdoll.GroundY > _ledgeTop - 0.05f && _origin.Origin.Y > _ledgeTop - 0.1f;
_lastRise = _maxPelvisRise; _lastRise = _maxPelvisRise;
_maxPelvisRise = 0f; _maxPelvisRise = 0f;
return _finite && NewSnaps == 0 && (both ? onTop : !onTop); // standing tall also lifts the hips; one hand never gets you up return _finite && NewSnaps == 0 && (both ? onTop : !onTop); // standing tall also lifts the hips; one hand never gets you up
@@ -949,20 +1002,32 @@ public partial class RagdollSpike : Node3D
private void TickHumanMantle(bool both) private void TickHumanMantle(bool both)
{ {
float t = _t; float t = _t;
_stickVelocity = t < 1f || (t > 4.5f && t < 5.5f) ? new Vector3(0f, 0f, -0.8f) : Vector3.Zero; // stop before the ledge's far end // Walk up to a waist-high ledge (stop before its far end); a chest-high one starts within reach.
float crouch = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((t - 1f) / 0.8f, 0f, 1f)); _stickVelocity = (t < 1f && _ledgeTop < 1f) || (t > 4.5f && t < 5.5f) ? new Vector3(0f, 0f, -0.8f) : Vector3.Zero;
// Crouch to load the legs for a waist-high ledge; at a chest-high one you reach up instead.
float crouch = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((t - 1f) / 0.8f, 0f, 1f)) * (_ledgeTop < 1f ? 1f : 0f);
float rise = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((t - 2.2f) / 1.6f, 0f, 1f)); float rise = Mathf.SmoothStep(0f, 1f, Mathf.Clamp((t - 2.2f) / 1.6f, 0f, 1f));
float eye = EyeHeight - 0.25f * crouch + 0.35f * rise; float eye = EyeHeight - 0.25f * crouch + 0.35f * rise;
float lean = 0.25f * rise; float lean = (_ledgeTop < 1f ? 0.25f : 0.55f) * rise; // over a chest-high edge you put your head well over it
_headLocal = new Transform3D(new Basis(Vector3.Right, -0.3f * crouch), new Vector3(0f, eye, -lean)); _headLocal = new Transform3D(new Basis(Vector3.Right, -0.3f * crouch), new Vector3(0f, eye, -lean));
float handZ = ObstacleFront - 0.08f - _origin.Origin.Z; // Hands on the top just past the near edge (of the box itself when it's a movable one).
float handY = t < 1f ? 1.0f : Mathf.Lerp(0.95f, 0.55f, rise) - 0.05f * crouch; float front = _ledgeBox != null ? _ledgeBox.GlobalPosition.Z + 0.35f : ObstacleFront;
float handZ = front - 0.08f - _origin.Origin.Z;
float handY = t < 1f ? 1.0f : Mathf.Lerp(_ledgeTop + 0.05f, _ledgeTop - 0.35f, rise) - 0.05f * crouch - _origin.Origin.Y;
Basis down = Aim(Vector3.Down + Vector3.Forward * 0.3f); Basis down = Aim(Vector3.Down + Vector3.Forward * 0.3f);
_leftLocal = both ? new Transform3D(down, new Vector3(-0.2f, handY, handZ)) : new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f)); // Walking up, the hands are down at the sides; then they go up over the edge onto the top.
_rightLocal = new Transform3D(down, new Vector3(0.2f, handY, handZ)); float reach = Mathf.Clamp((t - 0.8f) / 0.4f, 0f, 1f);
Vector3 rest = new(0.2f, 1.0f, -0.3f), onTop = new(0.2f, Mathf.Max(handY, _ledgeTop + 0.08f - _origin.Origin.Y), handZ);
Vector3 r = reach < 1f ? rest.Lerp(new Vector3(0.2f, _ledgeTop + 0.12f - _origin.Origin.Y, -0.3f), reach) : new Vector3(0.2f, handY, handZ);
if (reach >= 1f && t < 1.4f)
r = onTop;
_leftLocal = both ? new Transform3D(down, new Vector3(-r.X, r.Y, r.Z)) : new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
_rightLocal = new Transform3D(down, r);
if (_ragdoll.Built && !_needBuild) if (_ragdoll.Built && !_needBuild)
_maxPelvisRise = Mathf.Max(_maxPelvisRise, _ragdoll.Pelvis.GlobalPosition.Y - 0.99f); _maxPelvisRise = Mathf.Max(_maxPelvisRise, _ragdoll.Pelvis.GlobalPosition.Y - 0.99f);
if (t > 4.5f) if (OS.HasEnvironment("MANTLETRACE") && _ragdoll.Built && ((int)(t * 144f)) % 14 == 0)
GD.Print($"MT t {t:F2} pelvis {_ragdoll.Pelvis.GlobalPosition} v {_ragdoll.Pelvis.LinearVelocity.Y:F2} o {_origin.Origin.Y:F2} j {_ragdoll.Jumping} braced {_ragdoll.Braced} lifted {_ragdoll.Lifted} legs {_ragdoll.LegsSolid} g {_ragdoll.Grounded} foot {_ragdoll.GroundY:F2} hands {_ragdoll.Hand(0).GlobalPosition.Y:F2}/{_ragdoll.Hand(1).GlobalPosition.Y:F2} {(_stage.GetChildCount() > 0 && _stage.GetChild(0) is RigidBody3D rb ? rb.GlobalPosition.ToString() : "")}");
if (t > (_ledgeTop < 1f ? 4.5f : 5.5f))
{ {
_leftLocal = new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f)); _leftLocal = new Transform3D(Basis.Identity, new Vector3(-0.2f, 1.0f, -0.3f));
_rightLocal = new Transform3D(Basis.Identity, new Vector3(0.2f, 1.0f, -0.3f)); _rightLocal = new Transform3D(Basis.Identity, new Vector3(0.2f, 1.0f, -0.3f));