PlayerPawn visual and effect logic. Handles animation helper updates, weapon animation flags, vfx RPCs for shooting, death/gib spawning, blood and sounds, and reacts to damage and healing game events.
using KOTH.UI;
using Sandbox;
using Sandbox.Citizen;
using Sandbox.Diagnostics;
using Sandbox.Events;
using System.Numerics;
namespace KOTH;
public partial class PlayerPawn :
IGameEventHandler<DamageBroadcastEvent>,
IGameEventHandler<HealingBroadcastEvent>
{
private void TickVFXs()
{
if (!IsAlive /*HACK for proxy characters when body dies b4 health knows*/|| !Body.IsValid())
{
return;
}
// Assert.True(Body.IsValid());
Assert.True(AnimationHelper.IsValid());
Body.WorldRotation = Rotation.FromYaw(EyeAngles.yaw);
AnimationHelper.WithVelocity(CharacterController.Velocity);
AnimationHelper.WithWishVelocity(WishVelocity);
AnimationHelper.WithLook(EyeAngles.Forward, 1, 1, 1.0f);
AnimationHelper.IsGrounded = IsGrounded;
AnimationHelper.DuckLevel = IsCrouching ? .5f : 0;
AnimationHelper.MoveStyle = CitizenAnimationHelper.MoveStyles.Run;
AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.Shotgun;
AnimationHelper.Handedness = CitizenAnimationHelper.Hand.Both;
AnimationHelper.IsWeaponLowered = false;
AnimationHelper.AimBodyWeight = 1f;
TickWeaponAnimations();
if (CurrentEquipment.IsValid())
{
AnimationHelper.HoldType = CurrentEquipment.HoldType;
AnimationHelper.Handedness = CurrentEquipment.Handedness;
}
else
{
AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.None;
}
if (Networking.IsHost)
{
CheckWalkSound(CharacterController.Velocity.WithZ(0).Length);
CheckUber(DamageComponent.IsUbered);
}
}
const float ShotCooldown = 0.33f;
private bool ShowShoot = false;
private void TickWeaponAnimations()
{
if (!CurrentEquipment.IsValid())
{
return;
}
if (TimeSinceLastShot < ShotCooldown && !ShowShoot)
{
Body.ModelRenderer.Set("b_attack", true);
CurrentEquipment.ViewModel?.ModelRenderer.Set("b_attack", true);
ShowShoot = true;
}
bool ShowReload = false;
if (CurrentEquipment.GetWeaponComponent() is InputWeaponComponent WeaponComp)
{
ShowReload = WeaponComp.IsReloading && TimeSinceLastShot > ShotCooldown;
Body.ModelRenderer.Set("b_reload", ShowReload);
}
bool ShowSprint = WishMove == Vector3.Forward && TimeSinceLastShot > 1f;
if (CurrentEquipment.ViewModel.IsValid())
{
CurrentEquipment.ViewModel.ModelRenderer.Set("b_reload", ShowReload);
CurrentEquipment.ViewModel.ModelRenderer.Set("b_sprint", ShowSprint);
CurrentEquipment.ViewModel.ModelRenderer.Set("move_bob", IsGrounded ? 0f : 1f);
}
Body.ModelRenderer.Set("b_sprint", ShowSprint);
}
TimeSince TimeSinceLastShot { get; set; } = 0;
[Rpc.Broadcast]
public void VFXOnShoot()
{
TimeSinceLastShot = 0;
ShowShoot = false;
}
/////////////////////////////////////////////////////////////////////////////////
private GameObject GetClosestPlayerGameobject()
{
// TODO : this would be fun but its causing issues with base look
// var ClosestPlayerObject = GetClosestPlayerGameobject();
//if (ClosestPlayerObject != null)
//{
// AnimationHelper.LookAtEnabled = true;
// AnimationHelper.LookAt = ClosestPlayerObject;
//}
//else
//{
// AnimationHelper.LookAtEnabled = false;
//}
GameObject ClosestPlayerObject = null;
foreach (var PlayerState in GameNetworkManager.PlayerStates)
{
if (!PlayerState.PlayerPawn.IsValid() || !PlayerState.PlayerPawn.IsAlive || PlayerState == PlayerState.Local)
{
continue;
}
var Distance = Boom.WorldPosition.Distance(PlayerState.PlayerPawn.Boom.WorldPosition);
if (ClosestPlayerObject == null && Distance < 500f)
{
ClosestPlayerObject = PlayerState.PlayerPawn.Boom;
}
else if (ClosestPlayerObject.IsValid())
{
if (Boom.WorldPosition.Distance(ClosestPlayerObject.WorldPosition) > Distance)
{
ClosestPlayerObject = PlayerState.PlayerPawn.Boom;
}
}
}
return ClosestPlayerObject;
}
/////////////////////////////////////////////////////////////////////////////////
bool UberApplied = false;
[Rpc.Broadcast]
private void CheckUber(bool Ubered)
{
if (UberApplied && !Ubered)
{
Body.ModelRenderer.ClearMaterialOverrides();
CurrentEquipment?.ModelRenderer.ClearMaterialOverrides();
UberApplied = false;
}
if (!UberApplied && Ubered)
{
Body.ModelRenderer.SetMaterial(UberMaterial);
CurrentEquipment?.ModelRenderer.SetMaterial(UberMaterial);
UberApplied = true;
}
}
/////////////////////////////////////////////////////////////////////////////////
[Property] public GameObject DeathBlood { get; set; }
const float GibMinDamage = 50f; // TODO : should be based on last hp!
const float GibForce = 66f;
[Rpc.Broadcast(NetFlags.HostOnly)]
private void BroadcastLocalPlayerDeath(FDamageTaken DamageTaken)
{
if (!Body.IsValid())
{
return;
}
if (DeathBlood.IsValid())
{
DeathBlood.Clone(CenterPosition);
}
if (DamageTaken.Damage > GibMinDamage)
{
CreateGibs(DamageTaken);
Body.Destroy();
}
else
{
Body.Ragdoll(DamageTaken);
Body.GameObject.SetParent(null, true);
}
}
private void CreateGibs(FDamageTaken DamageTaken)
{
if (GibPrefab.IsValid())
{
var Gibs = GibPrefab.Clone(WorldPosition);
foreach (var ChildGib in Gibs.Root.Children)
{
var Rigidbody = ChildGib.Components.Get<Rigidbody>();
if (Rigidbody.IsValid())
{
// TODO : explode more outward
Rigidbody.Velocity = (CenterPosition - DamageTaken.DamageLocation) * GibForce;
}
}
Gibs.NetworkSpawn();
}
}
/////////////////////////////////////////////////////////////////////////////////
[Property] public GameObject BloodSquirt { get; set; }
[Property] public SoundEvent HitSound { get; set; }
[Property] public SoundEvent WalkSound { get; set; }
public SoundHandle WalkSoundHandle = null;
[Rpc.Broadcast(NetFlags.HostOnly)]
void CheckWalkSound(float Velocity)
{
if (!WalkSound.IsValid())
{
return;
}
if (!IsGrounded && WalkSoundHandle.IsValid())
{
WalkSoundHandle.Stop();
WalkSoundHandle = null;
}
if (Velocity > 100)
{
if (WalkSoundHandle is null || WalkSoundHandle.Finished)
{
if (Sound.Play(WalkSound, WorldPosition) is { } SoundHandel)
{
WalkSoundHandle = SoundHandel;
}
}
WalkSoundHandle.Position = WorldPosition;
}
else if (WalkSoundHandle is not null)
{
WalkSoundHandle.Stop();
WalkSoundHandle = null;
}
}
void IGameEventHandler<DamageBroadcastEvent>.OnGameEvent(DamageBroadcastEvent EventArgs)
{
if (EventArgs.DamageEvent.VictimPlayerState?.PlayerPawn == this)
{
OnDamageTaken(EventArgs.DamageEvent);
}
if (EventArgs.DamageEvent.AttackerPlayerState?.PlayerPawn == this)
{
OnDamageGiven(EventArgs.DamageEvent);
}
}
void IGameEventHandler<HealingBroadcastEvent>.OnGameEvent(HealingBroadcastEvent EventArgs)
{
OnHealingGiven(EventArgs.HealingRequest);
}
[Rpc.Broadcast(NetFlags.HostOnly)]
public void OnHealingGiven(FHealingReceived HealingDone)
{
if (IsLocallyControlled)
{
if (HealingDone.TargetPlayerPawn == this)
{
UI.Health.Instance?.OnHealing(HealingDone);
}
UI.DamageNumbers.Instance?.OnHit(HealingDone);
}
}
[Rpc.Broadcast(NetFlags.HostOnly)]
public void OnDamageTaken(FDamageTaken DamageTaken)
{
if (IsLocallyControlled)
{
var DamageLocation = DamageTaken.DamageLocation;
UI.DamageIndicatorNew.Instance?.OnHit(DamageLocation);
UI.Health.Instance?.OnDamage(DamageTaken);
}
if (BloodSquirt.IsValid())
{
BloodSquirt.Clone(DamageTaken.DamageLocation);
}
if (HitSound.IsValid())
{
if (Sound.Play(HitSound, WorldPosition) is { } SoundHandel)
{
}
}
}
[Rpc.Broadcast(NetFlags.HostOnly)]
public void OnDamageGiven(FDamageTaken DamageGiven)
{
if (IsLocallyControlled)
{
UI.DamageNumbers.Instance?.OnHit(DamageGiven);
}
}
}