Arms playtest-1 fixes: open-loop torso lean (chest hunting), hand-driven elbow pole + height cap, held-prop arm collision exceptions

- Lean computed from PhysicsTorso.NominalTarget/TargetBasis, not the body's own shoulders:
  the closed loop made the chest hunt (spike: |v| 1.1 -> 0.019 m/s, lean range 2.7 cm -> 0.9 mm)
- Elbow on its IK circle, 75% hand-axis (forearm continues behind the wrist), bounded-rate
  rotation smoothing (no flips), capped at max(shoulder, wrist) + 5 cm
- PhysicsArms.SetHeld: held prop ignores that side's arm segments (PropGrabber calls it)
- ArmSpike: stretch settle, hug elbows out (0.17 m vs 0.02 m old pole), held-anvil stability;
  all six spikes PASS x3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 00:25:50 +03:00
1 parent 7589b13aba
commit 464cda2f71
6 files changed
+288 -65

No files matched your search

+126 -2
View File
@@ -5,9 +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, Track, Swing, Push, Pin, Teleport, NanGuard, Done }
private enum Phase { Settle, Stretch, Hug, Hold, 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 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);
private const float SwingGapLimit = 0.12f;
private const float ChestDisturbLimit = 0.01f;
private const float PushMinDisplacement = 0.05f;
@@ -34,6 +41,15 @@ public partial class ArmSpike : Node3D
private float _maxChestErr;
private float _minHandBoxDist;
private float _maxPinSpeed;
private StaticBody3D _pedestal;
private RigidBody3D _anvil;
private ConeTwistJoint3D _holdWeld;
private float _maxHoldPenetration;
private bool _holdFinite = true;
private float _maxStretchChestSpeed;
private Vector3 _stretchOffsetMin = Vector3.One * 9f;
private Vector3 _stretchOffsetMax = -Vector3.One * 9f;
private readonly RandomNumberGenerator _rng = new() { Seed = 42 };
private float _maxPinObstruction;
private readonly float[] _pinPrevY = { float.MinValue, float.MinValue };
private bool _pinTunneled;
@@ -89,9 +105,102 @@ public partial class ArmSpike : Node3D
{
case Phase.Settle:
if (_phaseT > 1f)
Start(Phase.Track, "track — hands circle, segments must stay connected, chest undisturbed");
Start(Phase.Stretch, "stretch — right hand held beyond reach with 1 mm tracker noise, chest must settle (no lean hunting)");
break;
case Phase.Stretch:
{
_handTarget[1].Origin = new Vector3(0.2f, 1.38f, -0.72f)
+ new Vector3(_rng.Randfn(), _rng.Randfn(), _rng.Randfn()) * 0.001f;
if (_phaseT > 1.5f)
{
float v = _torso.Body.LinearVelocity.Length();
_maxStretchChestSpeed = Mathf.Max(_maxStretchChestSpeed, v);
_stretchOffsetMin = _stretchOffsetMin.Min(_torso.ReachOffset);
_stretchOffsetMax = _stretchOffsetMax.Max(_torso.ReachOffset);
}
if (_phaseT > 3f)
{
float range = (_stretchOffsetMax - _stretchOffsetMin).Length();
Record($"stretch: chest settles (|v| max {_maxStretchChestSpeed:F3} m/s, limit {StretchChestSpeedLimit}; lean range {range:F4} m, limit {StretchLeanRangeLimit}; lean {_torso.ReachOffset})",
_maxStretchChestSpeed < StretchChestSpeedLimit && range < StretchLeanRangeLimit);
_handTarget[1].Origin = _handRest[1];
Start(Phase.Hug, "hug — hands wrapped inward in front of the chest, elbows must go out to the sides");
}
break;
}
case Phase.Hug:
{
// Hands pointing across each other (left points +X, right points -X), as when
// wrapping arms around someone.
_handTarget[0] = new Transform3D(Basis.LookingAt(Vector3.Right, Vector3.Up), new Vector3(-0.12f, 1.25f, -0.45f));
_handTarget[1] = new Transform3D(Basis.LookingAt(Vector3.Left, Vector3.Up), new Vector3(0.12f, 1.25f, -0.45f));
if (_phaseT > 1.5f)
{
float outL = -_arms.Elbow(0).X - (-_arms.Shoulder(0).X);
float outR = _arms.Elbow(1).X - _arms.Shoulder(1).X;
float outMin = Mathf.Min(outL, outR);
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]);
_maxHoldPenetration = 0f;
Start(Phase.Hold, "hold — right hand grips a 50 kg anvil from the top, forearm must stay out of it");
}
break;
}
case Phase.Hold:
{
// Far half of the top, fingers pointing forward and steeply down.
Basis down = new Basis(Vector3.Right, Mathf.DegToRad(-HoldPitchDeg));
_handTarget[1] = new Transform3D(down, new Vector3(0.22f, HoldAnvilY + AnvilSize.Y * 0.5f + 0.08f, -0.44f));
if (_anvil == null && _phaseT > 0.5f)
{
// 50 kg anvil on a pedestal in front-right; the right hand grips its top, rear
// half, pointing forward and 35 deg down (the playtest screenshot pose).
_pedestal = new StaticBody3D { CollisionMask = 0b11101, Position = new Vector3(0.22f, HoldAnvilY - 0.13f, -0.40f) };
_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, HoldAnvilY, -0.40f), CollisionMask = 0b11101 };
_anvil.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = AnvilSize } });
AddChild(_anvil);
}
if (_holdWeld == null && _phaseT > 0.9f)
{
_holdWeld = new ConeTwistJoint3D
{
ExcludeNodesFromCollision = true,
Position = _hands[1].GlobalPosition,
NodeA = _hands[1].GetPath(),
NodeB = _anvil.GetPath(),
SwingSpan = 0f,
TwistSpan = 0f,
Bias = 0f,
};
AddChild(_holdWeld);
_arms.SetHeld(1, _anvil);
}
if (_phaseT > 1.3f)
{
_holdFinite &= BodyMath.IsFinite(_arms.Forearm(1)) && BodyMath.IsFinite(_hands[1]) && BodyMath.IsFinite(_anvil);
Vector3 wrist = _hands[1].GlobalPosition + _hands[1].GlobalTransform.Basis * new Vector3(0f, 0f, 0.06f);
_maxHoldPenetration = Mathf.Max(_maxHoldPenetration, LinePenetration(_arms.Elbow(1), wrist, _anvil));
}
if (_phaseT > 2.8f)
{
Record($"hold: held 50 kg anvil stable, drawn forearm stays out of it (penetration max {_maxHoldPenetration:F3} m, limit {HoldPenetrationLimit}; elbow {_arms.Elbow(1)}; finite {_holdFinite})",
_maxHoldPenetration < HoldPenetrationLimit && _holdFinite);
_arms.SetHeld(1, null);
_holdWeld.QueueFree();
_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;
}
case Phase.Track:
for (int side = 0; side < 2; side++)
{
@@ -233,6 +342,21 @@ 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)
{
Vector3 half = AnvilSize * 0.5f;
Transform3D inv = box.GlobalTransform.AffineInverse();
float deepest = 0f;
for (int i = 0; i <= 40; i++)
{
Vector3 local = inv * a.Lerp(b, i / 40f);
float depth = Mathf.Min(half.X - Mathf.Abs(local.X), Mathf.Min(half.Y - Mathf.Abs(local.Y), half.Z - Mathf.Abs(local.Z)));
deepest = Mathf.Max(deepest, depth);
}
return deepest;
}
private float MaxGap()
{
float gap = 0f;