diff --git a/scripts/PropGrabber.cs b/scripts/PropGrabber.cs index 9d767c8..1c5e177 100644 --- a/scripts/PropGrabber.cs +++ b/scripts/PropGrabber.cs @@ -26,6 +26,7 @@ public partial class PropGrabber : Node private readonly uint[] _grabSeq = new uint[2]; private readonly float[] _breakTimer = new float[2]; private readonly bool[] _gripPrev = new bool[2]; + private readonly SoftGrab[] _easing = new SoftGrab[2]; // grabbed, being eased into the hand (no weld yet) private uint _grabCount; public override void _Ready() @@ -50,32 +51,50 @@ public partial class PropGrabber : Node public override void _PhysicsProcess(double delta) { float dt = (float)delta; + for (int hand = 0; hand < 2; hand++) + { + SoftGrab ease = _easing[hand]; + if (ease == null) + continue; + if (!IsInstanceValid(ease.Target) || !IsInstanceValid(ease.Hand)) + _easing[hand] = null; + else if (ease.Step(dt)) + { + _easing[hand] = null; + Weld(hand == 0 ? _left : _right, hand, ease.Hand, ease.Target); + } + } HoldUpdate(_left, 0, dt); HoldUpdate(_right, 1, dt); } + private void Weld(XRController3D controller, int hand, RigidBody3D handBody, PhysicsBody3D prop) + { + _held[hand] = prop; + _grabSeq[hand] = _grabCount++; + Vector3 pivot = PalmPoint(handBody, hand); + _joints[hand] = MakeWeld(handBody, prop, pivot, RigidWeldBias); + _localAnchor[hand] = prop.GlobalTransform.AffineInverse() * pivot; + _arms?.SetHeld(hand, prop as RigidBody3D); + PlayerRig.SetHeld(controller, prop); + string what = prop is RigidBody3D rb ? $"{rb.Mass:0.###} kg" : "static world"; + GD.Print($"PropGrabber: grabbed '{prop.Name}' ({what}, {(hand == 0 ? "left" : "right")}{(_held[1 - hand] == prop ? ", two-handed" : "")})"); + } + private void UpdateHand(XRController3D controller, int hand) { bool grip = controller.GetFloat("grip") > GripThreshold; int other = 1 - hand; - if (grip && !_gripPrev[hand] && _held[hand] == null) + if (grip && !_gripPrev[hand] && _held[hand] == null && _easing[hand] == null) { PhysicsBody3D prop = FindProp(controller, hand); RigidBody3D handBody = PhysicsHands.GetBody(controller); if (prop != null && handBody != null) - { - _held[hand] = prop; - _grabSeq[hand] = _grabCount++; - Vector3 pivot = PalmPoint(handBody, hand); - _joints[hand] = MakeWeld(handBody, prop, pivot, RigidWeldBias); - _localAnchor[hand] = prop.GlobalTransform.AffineInverse() * pivot; - _arms?.SetHeld(hand, prop as RigidBody3D); - PlayerRig.SetHeld(controller, prop); - string what = prop is RigidBody3D rb ? $"{rb.Mass:0.###} kg" : "static world"; - GD.Print($"PropGrabber: grabbed '{prop.Name}' ({what}, {(hand == 0 ? "left" : "right")}{(_held[other] == prop ? ", two-handed" : "")})"); - } + _easing[hand] = SoftGrab.Begin(handBody, hand, prop); // welded once it's in the hand (_PhysicsProcess) } + if (!grip) + _easing[hand] = null; if (!grip && _held[hand] != null) { diff --git a/scripts/RagdollSpike.cs b/scripts/RagdollSpike.cs index a52a2db..1e26c17 100644 --- a/scripts/RagdollSpike.cs +++ b/scripts/RagdollSpike.cs @@ -119,6 +119,8 @@ public partial class RagdollSpike : Node3D _phases.Add(("jump: a tap from a real crouch beats a standing tap", Rest, t => TickJump(0.45f, 0.05f), 4.5f, () => FinishJump(2))); _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(("soft grab: a 1 kg cube 6 cm off the palm is eased into the hand, then held", () => SetupSoftGrab(prop: true), TickSoftGrab, 3f, FinishSoftGrab)); + _phases.Add(("soft grab: a bar 5 cm off the palm — the hand goes to it", () => SetupSoftGrab(prop: false), TickSoftGrab, 3f, FinishSoftGrab)); _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))); @@ -782,6 +784,83 @@ public partial class RagdollSpike : Node3D && Mathf.Abs(_ragdoll.Pelvis.GlobalPosition.Y - _ragdoll.GroundY - _dropStand) < 0.05f; } + // ---------------------------------------------------------------- soft grab + + private PhysicsBody3D _sgTarget; + private SoftGrab _sg; + private ConeTwistJoint3D _sgWeld; + private float _sgGapAtWeld, _sgMaxSpeed, _sgHeldGap, _sgWeldTime; + private bool _sgProp; + + private void SetupSoftGrab(bool prop) + { + Rest(); + _sgProp = prop; + _sg = null; + _sgWeld = null; + _sgGapAtWeld = _sgHeldGap = -1f; + _sgMaxSpeed = 0f; + _sgWeldTime = -1f; + _sgTarget = null; + } + + // Right controller held still in front; at 1.5 s the target appears off the palm and grip is pressed. + private void TickSoftGrab(float dt) + { + _measureFrom = float.MaxValue; + if (!_ragdoll.Built || _needBuild) + return; + RigidBody3D hand = _ragdoll.Hand(1); + if (_sgTarget == null && _t >= 1.5f) + { + Vector3 n = SoftGrab.PalmNormal(hand, 1); + Vector3 surface = SoftGrab.PalmSurface(hand, 1); + if (_sgProp) + { + RigidBody3D cube = MakeBox(Vector3.One * 0.08f, 1f, surface + n * (0.06f + 0.04f)); + cube.GravityScale = 0f; // "floating" just for the setup instant; gravity back on at the weld + _sgTarget = cube; + } + else + { + StaticBody3D bar = new() { CollisionMask = 29, Position = surface + n * (0.05f + 0.025f) }; + bar.AddChild(new CollisionShape3D { Shape = new CylinderShape3D { Radius = 0.025f, Height = 1f } }); + _stage.AddChild(bar); + _sgTarget = bar; + } + return; // let the new body enter the space before querying it + } + if (_sgTarget == null) + return; + if (_sg == null && _sgWeld == null) + _sg = SoftGrab.Begin(hand, 1, _sgTarget); + if (_sg != null) + { + if (_sgTarget is RigidBody3D rb) + _sgMaxSpeed = Mathf.Max(_sgMaxSpeed, rb.LinearVelocity.Length()); + _sgMaxSpeed = Mathf.Max(_sgMaxSpeed, hand.LinearVelocity.Length()); + if (_sg.Step(dt)) + { + _sgGapAtWeld = _sg.Gap; + _sgWeldTime = _t; + _sg = null; + _sgWeld = new ConeTwistJoint3D { Position = PropGrabber.PalmPoint(hand, 1), NodeA = hand.GetPath(), NodeB = _sgTarget.GetPath(), SwingSpan = 0f, TwistSpan = 0f, Bias = 0f, ExcludeNodesFromCollision = true }; + _stage.AddChild(_sgWeld); + if (_sgTarget is RigidBody3D cube) + cube.GravityScale = 1f; + _ragdoll.SetHeld(1, _sgTarget); + } + } + } + + private bool FinishSoftGrab() + { + float held = _sgTarget == null ? -1f : (SoftGrab.PalmSurface(_ragdoll.Hand(1), 1) - _sgTarget.GlobalPosition).Length() - (_sgProp ? 0.04f : 0.025f); + GD.Print($"RAGDOLL: soft grab ({(_sgProp ? "1 kg cube" : "static bar")}): welded after {(_sgWeldTime - 1.5f):F3} s with gap {_sgGapAtWeld:F4} m, fastest motion {_sgMaxSpeed:F2} m/s, held gap at the end {held:F3} m, snaps {NewSnaps}"); + _ragdoll.SetHeld(1, null); + return _finite && NewSnaps == 0 && _sgWeldTime > 0f && _sgWeldTime - 1.5f < 0.25f && _sgGapAtWeld < (_sgProp ? 0.01f : 0.015f) && _sgMaxSpeed < 3.5f && held < 0.02f; + } + // ---------------------------------------------------------------- jump private float _jumpStand, _jumpApex, _jumpEyeErr, _tapApex; diff --git a/scripts/SoftGrab.cs b/scripts/SoftGrab.cs new file mode 100644 index 0000000..eae3101 --- /dev/null +++ b/scripts/SoftGrab.cs @@ -0,0 +1,88 @@ +using Godot; + +// Nimso's "soft grab" (NEXT GEN — No Jank): a grab never snaps and never welds across a gap — the +// grabbed thing is EASED into the hand first, then the weld locks the pose. Without it the weld +// froze whatever gap there was at the moment of the press: hands hovering next to bars and holds, +// props held a few centimetres off the palm (owner). +// The grip point is where the palm actually faces the object's surface (a short sphere cast along +// the palm normal, else toward the object's centre). Over EaseTime that surface point and the palm +// surface are brought together: a light prop flies into the hand, the hand goes to static world or +// anything too heavy to pull. Position only — the relative orientation is kept (no rotation snap). +public sealed class SoftGrab +{ + public const float EaseTime = 0.1f; // s + public const float SettleGap = 0.004f; // m: close enough to weld + public const float MaxEaseSpeed = 3f; // m/s: a far grab takes a little longer instead of jerking + public const float LightMass = 6f; // kg: lighter props come to the hand, heavier things pull the hand + + public readonly RigidBody3D Hand; + public readonly PhysicsBody3D Target; + public readonly int Side; + private readonly Vector3 _onTarget; // grip point, target-local + private float _t, _lastGap = float.MaxValue; + + public float Gap => (PalmSurface(Hand, Side) - Target.GlobalTransform * _onTarget).Length(); + + private SoftGrab(RigidBody3D hand, int side, PhysicsBody3D target, Vector3 gripWorld) + { + Hand = hand; + Side = side; + Target = target; + _onTarget = target.GlobalTransform.AffineInverse() * gripWorld; + } + + // The palm faces +X on the left hand, −X on the right (PropGrabber.PalmPoint). + public static Vector3 PalmNormal(RigidBody3D hand, int side) => + hand.GlobalTransform.Basis.Orthonormalized().X * (side == 0 ? 1f : -1f); + + public static Vector3 PalmSurface(RigidBody3D hand, int side) => + hand.GlobalPosition + PalmNormal(hand, side) * (PlayerRagdoll.HandSize.X * 0.5f); + + public static SoftGrab Begin(RigidBody3D hand, int side, PhysicsBody3D target) + { + Vector3 surface = PalmSurface(hand, side); + Vector3 grip = CastTo(hand, target, hand.GlobalPosition, PalmNormal(hand, side) * 0.14f) + ?? CastTo(hand, target, surface, target.GlobalPosition - surface) + ?? surface; // already touching/inside: weld where it is + return new SoftGrab(hand, side, target, grip); + } + + private static Vector3? CastTo(RigidBody3D hand, PhysicsBody3D target, Vector3 from, Vector3 motion) + { + if (motion.LengthSquared() < 1e-8f) + return null; + PhysicsShapeQueryParameters3D q = new() + { + Shape = new SphereShape3D { Radius = 0.01f }, + Transform = new Transform3D(Basis.Identity, from), + Motion = motion, + CollisionMask = 1, + }; + q.Exclude = new Godot.Collections.Array { hand.GetRid() }; + Godot.Collections.Dictionary hit = hand.GetWorld3D().DirectSpaceState.GetRestInfo(q); + if (hit.Count == 0 || (ulong)hit["collider_id"] != target.GetInstanceId()) + return null; + return (Vector3)hit["point"]; + } + + // Every physics tick until it returns true (then weld). Velocities are set, not forces: the ease + // is a short, exact move, and the solver still resolves contacts along the way. + public bool Step(float dt) + { + _t += dt; + Vector3 gap = PalmSurface(Hand, Side) - Target.GlobalTransform * _onTarget; + // Done when touching, or when it stopped closing (the palm is against the surface — contact + // slop leaves ~1 cm on curved holds — or the arm can't get there). + bool stalled = _t > EaseTime && _lastGap - gap.Length() < 0.0005f; + _lastGap = gap.Length(); + if (gap.Length() < SettleGap || stalled || _t > EaseTime * 3f) + return true; + float remaining = Mathf.Max(EaseTime - _t, dt); + Vector3 close = (gap / remaining).LimitLength(MaxEaseSpeed); + if (Target is RigidBody3D body && !body.Freeze && body.Mass <= LightMass) + body.LinearVelocity = Hand.LinearVelocity + close; + else + Hand.LinearVelocity = (Target is RigidBody3D moving ? moving.LinearVelocity : Vector3.Zero) - close; + return false; + } +}