"Citizen held-item pose map: HoldType, aim, and IK hand targets"
▸ SYMPTOM
You set different HoldType values trying to move a character's held-item arm position, but the arm stays locked in the same pose regardless of holdtype. Or the arm unexpectedly tracks the camera up/down when looking around.
▸ CAUSE
Three independent systems control the citizen's arm, and they interact in a specific priority order (verified against CitizenAnimationHelper.cs in sbox-public):
-
HoldType(enum: None, Pistol, Rifle, Shotgun, HoldItem, Punch, Swing, RPG, Physgun) → sets"holdtype"animgraph param. This selects the arm pose + finger-curl blend only. There is no separate finger/grip float param. The fist comes from the holdtype:Pistol= grip-sized curl (a thin item sits loose between fingers)Swing= one-handed closed fist wrapping a handleHoldItem= flat open palmsPunch= both fists (raises the off hand too)
-
AimAngle/WithLook→ body-aim tilt is driven bySetLookDirection( "aim_body", dir, weight )(fed by the helper'sWithLook/OnUpdate), NOT by theaim_body_pitch/aim_body_yawfloats. Those floats read back the exact value you write but leave the pose unmoved, so the held arm only tilts with look pitch while you actually drive the look direction through the helper. If your held arm tracks the camera up/down, you're feeding it a pitched look direction: flatten or clamp it to horizontal. -
IkLeftHand/IkRightHand(GameObject targets) → eachOnUpdate, callsSetIk("hand_left"/"hand_right", go.WorldTransform). This OWNS the hand position: it is applied AFTER the holdtype pose evaluates, so the arm reaches the IK target regardless of what holdtype would set.
The common trap: the arm stays locked because an IK target is active and overriding the holdtype. Iterating through holdtypes won't move the arm. Only the fingers change.
▸ FIX
To place a held-item arm, move the IK target, not the holdtype:
// HoldType controls only the fist/finger pose
AnimHelper.HoldType = CitizenAnimationHelper.HoldTypes.Swing;
// The IK target controls WHERE the hand goes
IkTarget.WorldPosition = desiredHandPosition;
// Leave IK rotation as identity to keep model-grip
// offset dials valid (hand frame unchanged, model just translates)To prevent the arm from tracking the camera vertically, feed a horizontal look direction:
// Flatten the look direction - no camera pitch
AnimHelper.WithLook( WorldRotation.Forward );Additional params:
Handedness(Both, Right, Left) →"holdtype_handedness"AimBodyWeight/AimHeadWeight/AimEyesWeight→"aim_body/head/eyes_weight"(default 0)
Note: SetIk is animgraph-gated. It's inert on a direct-sequence rig. The citizen uses a full AnimGraph rig, so IK works.
▸ WHY IT WORKS
The animgraph evaluates holdtype first (setting the base arm pose and finger curl), then applies IK targets on top (overriding the arm position to reach the target). This means IK always wins for position, while holdtype only controls the grip shape. Understanding this priority (holdtype for fingers, IK for position, aim for camera-driven tilt) prevents the common debugging loop of cycling holdtypes to fix what's actually an IK problem.
- Corrected body-aim mechanism: driven by SetLookDirection, not the aim_body_pitch/yaw floats (per newer measured tip).