CitizenLocomotion.cs
using Sandbox;
using Sandbox.Citizen;
namespace PaintballBaddies;
/// <summary>Native animation inputs shared by player and AI presentation.</summary>
public sealed class CitizenLocomotion : Component
{
public CitizenAnimationHelper Animation { get; private set; }
public float VaultWeight { get; private set; }
private SkinnedModelRenderer body;
private float aimPitch;
private float previousYaw;
private bool rotationReady;
private bool positionReady;
private Vector3 previousPosition;
private float stationaryTime;
public float AimPitch => aimPitch;
public float AimYaw { get; private set; }
public float RotationSpeed => Animation?.MoveRotationSpeed ?? 0;
public void Configure(SkinnedModelRenderer renderer, CitizenAnimationHelper.HoldTypes hold)
{
body = renderer;
Animation ??= Components.Create<CitizenAnimationHelper>();
Animation.Target = body;
Animation.HoldType = hold;
Animation.IsGrounded = true;
Animation.IsWeaponLowered = false;
aimPitch = 0;
VaultWeight = 0;
rotationReady=false;
positionReady=false;stationaryTime=0;
}
public void UpdateMotion(Vector3 velocity, Vector3 wishVelocity, bool grounded,
float duck, float turnSpeed, Rotation lookYaw, float pitch, bool lowerWeapon, float delta, float bodyAimWeight=1, Vector3? rootPosition=null)
{
if (!body.IsValid() || !Animation.IsValid()) return;
if(rootPosition.HasValue)
{
var position=rootPosition.Value;
stationaryTime=positionReady && grounded && (position-previousPosition).WithZ(0).Length<.02f
? stationaryTime+delta : 0;
previousPosition=position;positionReady=true;
// Allow ordinary network interpolation gaps, but don't keep a run
// loop alive from stale replicated velocity or a blocked nav agent.
if(stationaryTime>.18f)
{
velocity=velocity.WithX(0).WithY(0);
wishVelocity=wishVelocity.WithX(0).WithY(0);
}
}
float bodyYaw=body.WorldRotation.Angles().yaw;
float measuredTurn=rotationReady&&delta>.0001f&&delta<.2f
? System.MathF.IEEERemainder(bodyYaw-previousYaw,360)/delta : 0;
previousYaw=bodyYaw;rotationReady=true;
// Rapid camera reversals can exceed 1,000 degrees/sec. Feeding that
// into Citizen's additive turn poses crosses the legs dramatically.
// Keep a restrained stepping response while the root turns freely.
// Citizen expects clockwise-positive; engine yaw increases the other
// way. Match the native controller's sign and half-rate turn response.
Animation.MoveRotationSpeed = (turnSpeed!=0 ? turnSpeed : -measuredTurn*.5f).Clamp(-180,180);
Animation.IsGrounded = grounded;
Animation.WithVelocity(velocity);
Animation.WithWishVelocity(wishVelocity);
Animation.DuckLevel = duck;
Animation.IsWeaponLowered = lowerWeapon;
aimPitch += (pitch.Clamp(-45,45)-aimPitch)*(1-System.MathF.Exp(-12*delta));
// Human aim poses have a limited twist range. Targets farther behind
// require a root/body turn, not a 180-degree spine and pelvis override.
AimYaw=System.MathF.IEEERemainder(lookYaw.Angles().yaw-bodyYaw,360).Clamp(-45,45);
Animation.WithLook((Rotation.FromYaw(bodyYaw+AimYaw)*Rotation.FromPitch(aimPitch)).Forward,1,1,bodyAimWeight);
}
public void UpdateVault(bool vaulting, float progress, float delta)
{
if (!body.IsValid()) return;
body.UseAnimGraph = true;
if (vaulting)
{
var entry = (progress/.25f).Clamp(0,1);
VaultWeight = entry*entry*(3-2*entry);
body.Set("pb_vault_phase", progress);
}
else VaultWeight *= System.MathF.Exp(-20*delta);
body.Set("pb_vault_weight", VaultWeight);
}
}