PaintballCombatant.cs
using Sandbox;
namespace PaintballBaddies;
public sealed partial class PaintballCombatant : Component
{
protected override void OnStart()
{
observedHits=PaintHits;observedHitSerial=LastHitSerial;
if(!Components.Get<OverdriveLoadout>().IsValid())Components.Create<OverdriveLoadout>();
GameObject.Tags.Add("paintball_actor");
CharacterSeparation.Ensure(Scene);
if(Components.Get<PaintHitReaction>() is null)Components.Create<PaintHitReaction>();
if(Components.Get<CharacterVoice>() is null)Components.Create<CharacterVoice>();
if(Components.Get<CloseCombat>() is null)Components.Create<CloseCombat>();
}
[Sync(SyncFlags.FromHost)] public int Team { get; set; }
[Sync(SyncFlags.FromHost)] public int PaintHits { get; private set; }
[Sync(SyncFlags.FromHost)] public int HitsLanded { get; private set; }
[Sync(SyncFlags.FromHost)] public int KnockdownsLanded { get; private set; }
[Sync(SyncFlags.FromHost)] public int KnockdownsReceived { get; private set; }
public const int KnockdownPoints=2;
public int Points => MultiplayerSession.Find(Scene)?.IsCooperative==true ? PopScore : HitsLanded-PaintHits+KnockdownPoints*(KnockdownsLanded-KnockdownsReceived);
[Sync(SyncFlags.FromHost)] public bool StayInMatch { get; set; }
[Sync(SyncFlags.FromHost)] public bool AcceptHits { get; set; } = true;
public Vector3 EyePosition => Components.Get<PlayerController>()?.EyePosition
?? WorldPosition+Vector3.Up*(Components.Get<ArenaOpponent>()?.CoverCrouched == true ? 35 : 60);
public int HitLimit { get; set; } = 3;
public bool Eliminated => !StayInMatch && PaintHits >= HitLimit;
public float HitFeedbackRemaining { get; private set; }
public const float DirectionIndicatorDuration=1.2f;
public float DirectionIndicatorRemaining { get; private set; }
[Sync(SyncFlags.FromHost)] public Vector3 LastHitDirection { get; private set; }
[Sync(SyncFlags.FromHost)] public int LastHitSerial { get; private set; }
public Vector3 LastHitOrigin { get; private set; }
public PaintballCombatant LastAttacker { get; private set; }
private int observedHits;
private int observedHitSerial;
protected override void OnUpdate()
{
TickOverdriveHealth();
if(PaintHits<observedHits)DirectionIndicatorRemaining=0;
// A serial also detects the first new hit when scores reset between rounds.
if(!MultiplayerSession.Authority && LastHitSerial!=observedHitSerial && (PaintHits>0 || HitSlowSeconds>0))
{
HitFeedbackRemaining=.45f;
DirectionIndicatorRemaining=DirectionIndicatorDuration;
}
observedHits=PaintHits;
observedHitSerial=LastHitSerial;
HitFeedbackRemaining=System.MathF.Max(0,HitFeedbackRemaining-Time.Delta);
DirectionIndicatorRemaining=System.MathF.Max(0,DirectionIndicatorRemaining-Time.Delta);
if(Components.Get<PaintHitReaction>() is null)Components.Create<PaintHitReaction>();
if(MultiplayerSession.Authority)HitSlowSeconds=System.MathF.Max(0,HitSlowSeconds-Time.Delta);
if(Components.Get<CharacterVoice>() is null)Components.Create<CharacterVoice>();
if(Components.Get<CloseCombat>() is null)Components.Create<CloseCombat>();
}
public bool RegisterHit( int attackingTeam, Vector3? sourcePosition = null, PaintballCombatant attacker = null, Vector3? observedOrigin = null )
{
if(!MultiplayerSession.Authority || MultiplayerSession.Find(Scene)?.IsCooperative==true)return false;
if ( !AcceptHits || attackingTeam == Team || Eliminated ) return false;
PaintHits++;
if(PaintHits%5==0)BeginHitSlowdown();
LastHitSerial=unchecked(LastHitSerial+1);
LastAttacker=attacker;
LastHitOrigin=observedOrigin ?? sourcePosition ?? (attacker.IsValid() ? attacker.WorldPosition : WorldPosition);
if(attacker.IsValid() && attacker!=this)
{
attacker.HitsLanded++;
if(MultiplayerSession.Online)attacker.Components.Get<NetworkPawn>()?.ConfirmHit();
}
LastHitDirection = sourcePosition.HasValue ? (sourcePosition.Value-WorldPosition).WithZ(0).Normal : Vector3.Zero;
HitFeedbackRemaining = 0.45f;
DirectionIndicatorRemaining=DirectionIndicatorDuration;
return true;
}
/// <summary>Clockwise screen angle: front 0, right 90, behind 180, left -90.</summary>
public float HitDirectionAngle(Rotation view)
{
var forward=Vector3.Dot(LastHitDirection,view.Forward.WithZ(0).Normal);
var right=Vector3.Dot(LastHitDirection,view.Right.WithZ(0).Normal);
return System.MathF.Atan2(right,forward)*180/System.MathF.PI;
}
public string HitDirectionLabel(Rotation view)
{
if(HitFeedbackRemaining<=0 || LastHitDirection.Length<.01f)return "";
var forward=Vector3.Dot(LastHitDirection,view.Forward.WithZ(0).Normal);
var right=Vector3.Dot(LastHitDirection,view.Right.WithZ(0).Normal);
return System.MathF.Abs(right)>System.MathF.Abs(forward) ? (right>0 ? "RIGHT" : "LEFT") : (forward>0 ? "AHEAD" : "BEHIND");
}
internal bool RegisterKnockdown(PaintballCombatant attacker)
{
if(!MultiplayerSession.Authority || MultiplayerSession.Find(Scene)?.IsCooperative==true || !AcceptHits || Eliminated || !attacker.IsValid()
|| !attacker.AcceptHits || attacker.Eliminated || attacker==this || attacker.Team==Team)return false;
KnockdownsReceived++;attacker.KnockdownsLanded++;
return true;
}
public void ResetPaintHits()
{
if(!MultiplayerSession.Authority)return;
Components.Get<CloseCombat>()?.ResetState();
Components.Get<CharacterPaint>()?.Clear();
ResetPopRiotScore();ResetOverdrive();
PaintHits = 0;
observedHits=0;
HitsLanded = 0;
KnockdownsLanded=KnockdownsReceived=0;
AcceptHits = true;
HitFeedbackRemaining = 0;
DirectionIndicatorRemaining=0;
LastHitDirection = Vector3.Zero;
}
public static PaintballCombatant Find( GameObject go )
{
while ( go is not null )
{
var actor = go.Components.Get<PaintballCombatant>();
if ( actor is not null ) return actor;
go = go.Parent;
}
return null;
}
}