Arms playtest-2 fixes: flip-proof elbow (angle clamp + rate limit, controller-driven), Stage 2b hand coupling (blocked arm holds the hand back)
- Elbow stored as an angle around the arm axis vs a torso-frame reference: 85% hand-suggested, from the controller pose (not the physics palm), clamped +-110 deg, 6 rad/s rate limit (flip spike: 1.8 cm/tick max vs 13.8 unclamped) - PhysicsArms.ConstrainHand: while an arm segment is in contact the hand may lead the forearm end by 3 cm; PhysicsHands applies it and suspends stuck-snap while active - ArmSpike: forearm-rest-over-anvil (owner pose: collider fine, drawn forearm 3.9 cm -> 0), flip sweep, bar test now asserts the hand is held back; all six spikes PASS x3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
464cda2f71
commit
7bc6a866eb
5 files changed
+193
-51
No files matched your search
+93
-7
@@ -5,13 +5,16 @@ using Godot;
|
||||
// Run: godot-mono --headless --fixed-fps 90 --path . scenes/arm_spike.tscn
|
||||
public partial class ArmSpike : Node3D
|
||||
{
|
||||
private enum Phase { Settle, Stretch, Hug, Hold, Track, Swing, Push, Pin, Teleport, NanGuard, Done }
|
||||
private enum Phase { Settle, Stretch, Hug, Flip, Hold, Rest, Track, Swing, Push, Pin, Teleport, NanGuard, Done }
|
||||
|
||||
private const float GapLimit = 0.04f;
|
||||
private const float StretchChestSpeedLimit = 0.05f;
|
||||
private const float StretchLeanRangeLimit = 0.01f;
|
||||
private const float HugElbowOutMin = 0.05f;
|
||||
private const float HoldPenetrationLimit = 0.01f;
|
||||
private const float FlipElbowStepLimit = 0.03f;
|
||||
private const float PinHandLagMin = 0.1f;
|
||||
private const float RestBodyPenLimit = 0.01f;
|
||||
private const float HoldAnvilY = 1.25f; // anvil top ≈ shoulder height, like a seated player at the table
|
||||
private const float HoldPitchDeg = 50f;
|
||||
private static readonly Vector3 AnvilSize = new(0.28f, 0.16f, 0.13f);
|
||||
@@ -31,6 +34,7 @@ public partial class ArmSpike : Node3D
|
||||
private readonly RigidBody3D[] _hands = new RigidBody3D[2];
|
||||
private readonly Transform3D[] _handTarget = new Transform3D[2];
|
||||
private readonly Vector3[] _handRest = new Vector3[2];
|
||||
private readonly Node3D[] _orient = new Node3D[2];
|
||||
private RigidBody3D _box;
|
||||
private Vector3 _boxStart;
|
||||
private StaticBody3D _slab;
|
||||
@@ -46,6 +50,12 @@ public partial class ArmSpike : Node3D
|
||||
private ConeTwistJoint3D _holdWeld;
|
||||
private float _maxHoldPenetration;
|
||||
private bool _holdFinite = true;
|
||||
private float _maxElbowStep;
|
||||
private Vector3 _lastElbow;
|
||||
private float _maxPinHandLag;
|
||||
private float _restBodyPen;
|
||||
private float _restDrawnPen;
|
||||
private float _restAnvilMove;
|
||||
private float _maxStretchChestSpeed;
|
||||
private Vector3 _stretchOffsetMin = Vector3.One * 9f;
|
||||
private Vector3 _stretchOffsetMax = -Vector3.One * 9f;
|
||||
@@ -87,7 +97,12 @@ public partial class ArmSpike : Node3D
|
||||
AddChild(_hands[side]);
|
||||
}
|
||||
|
||||
_arms = new PhysicsArms { HeadSource = _head, Torso = _torso, HandLeft = _hands[0], HandRight = _hands[1] };
|
||||
for (int side = 0; side < 2; side++)
|
||||
{
|
||||
_orient[side] = new Node3D();
|
||||
AddChild(_orient[side]);
|
||||
}
|
||||
_arms = new PhysicsArms { HeadSource = _head, Torso = _torso, HandLeft = _hands[0], HandRight = _hands[1], OrientLeft = _orient[0], OrientRight = _orient[1] };
|
||||
AddChild(_arms);
|
||||
GD.Print("ARM: arm spike scene ready");
|
||||
}
|
||||
@@ -97,7 +112,12 @@ public partial class ArmSpike : Node3D
|
||||
float dt = (float)delta;
|
||||
_phaseT += dt;
|
||||
for (int side = 0; side < 2; side++)
|
||||
PhysicsHands.Drive(_hands[side], _handTarget[side], dt);
|
||||
{
|
||||
_orient[side].GlobalTransform = _handTarget[side];
|
||||
Transform3D driveTarget = _handTarget[side];
|
||||
_arms.ConstrainHand(side, ref driveTarget);
|
||||
PhysicsHands.Drive(_hands[side], driveTarget, dt);
|
||||
}
|
||||
if (!_arms.Calibrated || _arms.UpperArm(0) == null)
|
||||
return;
|
||||
|
||||
@@ -144,6 +164,29 @@ public partial class ArmSpike : Node3D
|
||||
Record($"hug: elbows out beyond the shoulders (L {outL:F3} m, R {outR:F3} m, limit > {HugElbowOutMin}; elbows L {_arms.Elbow(0)} R {_arms.Elbow(1)})", outMin > HugElbowOutMin);
|
||||
for (int side = 0; side < 2; side++)
|
||||
_handTarget[side] = new Transform3D(Basis.Identity, _handRest[side]);
|
||||
Start(Phase.Flip, "flip — right controller's pointing direction sweeps full circles around the arm, elbow must not jump");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Phase.Flip:
|
||||
{
|
||||
// Hand fixed; its forward axis sweeps a 70° cone around the shoulder→hand
|
||||
// direction at 1 rev/s, so the hand-suggested elbow circles the arm axis and
|
||||
// passes the inward/up side every revolution.
|
||||
Vector3 handPos = new(0.2f, 1.25f, -0.4f);
|
||||
Vector3 axis = (handPos - _arms.Shoulder(1)).Normalized();
|
||||
Vector3 perp = axis.Cross(Vector3.Up).Normalized();
|
||||
Vector3 fwd = (axis * Mathf.Cos(1.22f) + perp.Rotated(axis, Mathf.Tau * _phaseT) * Mathf.Sin(1.22f)).Normalized();
|
||||
_handTarget[1] = new Transform3D(Basis.LookingAt(fwd, Mathf.Abs(fwd.Y) > 0.95f ? Vector3.Forward : Vector3.Up), handPos);
|
||||
Vector3 elbow = _arms.ElbowTarget(1);
|
||||
if (_phaseT > 0.5f)
|
||||
_maxElbowStep = Mathf.Max(_maxElbowStep, elbow.DistanceTo(_lastElbow));
|
||||
_lastElbow = elbow;
|
||||
if (_phaseT > 3.5f)
|
||||
{
|
||||
Record($"flip: elbow never jumps (max step {_maxElbowStep:F4} m/tick, limit {FlipElbowStepLimit})", _maxElbowStep < FlipElbowStepLimit);
|
||||
_handTarget[1] = new Transform3D(Basis.Identity, _handRest[1]);
|
||||
_maxHoldPenetration = 0f;
|
||||
Start(Phase.Hold, "hold — right hand grips a 50 kg anvil from the top, forearm must stay out of it");
|
||||
}
|
||||
@@ -196,6 +239,48 @@ public partial class ArmSpike : Node3D
|
||||
_anvil.QueueFree();
|
||||
_pedestal.QueueFree();
|
||||
_handTarget[1] = new Transform3D(Basis.Identity, _handRest[1]);
|
||||
_anvil = null;
|
||||
Start(Phase.Rest, "rest — palm behind a 50 kg anvil, forearm resting over it (owner playtest 2 pose)");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Phase.Rest:
|
||||
{
|
||||
// Anvil top below the shoulder; the hand first goes ABOVE the far side, the anvil
|
||||
// appears, then the hand sinks behind it so the forearm drapes over the top edge.
|
||||
const float anvilY = 1.25f;
|
||||
if (_anvil == null && _phaseT > 0.6f)
|
||||
{
|
||||
_pedestal = new StaticBody3D { CollisionMask = 0b11101, Position = new Vector3(0.22f, anvilY - 0.13f, -0.30f) };
|
||||
_pedestal.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.4f, 0.1f, 0.4f) } });
|
||||
AddChild(_pedestal);
|
||||
_anvil = new RigidBody3D { Mass = 50f, Position = new Vector3(0.22f, anvilY, -0.30f), CollisionMask = 0b11101 };
|
||||
_anvil.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = AnvilSize } });
|
||||
AddChild(_anvil);
|
||||
}
|
||||
float sink = Mathf.Clamp((_phaseT - 1f) / 0.8f, 0f, 1f);
|
||||
Basis down = new Basis(Vector3.Right, Mathf.DegToRad(-20f));
|
||||
_handTarget[1] = new Transform3D(down, new Vector3(0.22f, 1.45f - 0.17f * sink, -0.47f));
|
||||
if (_anvil != null && _phaseT > 1.8f)
|
||||
{
|
||||
RigidBody3D fore = _arms.Forearm(1);
|
||||
Vector3 axis = fore.GlobalTransform.Basis.Y * (_arms.ForeLen * 0.5f - 0.02f);
|
||||
// Collider overlap ≈ forearm center line depth inside the anvil grown by the
|
||||
// forearm half-thickness (3 cm).
|
||||
_restBodyPen = Mathf.Max(_restBodyPen, LinePenetration(fore.GlobalPosition - axis, fore.GlobalPosition + axis, _anvil, 0.03f));
|
||||
Vector3 wrist = _hands[1].GlobalPosition + _hands[1].GlobalTransform.Basis * new Vector3(0f, 0f, 0.06f);
|
||||
_restDrawnPen = Mathf.Max(_restDrawnPen, LinePenetration(_arms.Elbow(1), wrist, _anvil));
|
||||
_restAnvilMove = Mathf.Max(_restAnvilMove, _anvil.GlobalPosition.DistanceTo(new Vector3(0.22f, anvilY, -0.30f)));
|
||||
}
|
||||
if (_phaseT > 3.5f)
|
||||
{
|
||||
GD.Print($"ARM: rest metrics: forearm COLLIDER overlap max {_restBodyPen:F3} m, DRAWN forearm penetration max {_restDrawnPen:F3} m, anvil moved {_restAnvilMove:F3} m, hand {_hands[1].GlobalPosition} elbow {_arms.Elbow(1)}");
|
||||
Record($"rest: forearm collider stays out of the anvil (overlap {_restBodyPen:F3} m, limit {RestBodyPenLimit})", _restBodyPen < RestBodyPenLimit);
|
||||
Record($"rest: drawn forearm stays out of the anvil (penetration {_restDrawnPen:F3} m, limit {HoldPenetrationLimit})", _restDrawnPen < HoldPenetrationLimit);
|
||||
_anvil.QueueFree();
|
||||
_pedestal.QueueFree();
|
||||
_handTarget[1] = new Transform3D(Basis.Identity, _handRest[1]);
|
||||
Start(Phase.Track, "track — hands circle, segments must stay connected, chest undisturbed");
|
||||
}
|
||||
break;
|
||||
@@ -298,12 +383,13 @@ public partial class ArmSpike : Node3D
|
||||
_pinPrevY[side] = c.Y;
|
||||
Vector3 foreTarget = (_arms.ElbowTarget(side) + _arms.WristTarget(side)) * 0.5f;
|
||||
_maxPinObstruction = Mathf.Max(_maxPinObstruction, _arms.Forearm(side).GlobalPosition.DistanceTo(foreTarget));
|
||||
_maxPinHandLag = Mathf.Max(_maxPinHandLag, _hands[side].GlobalPosition.DistanceTo(_handTarget[side].Origin));
|
||||
}
|
||||
}
|
||||
if (_phaseT > 3.5f)
|
||||
{
|
||||
Record($"forearm blocked by bar, no tunneling (deviation max {_maxPinObstruction:F3} m, limit > {PinObstructionMin}; tunneled {_pinTunneled})",
|
||||
_maxPinObstruction > PinObstructionMin && !_pinTunneled);
|
||||
Record($"forearm blocked by bar holds the hand back, no tunneling (hand lag max {_maxPinHandLag:F3} m, limit > {PinHandLagMin}; forearm deviation {_maxPinObstruction:F3} m; tunneled {_pinTunneled})",
|
||||
_maxPinHandLag > PinHandLagMin && !_pinTunneled);
|
||||
Record($"pinned under bar: finite and quiet in the last 1 s (segment |v| max {_maxPinSpeed:F3} m/s, limit {PinSpeedLimit})", _pinFinite && _maxPinSpeed < PinSpeedLimit);
|
||||
_slab.QueueFree();
|
||||
for (int side = 0; side < 2; side++)
|
||||
@@ -343,9 +429,9 @@ public partial class ArmSpike : Node3D
|
||||
}
|
||||
|
||||
// Deepest point of the segment a→b inside the box body (0 if it never enters).
|
||||
private static float LinePenetration(Vector3 a, Vector3 b, RigidBody3D box)
|
||||
private static float LinePenetration(Vector3 a, Vector3 b, RigidBody3D box, float grow = 0f)
|
||||
{
|
||||
Vector3 half = AnvilSize * 0.5f;
|
||||
Vector3 half = AnvilSize * 0.5f + Vector3.One * grow;
|
||||
Transform3D inv = box.GlobalTransform.AffineInverse();
|
||||
float deepest = 0f;
|
||||
for (int i = 0; i <= 40; i++)
|
||||
|
||||
Reference in new issue
Block a user