PaintballCombatant.Overdrive.cs
using Sandbox;
namespace PaintballBaddies;

public sealed partial class PaintballCombatant
{
    [Sync(SyncFlags.FromHost)] public int OverdriveHealth {get;private set;}=100;
    [Sync(SyncFlags.FromHost)] public int OverdriveDowns {get;private set;}
    [Sync(SyncFlags.FromHost)] public int SausageKills {get;private set;}
    [Sync(SyncFlags.FromHost)] public int EquippedWeapon {get;private set;}
    public bool HasCrossbow=>EquippedWeapon==1;
    internal void EquipWeapon(int weapon)
    {
        if(!MultiplayerSession.Authority || weapon<0 || weapon>1 || MultiplayerSession.Find(Scene) is not {IsOverdrive:true,InRound:true}
            || !AcceptHits || Components.Get<CloseCombat>()?.Incapacitated==true)return;
        EquippedWeapon=weapon;
    }
    [Sync(SyncFlags.FromHost)] public int SausageHeadshots {get;private set;}
    [Sync(SyncFlags.FromHost)] public int GrenadeCharges {get;private set;}
    [Sync(SyncFlags.FromHost)] public string SpecialQueue {get;private set;}="";
    public bool FreezeGrenadeReady=>SpecialQueue.StartsWith("F");
    public string SpecialName=>FreezeGrenadeReady ? "FLASH FREEZE" : "EXPLOSIVE GRENADE";
    public string NextSpecialName=>(SausageHeadshots/5)%2==0 ? "EXPLOSIVE GRENADE" : "FLASH FREEZE";
    [Sync(SyncFlags.FromHost)] public float HotShotSeconds {get;private set;}
    [Sync(SyncFlags.FromHost)] public int PowerupSerial {get;private set;}
    [Sync(SyncFlags.FromHost)] public string LastPowerup {get;private set;}="";
    internal void RegisterSausageKill(bool headshot=false)
    {if(MultiplayerSession.Authority){SausageKills++;if(headshot)RegisterHeadshot();}}
    internal void RegisterHeadshot()
    {
        if(!MultiplayerSession.Authority || MultiplayerSession.Find(Scene) is not {IsOverdrive:true,InRound:true})return;
        SausageHeadshots++;
        if(SausageHeadshots%5!=0)return;
        bool freeze=(SausageHeadshots/5)%2==0;
        QueueSpecial(freeze);
    }
    private void QueueSpecial(bool freeze)
    {
        SpecialQueue+=freeze ? "F" : "E";GrenadeCharges++;
        LastPowerup=freeze ? "FLASH FREEZE READY" : "GRENADE READY";
        PowerupSerial++;
    }
    // Helpers have one wave allowance, so body-aiming bots can demonstrate the
    // specials without taking human headshot rewards or accumulating free ammo.
    internal void PrepareAISpecial(int wave,int character)
    {
        if(!MultiplayerSession.Authority || !Components.Get<NetworkBot>().IsValid() || GrenadeCharges>0
            || MultiplayerSession.Find(Scene) is not {IsOverdrive:true,InRound:true})return;
        QueueSpecial((wave+character)%2==0);
    }
    internal bool ConsumeGrenade()
    {if(!MultiplayerSession.Authority || GrenadeCharges<=0)return false;GrenadeCharges--;if(SpecialQueue.Length>0)SpecialQueue=SpecialQueue.Substring(1);return true;}
    private float grenadeCooldown;
    public bool TryThrowGrenade(Vector3 aim)
    {
        if(!MultiplayerSession.Authority || grenadeCooldown>0 || GrenadeCharges<=0 || !AcceptHits
            || MultiplayerSession.Find(Scene) is not {IsOverdrive:true,InRound:true}
            || Components.Get<CloseCombat>() is {Incapacitated:true} or {Attacking:true}
            || Components.Get<VaultController>()?.IsVaulting==true || Components.Get<CoverController>()?.Sliding==true
            || !float.IsFinite(aim.x) || !float.IsFinite(aim.y) || !float.IsFinite(aim.z))return false;
        var origin=Components.Get<PlayerController>()?.EyePosition ?? WorldPosition+Vector3.Up*52;
        if((aim-origin).Length<1)return false;
        // Start beside the thrower's eye, never on the other side of cover.
        // The projectile's swept trace handles any immediate wall impact.
        bool freeze=FreezeGrenadeReady;
        if(!ConsumeGrenade())return false;
        grenadeCooldown=.65f;SausageGrenade.Fire(this,origin,aim,freeze);return true;
    }

    private float overdriveImmunity;
    private bool overdriveWasDown;
    internal void ResetOverdrive(){OverdriveHealth=100;OverdriveDowns=0;SausageKills=SausageHeadshots=GrenadeCharges=PowerupSerial=0;HotShotSeconds=0;LastPowerup="";SpecialQueue="";EquippedWeapon=0;overdriveImmunity=grenadeCooldown=0;overdriveWasDown=false;}
    internal void HealOverdrive(){if(MultiplayerSession.Authority)OverdriveHealth=100;}
    private void TickOverdriveHealth()
    {
        if(!MultiplayerSession.Authority)return;
        grenadeCooldown=System.MathF.Max(0,grenadeCooldown-Time.Delta);
        overdriveImmunity=System.MathF.Max(0,overdriveImmunity-Time.Delta);
        if(MultiplayerSession.Find(Scene)?.IsOverdrive!=true)return;
        HotShotSeconds=System.MathF.Max(0,HotShotSeconds-Time.Delta);
        bool down=Components.Get<CloseCombat>()?.Incapacitated==true;
        if(overdriveWasDown && !down){OverdriveHealth=50;overdriveImmunity=2;}
        overdriveWasDown=down;
    }
    internal bool ReceiveOverdriveDamage(int amount,Vector3 origin)
    {
        var session=MultiplayerSession.Find(Scene);
        if(!MultiplayerSession.Authority || session?.IsOverdrive!=true || !session.InRound || !AcceptHits
            || overdriveImmunity>0 || Components.Get<CloseCombat>()?.Incapacitated==true)return false;
        OverdriveHealth=System.Math.Max(0,OverdriveHealth-amount);overdriveImmunity=.6f;
        LastHitSerial=unchecked(LastHitSerial+1);LastHitDirection=(origin-WorldPosition).WithZ(0).Normal;
        HitFeedbackRemaining=.45f;DirectionIndicatorRemaining=DirectionIndicatorDuration;
        if(OverdriveHealth==0){OverdriveDowns++;Components.Get<CloseCombat>()?.ReceiveContact(origin,true);overdriveWasDown=true;}
        else {BeginHitSlowdown();Components.Get<CloseCombat>()?.ReceiveContact(origin,false);}
        return true;
    }
}