diff --git a/scripts/PlayerRagdoll.cs b/scripts/PlayerRagdoll.cs index 142a236..b329d39 100644 --- a/scripts/PlayerRagdoll.cs +++ b/scripts/PlayerRagdoll.cs @@ -257,8 +257,11 @@ public partial class PlayerRagdoll : Node3D _eyeLocal -= headCenterFromPivot; // eye relative to the head body's centre Color skin = new(0.85f, 0.72f, 0.6f), shirt = new(0.3f, 0.45f, 0.7f), pants = new(0.25f, 0.27f, 0.32f); - Pelvis = MakeBody("Pelvis", new BoxShape3D { Size = pelvisSize }, (0.142f + 0.139f + 0.322f) * m - LegsMass, BodyLayer, WorldMask, pants, - c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal), friction: TorsoFriction); + // Hips = a sideways capsule (Nimso: "hip collider: a capsule, ~27 cm diameter"): a box's bottom + // edge caught on ledge lips a centimetre below it, so the hips couldn't slide over onto them. + float hipRadius = Mathf.Min(0.5f * Mathf.Max(pelvisSize.Y, pelvisSize.Z), 0.5f * pelvisSize.X); + Pelvis = MakeBody("Pelvis", new CapsuleShape3D { Radius = hipRadius, Height = pelvisSize.X }, (0.142f + 0.139f + 0.322f) * m - LegsMass, BodyLayer, WorldMask, pants, + c * new Transform3D(Basis.Identity, hip + _pelvisCenterLocal), friction: TorsoFriction, shapeBasis: new Basis(Vector3.Back, Mathf.Pi / 2f)); float standingHip = pose.PelvisTarget.Y - (pose.Eye.Y - p.EyeHeight); _standingHip = standingHip + _pelvisCenterLocal.Y; // of the pelvis BODY (what ground control holds) _legsHeight = Mathf.Max(0.2f, standingHip - MaxStepHeight); @@ -372,14 +375,22 @@ public partial class PlayerRagdoll : Node3D // NOTE (Box3D wrapper): friction is read only when the body's shapes are created — the material // must be set before AddChild (setting it later silently does nothing). - private RigidBody3D MakeBody(string name, Shape3D shape, float mass, uint layer, uint mask, Color color, Transform3D at, bool visible = true, float friction = 1f) + private RigidBody3D MakeBody(string name, Shape3D shape, float mass, uint layer, uint mask, Color color, Transform3D at, bool visible = true, float friction = 1f, + Basis? shapeBasis = null) { RigidBody3D body = new() { Name = name, Mass = mass, CollisionLayer = layer, CollisionMask = mask, Transform = at, ContactMonitor = true, MaxContactsReported = 4, PhysicsMaterialOverride = new PhysicsMaterial { Friction = friction } }; - body.AddChild(new CollisionShape3D { Shape = shape }); - if (shape is BoxShape3D box && layer != 0 && visible) + Transform3D shapeAt = new(shapeBasis ?? Basis.Identity, Vector3.Zero); + body.AddChild(new CollisionShape3D { Shape = shape, Transform = shapeAt }); + Mesh visual = shape switch { - MeshInstance3D mesh = new() { Mesh = new BoxMesh { Size = box.Size } }; + BoxShape3D box => new BoxMesh { Size = box.Size }, + CapsuleShape3D cap => new CapsuleMesh { Radius = cap.Radius, Height = cap.Height }, + _ => null, + }; + if (visual != null && layer != 0 && visible) + { + MeshInstance3D mesh = new() { Mesh = visual, Transform = shapeAt }; mesh.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.6f }); body.AddChild(mesh); } @@ -762,7 +773,7 @@ public partial class PlayerRagdoll : Node3D bool deep = center + _legsHeight * 0.5f >= hip.Y + LegsOffAbove; // 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 kneeUp = (Braced || _sinceBraced < ClamberMemory) && HeldBack != Vector3.Zero; bool wantTuck = deep || _lifted || kneeUp || (!_legsSolid && Braced); _tuckFree = wantTuck ? 0f : _tuckFree + _dt; Vector3 untucked = new(hip.X, center, hip.Z); @@ -863,7 +874,8 @@ public partial class PlayerRagdoll : Node3D bool wasGrounded = Grounded; Grounded = false; _groundBody = null; - if (ProbeGround(pos, hipAboveFloor + GroundProbeExtra + 0.5f, wasGrounded, out Vector3 point, out Vector3 normal, out GodotObject groundObject, out bool underHips)) + bool clambering = Braced || _lifted || _sinceBraced < ClamberMemory || !_legsSolid; + if (ProbeGround(pos, hipAboveFloor + GroundProbeExtra + 0.5f, wasGrounded || clambering ? (clambering ? 2 : 1) : 0, out Vector3 point, out Vector3 normal, out GodotObject groundObject, out bool underHips)) { _groundBody = groundObject as RigidBody3D; _groundPoint = point; @@ -1099,7 +1111,9 @@ public partial class PlayerRagdoll : Node3D // 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) + // mode: 0 airborne, 1 standing, 2 clambering (pressing onto something: the footprint may take + // higher ground the hips are only partly over — the capsule hips then ride up onto its edge). + private bool ProbeGround(Vector3 pos, float length, int mode, out Vector3 point, out Vector3 normal, out GodotObject collider, out bool underHips) { PhysicsDirectSpaceState3D space = GetWorld3D().DirectSpaceState; point = normal = Vector3.Zero; @@ -1135,7 +1149,7 @@ public partial class PlayerRagdoll : Node3D 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 sameLevel = sPoint.Y <= _footY + MaxStepHeight || mode != 1; bool better = !haveRay || normal.Y <= Mathf.Cos(Mathf.DegToRad(MaxSlopeDegrees)) || sPoint.Y > point.Y + 0.01f; if (sameLevel && better) { diff --git a/scripts/RagdollSpike.cs b/scripts/RagdollSpike.cs index 7ff9be7..a52a2db 100644 --- a/scripts/RagdollSpike.cs +++ b/scripts/RagdollSpike.cs @@ -1142,6 +1142,15 @@ public partial class RagdollSpike : Node3D } if (t > 1f) _climbEyeErr = Mathf.Max(_climbEyeErr, _ragdoll.Eye.DistanceTo(Camera.Origin)); + // The free hand reaching for the next hold while hanging on the other (tipped body): it + // should stay on its controller (owner: "tracking off when the body is tilted"). + for (int side = 0; side < 2; side++) + if (_climbWeld[side] == null && _climbStep is 2 or 3 && _ragdoll.Hand(side).GetContactCount() == 0) + { + Vector3 ctrl = (side == 0 ? Left : Right).Origin; + _climbFreeErr += _ragdoll.Hand(side).GlobalPosition.DistanceTo(ctrl); + _climbFreeN++; + } void Next(int step) { @@ -1158,9 +1167,15 @@ public partial class RagdollSpike : Node3D } } + private float _climbFreeErr; + private int _climbFreeN; + private bool FinishClimbWall() { float rise = _ragdoll.Pelvis.GlobalPosition.Y - _climbStartPelvis; + GD.Print($"RAGDOLL: wall climb: free hand vs its controller while reaching {(_climbFreeN > 0 ? _climbFreeErr / _climbFreeN : 0f):F3} m mean"); + _climbFreeErr = 0f; + _climbFreeN = 0; GD.Print($"RAGDOLL: wall climb: reached step {_climbStep}/5, pelvis rose {rise:F3} m (> 0.35), holding {_climbWeld[0] != null}/{_climbWeld[1] != null}, eye vs camera max {_climbEyeErr:F3} (< {PlayerRagdoll.MaxViewLead + 0.03f:F2}), snaps {NewSnaps}"); Grip(0, false); Grip(1, false);