Code/Runtime/RetargetedWeaponController.cs
#nullable enable
using Sandbox;
using HumanoidHandRetargeter.Target;
namespace HumanoidHandRetargeter;
/// <summary>Optional local player controls for exported weapons. Games may instead drive RetargetedWeapon directly.</summary>
[Title("Retargeted Weapon Controls"), Category("Animation")]
public sealed class RetargetedWeaponController : Component, ICameraModifier
{
[Property] public PlayerController? Player { get; set; }
[Property] public bool ReadInput { get; set; } = true;
[Property] public int MagazineSize { get; set; } = 30;
[Property] public int StartingReserve { get; set; } = 120;
[Property] public float RoundsPerMinute { get; set; } = 800;
[Property] public float ReloadSeconds { get; set; } = 2.7f;
[Property] public float EmptyReloadSeconds { get; set; } = 3.2f;
[Property] public float Damage { get; set; } = 20;
[Property] public float Range { get; set; } = 10000;
[Property] public float HipFov { get; set; } = 75;
[Property] public float AimFov { get; set; } = 55;
[Property] public string WorldGripBone { get; set; } = "hand_R";
[Property] public int BodyHoldType { get; set; } = 2;
[Property] public int BodyHandedness { get; set; }
public int Ammo => magazine?.Rounds ?? MagazineSize;
public int Reserve => magazine?.Reserve ?? StartingReserve;
public bool Reloading => magazine?.Reloading ?? false;
public bool Aiming { get; private set; }
public int ShotsFired { get; private set; }
public int Hits { get; private set; }
public global::Vector3 LastHit { get; private set; }
public Action<SceneTraceResult>? Shot { get; set; }
private WeaponMagazine? magazine;
private RetargetedWeapon? weapon;
private float nextShot, reloadEnds, currentFov;
public int CameraOrder => 200;
protected override void OnStart()
{
weapon=GetComponent<RetargetedWeapon>();
Player ??= Scene.GetAllComponents<PlayerController>().FirstOrDefault();
if(Player is not null)Player.HideBodyInFirstPerson=true;
magazine=new(MagazineSize,StartingReserve);
currentFov=HipFov;
weapon?.Deploy(true);
}
public bool Fire()
{
if(weapon is null||magazine is null||Player is null||Time.Now<nextShot||Reloading)return false;
nextShot=Time.Now+60/Math.Max(1,RoundsPerMinute);
if(!magazine.TryFire()){weapon.DryFire();return false;}
weapon.Attack();weapon.SetEmpty(Ammo==0);ShotsFired++;
UpdateBodyPose();
Player.Renderer?.Set("b_attack",true);
var eye=Player.EyeTransform;
var hit=Scene.Trace.Ray(eye.Position,eye.Position+eye.Rotation.Forward*Range)
.IgnoreGameObjectHierarchy(Player.GameObject).IgnoreGameObjectHierarchy(GameObject).UseHitboxes().Run();
if(hit.Hit)
{
Hits++;LastHit=hit.HitPosition;
var info=new DamageInfo{Damage=Damage,Attacker=Player.GameObject,Weapon=GameObject,Position=hit.HitPosition,Origin=eye.Position};
for(var obj=hit.GameObject;obj is not null;obj=obj.Parent)
if(obj.GetComponent<IDamageable>() is {} damageable){damageable.OnDamage(info);break;}
}
Shot?.Invoke(hit);
return true;
}
public bool Reload()
{
if(magazine is null||weapon is null||!magazine.BeginReload())return false;
weapon.Reload(Ammo==0);reloadEnds=Time.Now+(Ammo==0?EmptyReloadSeconds:ReloadSeconds);
UpdateBodyPose();
Player?.Renderer?.Set("b_reload",true);
return true;
}
public void Aim(bool value){Aiming=value&&!Reloading;if(weapon is not null)weapon.Aiming=Aiming;}
protected override void OnUpdate()
{
if(weapon is null||Player is null||magazine is null)return;
if(Reloading&&Time.Now>=reloadEnds){magazine.FinishReload();weapon.SetEmpty(Ammo==0);}
if(ReadInput)Aim(Input.Down("Attack2"));
if(Reloading)Aim(false);
weapon.Aiming=Aiming;
weapon.Sprinting=ReadInput&&Input.Down("Run")&&Player.Velocity.Length>10&&!Aiming;
weapon.Movement=Math.Clamp(Player.Velocity.Length/150f,0,1);
weapon.SetParameter("attack_hold",ReadInput&&Input.Down("Attack1")&&!Reloading&&Ammo>0?1f:0f);
weapon.SetParameter("firing_mode",3);
weapon.SetParameter("ironsights_fire_scale",.3f);
if(ReadInput)
{
if(Input.Pressed("Reload"))Reload();
if(Input.Down("Attack1")&&!weapon.Sprinting)Fire();
}
weapon.ShowHands=!Player.ThirdPerson;
if(Player.ThirdPerson)weapon.WorldTransform=Player.EyeTransform;
// Keep the body graph warm even when hidden, so a camera switch during a shot
// or reload reveals the same action rather than restarting a holding pose.
UpdateBodyPose();
weapon.ThirdPersonBody=Player.ThirdPerson?Player.Renderer:null;
weapon.WorldGripBone=WorldGripBone;
}
private void UpdateBodyPose()
{
if(Player?.Renderer is not {} body)return;
body.Set("holdtype",BodyHoldType);body.Set("holdtype_handedness",BodyHandedness);
body.Set("aim_body_weight",1f);
}
public void ModifyCamera(CameraComponent camera,ref CameraView view)
{
if(Player is not null&&!Player.ThirdPerson&&camera==Scene.Camera)
{
currentFov=MathX.Lerp(currentFov,Aiming?AimFov:HipFov,Math.Clamp(Time.Delta*12,0,1));
view.FieldOfView=currentFov;
}
}
public void PostCameraSetup(CameraComponent camera,in CameraView view)
{
// EyeTransform during Update can precede the player's final camera pose.
// Place both viewmodel meshes against the composed view before PreRender.
if(weapon is not null&&Player is not null&&!Player.ThirdPerson&&camera==Scene.Camera)
weapon.WorldTransform=new global::Transform(view.Position,view.Rotation);
}
protected override void OnDisabled()
{
magazine?.CancelReload();Aiming=false;
if(weapon is not null){weapon.Aiming=false;weapon.ThirdPersonBody=null;}
Player?.Renderer?.Set("holdtype",0);
}
}