Utility for third-person weapon grips. Measures a gun model to compute a grip frame, computes and applies left-hand IK targets to place the left hand on long guns, exposes console commands and stores per-gun state in GunGripState.
using Sandbox;
using System;
using System.Linq;
using SWB.Shared;
namespace NZombies;
/// <summary>
/// HOW A GUN SITS IN A THIRD-PERSON HAND, READ OFF THE GUN ITSELF (2026-10-05). The user: *"at the moment players are hardly
/// holding their weapons, a lot of times the weapon is far from the player"*, then, of the pattern with the chalk's rotation
/// fixes: *"Can you try fixing it"*.
///
/// ⛔ TWO FAMILIES OF PACK, MEASURED (`nz_3p_axes`, one long gun from each of the 25 packs):
/// - the 19 CoD-format packs carry a `j_gun` inside the gun, run the barrel down model +X, and already aim within 2–13° of the
/// body's forward. They keep their bone anchor; nothing here touches them.
/// - the six TFA-format packs (Destiny, golub's lowpoly, TFA Historical, Insurgency, Poly Arms, SIMER's) have no gun bone, so
/// `ThirdPersonWeapon` anchored them by the first-person ARMS' own hand (`ValveBiped_Bip01_R_Hand`): the gun went wherever
/// those arms happened to hold it in their bind pose. Destiny's muzzle pointed back past the hip, 121° off and 28 u from the
/// hand; golub's 62° off. Their barrels run down −Y (golub's +Z) — the same packs the chalk turns 90°
/// (`WallBuyManager.PreTweaks`), for the same reason.
///
/// ⚠️ SO THESE GUNS ARE SEATED BY THEIR OWN SHAPE: the barrel from the `muzzle` attachment (SWB fires from it, and all 25 packs
/// have one), up as model +Z (`Measure`), and the grip where the arms' hand sits if it is actually on the gun, else a point on
/// the gun's own proportions. The frame is +X down the barrel and +Z up, which is how a `j_gun` at the origin is laid out, so
/// the hand-relative turn that already aims the CoD guns (`ThirdPersonWeapon.Roll`) aims these too.
///
/// ⚠️ AND THE LEFT HAND GOES ON THE GUN. No gun ever had a second hand on it: the rifle hold left the left hand out in front of
/// the belly beside the gun, turned like a handshake. It is put on the gun, in a support grip, by the body's own hand IK
/// (`ik.hand_left.*` in the Citizen graphs, set through `SetIk( "hand_left" )` with a WORLD transform — `nz_3p_ik` landed the
/// hand exactly on a test point both through `SetIk` and written raw in the body's space). Where on the gun, how it is turned,
/// and why not further forward: `Hands`.
/// </summary>
public static class GunGrip
{
// ⛔ NULLABLE-BACKED, INSTRUCTIONS §1: a static's value survives a hotload, its initialiser does not re-run.
static bool? _meshGrip, _leftHand;
/// <summary>Seat untrusted-anchor guns by their own shape. `nz_3p_grip mesh 0` puts the old arms-hand anchor back.</summary>
public static bool MeshGrip { get => _meshGrip ?? true; set => _meshGrip = value; }
/// <summary>Put the left hand on long guns. `nz_3p_grip left 0` turns it off.</summary>
public static bool LeftHand { get => _leftHand ?? true; set => _leftHand = value; }
// ⛔ NULLABLE-BACKED like the switches: where the left hand's grip sits on the gun — as fractions of the gun's depth below
// its barrel line and of its left face's offset (0 = centred under it) — and how far the palm is tipped from straight up
// toward the gun's right, in degrees. `nz_3p_palm <depth> <side> <roll>` tunes them live.
static float? _palmDepth, _palmSide, _palmRoll;
public static float PalmDepth { get => _palmDepth ?? 0.22f; set => _palmDepth = value; }
public static float PalmSide { get => _palmSide ?? 0f; set => _palmSide = value; }
public static float PalmRoll { get => _palmRoll ?? 0f; set => _palmRoll = value; }
/// <summary>The left hand's grip keeps at least this far ahead of the right hand's, so the two hands do not overlap.</summary>
const float HandGap = 5f;
/// <summary>How much of its reach the left arm is sent: just short of locking straight, so the IK never snaps.</summary>
const float ReachUsed = 0.97f;
/// <summary>Seconds a new gun's hold is left to settle, IK off, before the left hand's turn and spacing are read.</summary>
const float SettleTime = 0.5f;
/// <summary>Seconds the left hand takes to ease from where the hold had it onto the gun.</summary>
const float BlendTime = 0.2f;
/// <summary>
/// An anchor whose transform says nothing about where the gun is: none at all, or a first-person arms bone.
/// ⚠️ The knife's `tag_knife` and the CoD `j_gun`/`tag_*` bones are trusted and keep their path.
/// </summary>
public static bool Untrusted( string anchor ) => anchor is null || anchor.StartsWith( "ValveBiped", StringComparison.Ordinal );
/// <summary>The gun's shape in its own object space: where the muzzle is, which way the barrel and the top face.</summary>
public readonly record struct Shape( Vector3 Muzzle, Vector3 Barrel, Vector3 Up, float TMuzzle, float TBack, float Below, float Side )
{
/// <summary>How long the gun is, from the back end to the muzzle, along the barrel.</summary>
public float Length => MathF.Max( 1f, TMuzzle - TBack );
/// <summary>A point on the gun: `t` along the barrel (TBack…TMuzzle), `down` below the barrel line.</summary>
public Vector3 At( float t, float down ) => Muzzle + Barrel * (t - TMuzzle) - Up * down;
/// <summary>The frame of the gun: +X down the barrel, +Z up.</summary>
public Rotation Frame => Rotation.LookAt( Barrel, Up );
/// <summary>The gun's left, the side the left hand comes from. `Side` is how far its face stands off the barrel line.</summary>
public Vector3 Left => Vector3.Cross( Up, Barrel );
}
/// <summary>
/// Measure a gun mesh. Null without a `muzzle` attachment (or before the model is up), and then the caller keeps whatever
/// anchor it had, or tries again next frame.
///
/// ⚠️ CARDINAL AXES, NOT THE MUZZLE'S EXACT DIRECTION: these meshes are authored axis-aligned, and a muzzle attachment a
/// little off the barrel line would otherwise tilt the whole gun.
/// </summary>
public static Shape? Measure( SkinnedModelRenderer gun, Vector3? grip = null )
{
if ( !gun.IsValid() || gun.Model is null || gun.Model.IsError ) return null;
// ⛔ THE MUZZLE RELATIVE TO THE GUN'S OWN SCENE OBJECT (`GetAttachment( name, worldspace: false )`), NEVER A WORLD
// POSITION BROUGHT BACK THROUGH THE GAMEOBJECT'S TRANSFORM. On a gun's first frame `ThirdPersonWeapon` has just moved the
// object, and the attachment still answers for where it was made: measured that way in `Hands`, every gun's muzzle came
// out ~45 u off and its depth below the barrel 46–52 u instead of 5–9, which put the left hand's target over the
// player's head (`nz_3p_ik`, 2026-10-05). Relative to the scene object it is the same answer on any frame.
var sm = gun.SceneModel;
if ( !sm.IsValid() || sm.GetAttachment( "muzzle", false ) is not Transform ml ) return null;
var muzzle = ml.Position;
var b = gun.Model.Bounds;
var size = b.Size;
var centre = b.Center;
Vector3[] axes = { Vector3.Forward, Vector3.Left, Vector3.Up };
float Ext( int i ) => i == 0 ? size.x : i == 1 ? size.y : size.z;
// The barrel: the longest side, pointing at the muzzle.
var li = Ext( 0 ) >= Ext( 1 ) && Ext( 0 ) >= Ext( 2 ) ? 0 : Ext( 1 ) >= Ext( 2 ) ? 1 : 2;
var barrel = axes[li] * (Vector3.Dot( muzzle - centre, axes[li] ) >= 0f ? 1f : -1f);
// Up: MODEL +Z whenever the barrel is not itself along Z. Every pack here is authored Z-up — the CoD-format ones with the
// barrel down +X, the TFA-format ones down −Y — all but golub's lowpoly, whose barrel runs +Z.
// ⛔ NOT "THE SIDE THE GUN HANGS FROM", WHICH IT WAS UNTIL 2026-10-05 15:20: the MG42's drum hangs off its left, so its
// left came out as down and the left hand went onto its side. And before that NOT "THE TALLER OF THE OTHER TWO SIDES",
// the first cut, which rolled four TFA guns 90°: their bounds are wider than they are tall.
int vi;
Vector3 up;
if ( li != 2 )
{
vi = 2;
up = Vector3.Up;
}
else
{
// golub's: up is away from where the gun hangs below its barrel line — the hand on the grip when there is one, else
// the middle of the mesh, which the grip, the magazine and the stock all pull down.
Vector3 Off( Vector3 p ) { var d = p - muzzle; return d - barrel * Vector3.Dot( d, barrel ); }
var down = grip is Vector3 g && Off( g ).Length > 2f ? Off( g ) : Off( centre );
var others = new[] { 0, 1, 2 }.Where( i => i != li ).ToArray();
if ( down.Length > 0.5f )
{
vi = MathF.Abs( Vector3.Dot( down, axes[others[0]] ) ) >= MathF.Abs( Vector3.Dot( down, axes[others[1]] ) ) ? others[0] : others[1];
up = axes[vi] * (Vector3.Dot( down, axes[vi] ) > 0f ? -1f : 1f);
}
else
{
vi = Ext( others[0] ) >= Ext( others[1] ) ? others[0] : others[1];
up = axes[vi];
}
}
var tMuzzle = Vector3.Dot( muzzle, barrel );
var tBack = Vector3.Dot( centre, barrel ) - Ext( li ) * 0.5f;
var below = Vector3.Dot( muzzle, up ) - (Vector3.Dot( centre, up ) - Ext( vi ) * 0.5f);
// The left face: the bounds' far side along the gun's left, measured from the barrel line.
var left = Vector3.Cross( up, barrel );
var side = Vector3.Dot( left, centre ) + Ext( 3 - li - vi ) * 0.5f - Vector3.Dot( left, muzzle );
return new Shape( muzzle, barrel, up, tMuzzle, tBack, MathF.Max( 0f, below ), MathF.Max( 0f, side ) );
}
/// <summary>
/// The grip frame in the gun object's space: origin where the right hand closes, +X down the barrel, +Z up. Null when the
/// gun cannot be measured.
///
/// ⚠️ THE ARMS' HAND IS USED FOR THE GRIP POINT ONLY WHEN IT SITS ON THE GUN (inside its bounds). Where it does (SIMER's,
/// Poly Arms, Insurgency) it is the authored grip; Destiny's is 28 u away and its turn is the arms', never the gun's.
/// Without it: a third of the way up from the back end (a fifth of the way on a pistol), and a little under halfway down
/// from the barrel line to the bottom of the gun — where a pistol grip is on a rifle, and the grip itself on a pistol.
/// </summary>
public static Transform? Frame( SkinnedModelRenderer gun, Transform? armsHand, bool pistol )
{
if ( !gun.IsValid() || gun.Model is null ) return null;
var onGun = armsHand is Transform a && gun.Model.Bounds.Contains( a.Position, 1.5f ) ? a.Position : (Vector3?)null;
if ( Measure( gun, onGun ) is not Shape s ) return null;
var grip = onGun ?? s.At( s.TBack + s.Length * (pistol ? 0.2f : 0.33f), s.Below * 0.45f );
return new Transform( grip, s.Frame, 1f );
}
/// <summary>
/// The hold a weapon takes. A long gun tagged with the pistol hold takes the rifle's (a shotgun the shotgun's), by its manifest
/// class.
///
/// ⛔ MEASURED (`nz_3p`): the Black Ops III ICR-1, the CODOL-XGG AK117, the Insurgency M16A4, the TFA Historical Type 1927 and
/// the MW Custom SIG MPX all came through as `Pistol`, so a rifle was held one-handed, up at the face and pointing sideways.
/// The prefabs carry it; the manifest's class is the one source of truth the whole game already uses (the browser, the class
/// rules).
/// </summary>
public static HoldTypes HoldFor( SWB.Base.Weapon wep, HoldTypes hold )
{
if ( hold != HoldTypes.Pistol || !wep.IsValid() ) return hold;
var category = WeaponLibrary.Find( Rarity.PrefabOf( wep ) )?.Category;
return category switch
{
null or "" or "Handguns" or "Revolvers" => hold,
"Shotguns" => HoldTypes.Shotgun,
_ => HoldTypes.Rifle,
};
}
/// <summary>Is this hold a two-handed long gun, which gets the left hand on it?</summary>
public static bool LongGun( HoldTypes hold ) => hold is HoldTypes.Rifle or HoldTypes.Shotgun;
/// <summary>
/// The left hand onto a long gun, every frame, after the gun is placed. `knife` (a knife swing or a reload is showing) and
/// anything that is not a long gun take it off.
///
/// ⛔ NOT ON THE HANDGUARD: THE ARM DOES NOT REACH IT (measured, `nz_3p_ik`, 2026-10-05). The Citizen's left arm reaches
/// 20.2 u from its shoulder joint (10.5 upper arm + 9.7 forearm). The rifle hold puts the right hand at the right hip and the
/// left palm 13.3 u from it, ~30° ACROSS the body — the animation's own gun points that way — while every gun here points
/// straight ahead from the right hand, so its handguard is 23–34 u from the left shoulder. The first cut sent the hand two
/// thirds of the way up the gun, and the arm stretched for it.
///
/// ⚠️ SO: THE GRIP AT THE HOLD'S OWN SPACING FROM THE RIGHT HAND, SLID BACK TOWARD IT UNTIL THE SHOULDER REACHES — worked out
/// live every frame, since the shoulder moves with the aim and the walk. On a straight gun that is the magazine well, with
/// the arm nearly straight: as straight as the hold already keeps it (its own palm sits at 99% of the reach). Where nothing on
/// the gun is in reach, the hand goes as near as it can.
///
/// ⚠️ TURNED INTO A SUPPORT GRIP, NOT THE HOLD'S OWN TURN. The hand's own axes (its finger bones, `nz_3p_ik`): fingers +X,
/// curled 90° toward +Z (the palm), thumb +Y. The hold turns it like a handshake beside the gun, and kept that way on the gun
/// it read as presenting the gun, not holding it. Here the palm is up under the gun, the fingers across its underside curling
/// up its right side, the thumb forward along its left — and what goes on the gun is the middle of that curl (halfway from
/// the middle finger's root to its middle joint), not the wrist, which is where the IK's bone is.
/// ⚠️ EASED BOTH WAYS: on a new gun the hold settles first with the IK off (`SettleTime`), then the hand eases from where the
/// hold had it onto the gun (`BlendTime`) — and again after a reload or a knife swing has had it off.
/// </summary>
public static void Hands( SkinnedModelRenderer body, GameObject gun, SkinnedModelRenderer gunRenderer, HoldTypes hold, bool knife,
Transform? seat = null )
{
if ( !body.IsValid() ) return;
if ( Probe is Vector3 probe )
{
if ( gun.IsValid() && gun.Components.Get<GunGripState>( FindMode.EverythingInSelf ) is { } probed ) probed.Target = null;
ApplyProbe( body, probe );
return;
}
if ( !LeftHand || knife || !LongGun( hold ) || !gun.IsValid() || !gunRenderer.IsValid() )
{
// Off — and eased back on afterwards (a reload, a knife swing), rather than snapped.
if ( gun.IsValid() && gun.Components.Get<GunGripState>( FindMode.EverythingInSelf ) is { } off )
{
off.Target = null;
off.Blend = 0f;
}
Clear( body );
return;
}
var st = gun.Components.Get<GunGripState>( FindMode.EverythingInSelf ) ?? gun.Components.Create<GunGripState>();
st.Target = null;
// ⚠️ MEASURED ONCE PER GUN, at the first call that can — the gun object is new for every gun, so this is a fresh state.
// With the seat's grip as the hint, so the left hand finds the same top of the gun the right hand was seated by.
if ( st.Shape is null )
{
st.Shape = Measure( gunRenderer, seat?.Position );
if ( st.Shape is null )
{
Clear( body );
return;
}
st.Settle = 0f;
}
var s = st.Shape.Value;
var g = gun.WorldTransform;
var frame = g.Rotation * s.Frame;
if ( st.LeftTurn is null )
{
// ⚠️ HALF A SECOND WITH THE IK OFF FIRST, so the bones read here are the settled hold's — not a blend still on its way
// from the last gun's pose: read two frames in, the hold's hand spacing came out anywhere from 5 to 21 u (`nz_3p_ik`).
if ( st.Settle < SettleTime
|| !Bone( body, "hand_L", out var hand ) || !Bone( body, "arm_lower_L", out var elbow )
|| !Bone( body, "arm_upper_L", out var upper ) )
{
Clear( body );
return;
}
st.LeftTurn = frame.Inverse * hand.Rotation;
st.Reach = upper.Position.Distance( elbow.Position ) + elbow.Position.Distance( hand.Position );
st.Spacing = Bone( body, "hold_R", out var right ) && Bone( body, "hold_L", out var palm )
? right.Position.Distance( palm.Position )
: 12f;
st.Grip = Bone( body, "finger_middle_meta_L", out var root ) && Bone( body, "finger_middle_1_L", out var joint )
? hand.Rotation.Inverse * ((root.Position + joint.Position) * 0.5f - hand.Position)
: new Vector3( 2.7f, 0.3f, 1.1f );
st.Rest = g.PointToLocal( hand.Position );
st.Blend = 0f;
}
// The right hand's place along the barrel, and the shoulder the left arm swings from, this frame.
if ( !Bone( body, "hold_R", out var grip ) || !Bone( body, "arm_upper_L", out var shoulder ) )
{
Clear( body );
return;
}
var scale = MathF.Max( 0.01f, gun.WorldScale.x );
var tGrip = Vector3.Dot( g.PointToLocal( grip.Position ), s.Barrel );
var tMin = tGrip + HandGap / scale;
var tMax = s.TMuzzle - s.Length * 0.15f;
if ( tMax < tMin )
{
Clear( body );
return;
}
// The support grip's turn, in the gun's frame: fingers to its right, palm up (`PalmRoll` tips it toward the right), so the
// thumb runs forward.
var roll = PalmRoll * MathF.PI / 180f;
var turn = Rotation.LookAt( new Vector3( 0f, -MathF.Cos( roll ), -MathF.Sin( roll ) ),
new Vector3( 0f, -MathF.Sin( roll ), MathF.Cos( roll ) ) );
var held = frame * turn;
// The grip's line along the gun: under the barrel line by a fraction of the gun's depth, and the wrist behind it.
var down = s.Below * PalmDepth;
var side = s.Left * (s.Side * PalmSide);
var lift = held * st.Grip;
Vector3 Wrist( float t ) => g.PointToWorld( s.At( t, down ) + side ) - lift;
// Where on that line the shoulder reaches: |Wrist( tGrip ) − shoulder + d·u| = r, a quadratic in u = t − tGrip.
var c = Wrist( tGrip ) - shoulder.Position;
var d = g.Rotation * s.Barrel * scale;
var r = st.Reach * ReachUsed;
var qa = d.LengthSquared;
var qb = 2f * Vector3.Dot( c, d );
var disc = qb * qb - 4f * qa * (c.LengthSquared - r * r);
var t = Math.Clamp( tGrip - qb / (2f * qa), tMin, tMax );
if ( disc >= 0f )
{
var root = MathF.Sqrt( disc );
var lo = MathF.Max( tMin, tGrip + (-qb - root) / (2f * qa) );
var hi = MathF.Min( tMax, tGrip + (-qb + root) / (2f * qa) );
if ( lo <= hi ) t = Math.Clamp( tGrip + st.Spacing / scale, lo, hi );
}
// Eased, so the hand slides rather than jumps when the reach changes under it (a step, a turn of the aim).
st.T = st.T is float was ? was + (t - was) * (1f - MathF.Exp( -Time.Delta * 14f )) : t;
st.TGrip = tGrip;
// And eased onto the gun from where the hold had the hand, rather than popping there when the IK comes on.
st.Blend = MathF.Min( 1f, st.Blend + Time.Delta / BlendTime );
var k = st.Blend * st.Blend * (3f - 2f * st.Blend);
var target = new Transform( Vector3.Lerp( g.PointToWorld( st.Rest ), Wrist( st.T.Value ), k ),
Rotation.Slerp( frame * st.LeftTurn.Value, held, k ) );
body.SetIk( "hand_left", target );
st.Body = body;
st.Target = target;
}
/// <summary>A body bone by either spelling: the right hand answers to `hand_R` and `hand_r` on these bodies (`nz_3p`).</summary>
static bool Bone( SkinnedModelRenderer body, string name, out Transform tx )
=> body.TryGetBoneTransform( name, out tx ) || body.TryGetBoneTransform( name.ToLowerInvariant(), out tx );
/// <summary>The left hand's IK target on this gun this frame, or null while the hand is off it (`nz_3p_axes`).</summary>
public static Transform? LeftTarget( GameObject gun )
=> gun.IsValid() ? gun.Components.Get<GunGripState>( FindMode.EverythingInSelf )?.Target : null;
/// <summary>One line on the left hand's state on this gun, for `nz_3p_ik`.</summary>
public static string Describe( GameObject gun )
{
var st = gun.IsValid() ? gun.Components.Get<GunGripState>( FindMode.EverythingInSelf ) : null;
if ( st is null ) return "no GunGripState on the gun";
if ( st.LeftTurn is null ) return $"the hold is settling ({(float)st.Settle:0.00} s of {SettleTime} s) — the hand is not on the gun yet";
return $"grip at t {st.T:0.0} (right hand at {st.TGrip:0.0}, {(st.T ?? st.TGrip) - st.TGrip:0.0} u ahead)"
+ $" · arm reach {st.Reach:0.0} u · the hold's hand spacing {st.Spacing:0.0} u"
+ $" · grip point {st.Grip.x:0.0},{st.Grip.y:0.0},{st.Grip.z:0.0} in the hand"
+ (st.Target is null ? " · ⛔ IK off this frame" : "");
}
/// <summary>Take the left hand off whatever it was on.</summary>
public static void Clear( SkinnedModelRenderer body )
{
if ( !body.IsValid() ) return;
if ( Probe is Vector3 probe ) { ApplyProbe( body, probe ); return; }
body.ClearIk( "hand_left" );
}
/// <summary>
/// A test point for the left hand in the body's own frame, forward/left/up from its origin (`nz_3p_ik`, a dev probe): while
/// it is set, `Hands` and `Clear` put the hand there and nowhere else. `ProbeRaw` writes the graph's three parameters as
/// they are instead of through `SetIk`, which is how the two answers about the graph's space were told apart.
/// </summary>
public static Vector3? Probe { get; set; }
public static bool ProbeRaw { get; set; }
static void ApplyProbe( SkinnedModelRenderer body, Vector3 p )
{
if ( ProbeRaw )
{
body.Set( "ik.hand_left.enabled", true );
body.Set( "ik.hand_left.position", p );
body.Set( "ik.hand_left.rotation", Rotation.Identity );
return;
}
body.SetIk( "hand_left", body.GameObject.WorldTransform.ToWorld( new Transform( p, Rotation.Identity, 1f ) ) );
}
/// <summary>`nz_3p_palm [depth] [side] [roll]` — where the left hand grips a long gun, live (see `PalmDepth`).</summary>
[ConCmd( "nz_3p_palm" )]
public static void PalmCmd( float depth = -1f, float side = float.NaN, float roll = float.NaN )
{
if ( depth >= 0f ) PalmDepth = depth;
if ( !float.IsNaN( side ) ) PalmSide = side;
if ( !float.IsNaN( roll ) ) PalmRoll = roll;
Log.Info( $"[nz-3p] palm: {PalmDepth:0.00} of the way down from the barrel line to the gun's bottom, {PalmSide:0.00} of the way"
+ $" out to its left face, tipped {PalmRoll:0}° toward its right (nz_3p_palm <depth> <side> <roll>)" );
}
/// <summary>`nz_3p_grip [mesh 0|1] [left 0|1]` — the two switches, and what they are.</summary>
[ConCmd( "nz_3p_grip" )]
public static void Cmd( string a = "", int av = -1, string b = "", int bv = -1 )
{
void Set( string k, int v )
{
if ( v < 0 ) return;
if ( k == "mesh" ) MeshGrip = v != 0;
else if ( k == "left" ) LeftHand = v != 0;
}
Set( (a ?? "").ToLowerInvariant(), av );
Set( (b ?? "").ToLowerInvariant(), bv );
Log.Info( $"[nz-3p] grip: TFA-format guns seated by their own shape {(MeshGrip ? "ON" : "off")}"
+ $" · left hand on long guns {(LeftHand ? "ON" : "off")}"
+ " (nz_3p_grip mesh 0|1 left 0|1; a gun picks up a change when it is next drawn)" );
}
}
/// <summary>
/// What `GunGrip.Hands` keeps for one gun: its measured shape and the left hand's turn and grip against it. On the third-person
/// gun object, which is made new for every gun and never saved or networked, so the state cannot outlive its gun.
/// </summary>
public sealed class GunGripState : Component
{
/// <summary>`ThirdPersonWeapon` has seated this gun by its shape (or given up: `FrameTries` ran out with no muzzle).</summary>
public bool FrameDone { get; set; }
public int FrameTries { get; set; }
public GunGrip.Shape? Shape { get; set; }
/// <summary>The hold's own turn of the left hand against the gun's frame, read once it settled — where the hand eases from.</summary>
public Rotation? LeftTurn { get; set; }
/// <summary>The middle of the left hand's grip, in the hand's own frame: the point that is put on the gun.</summary>
public Vector3 Grip { get; set; }
/// <summary>The left arm's reach (upper arm + forearm) and the hold's own right-to-left hand spacing, read with the turn.</summary>
public float Reach { get; set; }
public float Spacing { get; set; }
/// <summary>How long the gun has been measured (the hold settles before the turn is read), where the hold had the wrist then
/// (in the gun's space), and how far the hand has eased from there onto the gun (0…1).</summary>
public TimeSince Settle { get; set; }
public Vector3 Rest { get; set; }
public float Blend { get; set; }
/// <summary>Where along the barrel the grip is (eased), and where the right hand is, this frame.</summary>
public float? T { get; set; }
public float TGrip { get; set; }
/// <summary>The body and the left hand's target this frame, set by `GunGrip.Hands`; null when the hand is off the gun.</summary>
public SkinnedModelRenderer Body { get; set; }
public Transform? Target { get; set; }
protected override void OnDestroy()
{
if ( Body.IsValid() ) Body.ClearIk( "hand_left" );
}
}