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:
1 parent
7589b13aba
commit
464cda2f71
6 files changed
+288
-65
No files matched your search
+126
-2
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user