OverdriveEnemy.cs
using Sandbox;
using System;
namespace PaintballBaddies;

public sealed partial class OverdriveEnemy : Component
{
    [Sync(SyncFlags.FromHost)] public int Health {get;private set;}=110;
    [Sync(SyncFlags.FromHost)] public int MissingParts {get;private set;}
    [Sync(SyncFlags.FromHost)] public int HeadHits {get;private set;}
    public bool HeadWounded=>HeadHits==1 && !Dead;
    // AttackStyle is already host-synchronized, so the unarmed role agrees on every peer.
    public bool IsRusher=>AttackStyle==0;
    public int VisibleDamageParts=>MissingParts | (HeadHits>0 ? 1 : 0);
    public float MovementSpeed
    {
        get
        {
            float speed=(MissingParts&24)!=0 ? 82 : Hefty ? (HollowTorso ? 115 : 165) : 205;
            if(IsRusher && (MissingParts&24)==0)speed*=1.35f;
            if(Enraged && (MissingParts&24)==0)speed*=1.4f;
            return speed*(HeadWounded ? .6f : 1);
        }
    }
    [Sync(SyncFlags.FromHost)] public int HeadBreakVariant {get;private set;}=2;
    [Sync(SyncFlags.FromHost)] public Vector3 HeadImpactLocal {get;private set;}
    [Sync(SyncFlags.FromHost)] public float SlipRemaining {get;private set;}
    [Sync(SyncFlags.FromHost)] public int SlipSerial {get;private set;}
    public const float SlipDuration=2.65f;
    private float slipImmunity;
    public static int Slips {get;private set;}
    [Sync(SyncFlags.FromHost)] public int Round {get;set;}
    [Sync(SyncFlags.FromHost)] public bool Dead {get;private set;}
    [Sync(SyncFlags.FromHost)] public bool Hefty {get;private set;}
    [Sync(SyncFlags.FromHost)] public bool HollowTorso {get;private set;}
    [Sync(SyncFlags.FromHost)] public string EmbeddedBoltState {get;internal set;}="";
    private int torsoBlast;
    private bool shownHollow;
    internal void SetHefty(bool value){if(!MultiplayerSession.Authority)return;Hefty=value;Health=value ? 240 : 110;}
    internal void BlastTorso(int amount)
    {if(MultiplayerSession.Authority && Hefty && !Dead && amount>0){torsoBlast+=amount;if(torsoBlast>=40)HollowTorso=true;}}
    [Sync(SyncFlags.FromHost)] public Vector3 LastImpulse {get;private set;}
    [Sync(SyncFlags.FromHost)] public int ReactionSerial {get;private set;}
    [Sync(SyncFlags.FromHost)] public float Windup {get;private set;}
    [Sync(SyncFlags.FromHost)] public int AttackStyle {get;set;}
    [Sync(SyncFlags.FromHost)] public bool RangedWindup {get;private set;}
    [Sync(SyncFlags.FromHost)] public bool ThrowPlate {get;private set;}
    [Sync(SyncFlags.FromHost)] public bool ThrowEgg {get;private set;}
    [Sync(SyncFlags.FromHost)] public float ThrowRecovery {get;private set;}
    [Sync(SyncFlags.FromHost)] public float CollapseRemaining {get;private set;}
    public bool HeadlessStagger=>Dead && CollapseRemaining>0;
    public NavMeshAgent Agent {get;private set;}
    public Vector3 AimPoint=>WorldPosition+Vector3.Up*35;
    private readonly Dictionary<int,int> limbDamage=new();
    private PaintballCombatant target;
    private float rethink,attackClock=1,stagger,deadTime;
    private int shownParts,shownHeadHits;
    private Vector3 stumbleForward;
    private Vector3 throwAim;
    private int shieldThrowCount;
    private SoundHandle sputter;
    // Host-only: an entry door to pass through before chasing, so the horde
    // splits between the dining room doors instead of queuing at one.
    internal Vector3? RouteVia {get;set;}
    // Plate throwers carry a stack in the off hand. Shooting it out of the hand
    // smashes it and sends the sausage charging, enraged, at whoever did it.
    [Sync(SyncFlags.FromHost)] public int PlatesCarried {get;private set;}
    // Egg throwers carry a carton; shooting it splatters them and they slip.
    [Sync(SyncFlags.FromHost)] public int EggsCarried {get;private set;}
    internal void SetEggCarton(int count){if(MultiplayerSession.Authority)EggsCarried=Math.Clamp(count,0,6);}
    public bool Carrying=>PlatesCarried>0 || EggsCarried>0;
    public const float HeadbuttTime=.6f;
    // Reach into the load, take one, cock, throw.
    public const float ThrowWindup=1f,ThrowGrabAt=.3f;
    [Sync(SyncFlags.FromHost)] public bool Enraged {get;private set;}
    [Sync(SyncFlags.FromHost)] public int StackBreakSerial {get;private set;}
    [Sync(SyncFlags.FromHost)] public Vector3 StackBreakPoint {get;private set;}
    private PaintballCombatant rageTarget;
    public static int StacksShot {get;private set;}
    internal void SetPlateStack(int count){if(MultiplayerSession.Authority)PlatesCarried=Math.Clamp(count,0,10);}
    // Host-side estimate of where the stack is; the rig refines it every frame.
    internal Vector3 StackCenter=>Components.Get<OverdriveRobotRig>()?.StackCenter ?? WorldPosition+WorldRotation.Left*15+Vector3.Up*42;
    internal static string StackCallout(int kind)=>kind==2 ? "EGGS SMASHED! WATCH HIM SLIP" : "PLATES SMASHED! HE'S ANGRY";
    /// <summary>Returns 0 when nothing was hit, 1 for a plate stack, 2 for an egg carton.</summary>
    internal int ShootStack(PaintballCombatant shooter,Vector3 point)
    {
        var s=MultiplayerSession.Find(Scene);
        if(!MultiplayerSession.Authority || Dead || !Carrying || s?.IsOverdrive!=true || !s.InRound || Round!=s.RoundId)return 0;
        if(EggsCarried>0)
        {
            // A carton full of eggs bursts all over the sausage's own feet.
            EggsCarried=0;StackBreakPoint=point;StackBreakSerial++;StacksShot++;
            SausageEggSlick.Spawn(Scene,WorldPosition+Vector3.Up*20,Round);
            Windup=0;RangedWindup=false;ThrowRecovery=0;TrySlip(true);
            s.OverdriveKill(shooter,25,point);
            return 2;
        }
        PlatesCarried=0;StackBreakPoint=point;StackBreakSerial++;StacksShot++;
        if(shooter.IsValid()){Enraged=true;rageTarget=shooter;}
        // A beat of disbelief before the charge.
        Windup=0;RangedWindup=false;ThrowRecovery=0;stagger=.45f;LastImpulse=shooter.IsValid() ? (point-shooter.WorldPosition).WithZ(0).Normal : WorldRotation.Backward;
        RouteVia=null;attackClock=.4f;rethink=0;
        s.OverdriveKill(shooter,25,point);
        return 1;
    }
    // Ray against every carried stack (a skill shot, no assist). Host only.
    internal static OverdriveEnemy TraceStack(Scene scene,Vector3 origin,Vector3 direction,float distance,out Vector3 point)
    {
        point=default;OverdriveEnemy best=null;float bestAlong=distance;
        if(!MultiplayerSession.Authority)return null;
        foreach(var e in scene.GetAllComponents<OverdriveEnemy>())
        {
            if(e.Dead || !e.Carrying)continue;
            var c=e.StackCenter;float along=Vector3.Dot(c-origin,direction);
            if(along<0 || along>bestAlong)continue;
            // Stack is about 11 units across and a few plates tall.
            if((c-(origin+direction*along)).Length>7.5f)continue;
            best=e;bestAlong=along;point=origin+direction*along;
        }
        return best;
    }
    private bool sputterStarted;
    public static OverdriveEnemy Find(GameObject go)
    {while(go.IsValid()){var enemy=go.Components.Get<OverdriveEnemy>();if(enemy.IsValid())return enemy;go=go.Parent;}return null;}
    internal static SceneTraceResult TraceIntact(SceneTrace trace)
    {
        SceneTraceResult closest=default;
        foreach(var hit in trace.RunAll())
        {
            var enemy=Find(hit.GameObject);
            if(!hit.Hit || !enemy.IsValid() || enemy.Dead)continue;
            int part=enemy.Zone(hit.HitPosition,hit.Hitbox?.Bone?.Name);
            if(part!=0 && (enemy.MissingParts&part)!=0)continue;
            if(!closest.Hit || hit.Fraction<closest.Fraction)closest=hit;
        }
        return closest;
    }
    internal void PrepareComponents()
    {
        if(!Components.Get<OverdriveRobotRig>().IsValid())Components.Create<OverdriveRobotRig>();
        if(!Components.Get<SausageEmbeddedBolts>().IsValid())Components.Create<SausageEmbeddedBolts>();
        var collider=Components.Get<BoxCollider>() ?? Components.Create<BoxCollider>();collider.Center=Vector3.Up*37;collider.Scale=new Vector3(24,34,74);collider.Static=false;
        Agent=Components.Get<NavMeshAgent>() ?? Components.Create<NavMeshAgent>();Agent.Height=74;Agent.Radius=17;Agent.MaxSpeed=205;Agent.Acceleration=750;
    }
    protected override void OnStart()
    {
        GameObject.Tags.Add("overdrive_enemy");PrepareComponents();_=CryoBurst.Load();
        Agent.UpdatePosition=!IsProxy && !Dead;Agent.UpdateRotation=false;shownParts=MissingParts;shownHeadHits=HeadHits;
    }
    protected override void OnUpdate()
    {
        UpdateCryoPresentation();
        bool headHitChanged=shownHeadHits!=HeadHits;
        shownHeadHits=HeadHits;
        if(headHitChanged || ((shownParts&1)==0 && (MissingParts&1)!=0))
        {
            var rig=Components.Get<OverdriveRobotRig>();
            var point=rig?.Body.IsValid()==true ? rig.IntactPose("head").Position : WorldPosition+Vector3.Up*68;
            OverdriveHeadBurst.Spawn(Scene,point,LastImpulse,Round+(int)WorldPosition.x*11+(int)WorldPosition.y*7+HeadHits,HeadBreakVariant-2);
        }
        if(shownParts!=MissingParts)
        {
            int lost=MissingParts&~shownParts;shownParts=MissingParts;
            foreach(int part in new[]{2,4,8,16})if((lost&part)!=0)
                OverdriveFragment.Spawn(Components.Get<OverdriveRobotRig>()?.Body,part,LastImpulse);
            if((lost&30)!=0){OverdriveAudio.Play("limb",AimPoint);}
        }
        if(HollowTorso && !shownHollow)
        {
            shownHollow=true;OverdriveHeadBurst.Spawn(Scene,WorldPosition+Vector3.Up*40,LastImpulse,Round+37);
            OverdriveAudio.Play("sputter",AimPoint);
        }
        if(HeadlessStagger)
        {
            if(!sputterStarted){sputterStarted=true;sputter=OverdriveAudio.Play("sputter",AimPoint);}
            if(sputter is not null)
            {
                sputter.Position=AimPoint;
            }
        }
        else if(sputter is not null){sputter.Stop();sputter=null;}
        if(!MultiplayerSession.Authority)return;
        var session=MultiplayerSession.Find(Scene);
        if(session?.IsOverdrive!=true || !session.InRound || session.RoundId!=Round){GameObject.Destroy();return;}
        if(Dead)
        {
            Agent.Stop();Agent.UpdatePosition=false;deadTime+=Time.Delta;
            if(HeadlessStagger)
            {
                // Continue the last gait briefly, but never attack, retarget or walk through cover.
                var direction=Rotation.FromYaw(MathF.Sin(deadTime*13)*12)*stumbleForward;
                var start=WorldPosition+Vector3.Up*36;
                var end=start+direction*(55+50*CollapseRemaining)*Time.Delta;
                var hit=Scene.Trace.Sphere(19,start,end).IgnoreGameObjectHierarchy(GameObject).WithoutTags("paintball_debris").Run();
                if(!hit.Hit && Scene.NavMesh.GetClosestPoint(end-Vector3.Up*36,24) is {} floor && MathF.Abs(floor.z-WorldPosition.z)<20)
                    WorldPosition=new Vector3(end.x,end.y,floor.z+3);
                else CollapseRemaining=MathF.Min(CollapseRemaining,.18f);
                CollapseRemaining=MathF.Max(0,CollapseRemaining-Time.Delta);
            }
            if(deadTime>5)GameObject.Destroy();return;
        }
        if(session.OverdriveReviewPaused){Agent.Stop();return;}
        if(TickFreeze())return;
        if(session.OverdrivePhase=="READY")
        {
            Windup=ThrowRecovery=0;RangedWindup=false;Agent.MaxSpeed=85*(HeadWounded ? .6f : 1);
            if(RouteVia is {} patrol)Agent.MoveTo(patrol);else Agent.Stop();
            return;
        }
        slipImmunity=MathF.Max(0,slipImmunity-Time.Delta);
        if(SlipRemaining>0)
        {
            Agent.Stop();SlipRemaining=MathF.Max(0,SlipRemaining-Time.Delta);
            // A short grounded skid, swept against geometry; never a teleport.
            float elapsed=SlipDuration-SlipRemaining;
            if(elapsed<.42f)
            {
                var start=WorldPosition+Vector3.Up*36;
                var end=start+WorldRotation.Forward*(150*(1-elapsed/.42f))*Time.Delta;
                // Slips affect this enemy only; close crowds must not domino.
                var hit=Scene.Trace.Sphere(18,start,end).IgnoreGameObjectHierarchy(GameObject).WithoutTags("paintball_debris").Run();
                if(!hit.Hit && Scene.NavMesh.GetClosestPoint(end-Vector3.Up*36,20) is {} floor && MathF.Abs(floor.z-WorldPosition.z)<12)
                    Agent.SetAgentPosition(new Vector3(end.x,end.y,floor.z+3));
            }
            return;
        }
        rethink-=Time.Delta;attackClock-=Time.Delta;stagger=MathF.Max(0,stagger-Time.Delta);ThrowRecovery=MathF.Max(0,ThrowRecovery-Time.Delta);
        bool targetLost=!target.IsValid() || !target.AcceptHits || target.Components.Get<CloseCombat>()?.Incapacitated==true;
        if(targetLost){Windup=0;RangedWindup=false;}
        // Finish a telegraphed throw at its original target. Retargeting midway
        // used to turn the torso while the projectile still used the old aim.
        bool rageValid=Enraged && rageTarget.IsValid() && rageTarget.AcceptHits && rageTarget.Components.Get<CloseCombat>()?.Incapacitated!=true;
        if(rageValid){target=rageTarget;targetLost=false;RouteVia=null;}
        else if((rethink<=0 && Windup<=0 && ThrowRecovery<=0) || targetLost)
        {
            target=Scene.GetAllComponents<PaintballCombatant>().Where(p=>p.AcceptHits && p.Components.Get<CloseCombat>()?.Incapacitated!=true)
                .OrderBy(p=>(p.WorldPosition-WorldPosition).Length*(p.Components.Get<NetworkPawn>().IsValid() ? .6f : 1)).FirstOrDefault();
            rethink=.6f;
        }
        if(!target.IsValid()){Agent.Stop();return;}
        var delta=(target.WorldPosition-WorldPosition).WithZ(0);
        var facing=RangedWindup ? (throwAim-WorldPosition).WithZ(0) : delta;
        if(facing.Length>1)WorldRotation=Rotation.Slerp(WorldRotation,Rotation.LookAt(facing.Normal),1-MathF.Exp(-10*Time.Delta));
        Agent.MaxSpeed=MovementSpeed;
        if(stagger>0)
        {
            Agent.Stop();var start=WorldPosition+Vector3.Up*36;
            var end=start+LastImpulse.WithZ(0).Normal*(90*stagger/.28f)*Time.Delta;
            var push=Scene.Trace.Sphere(17,start,end).IgnoreGameObjectHierarchy(GameObject).WithoutTags("paintball_debris").Run();
            if(!push.Hit)Agent.SetAgentPosition(end-Vector3.Up*36);return;
        }
        // Clear the assigned doorway before stopping to throw. A nearby squad
        // used to make every thrower wait inside the same narrow opening.
        if(RouteVia is {} via && delta.Length>=60)
        {
            if((via-WorldPosition).WithZ(0).Length<30)RouteVia=null;
            else
            {
                var door=Scene.NavMesh.GetClosestPoint(via,40);
                if(door.HasValue)
                {
                    var direction=(via-WorldPosition).WithZ(0).Normal;
                    WorldRotation=Rotation.Slerp(WorldRotation,Rotation.LookAt(direction),1-MathF.Exp(-10*Time.Delta));
                    Agent.MoveTo(door.Value);return;
                }
                RouteVia=null;
            }
        }
        if(Windup>0)
        {
            Agent.Stop();Windup=MathF.Max(0,Windup-Time.Delta);
            if(Windup==0 && RangedWindup)
            {
                if(SausageProjectile.Launch(this,throwAim,ThrowPlate,target.GameObject,ThrowEgg))
                {
                    if(ThrowPlate && PlatesCarried>0)PlatesCarried--;
                    else if(ThrowEgg && EggsCarried>0)EggsCarried--;
                }
                RangedWindup=false;ThrowRecovery=.6f;
            }
            else if(Windup==0 && delta.Length<74)
            {
                var hit=Scene.Trace.Ray(AimPoint,target.EyePosition-Vector3.Up*14).IgnoreGameObjectHierarchy(GameObject).WithoutTags("paintball_debris").Run();
                if(!hit.Hit || PaintballCombatant.Find(hit.GameObject)==target)
                {
                    if(ArenaShield.TryAbsorbMelee(target,WorldPosition)==ArenaShield.MeleeResult.None)
                        target.ReceiveOverdriveDamage((MissingParts&6)!=0 ? 15 : Enraged ? 36 : IsRusher ? 32 : 28,WorldPosition);
                }
            }
            return;
        }
        if(ThrowRecovery>.25f){Agent.Stop();return;}
        if(AttackStyle>0 && !rageValid && (MissingParts&6)!=6 && delta.Length>150 && delta.Length<850)
        {
            var lane=Scene.Trace.Ray(AimPoint,target.EyePosition-Vector3.Up*15).WithSurfaceMeshes().IgnoreGameObjectHierarchy(GameObject).WithoutTags("paintball_debris","overdrive_enemy").Run();
            if(!lane.Hit || PaintballCombatant.Find(lane.GameObject)==target)
            {
                if(attackClock<=0)
                {
                    throwAim=target.EyePosition-Vector3.Up*15;
                    // Shield carriers face a varied mix, with the same warning and fair flight time.
                    bool shielded=Scene.GetAllComponents<ArenaShield>().Any(x=>x.Owner==target && x.Remaining>0);
                    // Eggs are a regular part of every thrower's mix, not only one style.
                    ThrowEgg=AttackStyle==3 ? EggsCarried>0 : Game.Random.Float(0,1)<(AttackStyle==1 ? .4f : .25f);
                    // Plates come off the carried stack; an empty-handed thrower uses knives.
                    ThrowPlate=!ThrowEgg && (shielded ? ++shieldThrowCount%2==0 : AttackStyle==2) && (AttackStyle!=2 || PlatesCarried>0);
                    Windup=ThrowWindup;RangedWindup=true;attackClock=3.4f+AttackStyle*.2f;
                    Agent.Stop();return;
                }
                if(delta.Length<580){Agent.Stop();return;}
            }
        }
        if(delta.Length<60 && attackClock<=0){Windup=HeadbuttTime;attackClock=Enraged || IsRusher ? 1.1f : 1.8f;DroneAudio.Play("charge",AimPoint);Agent.Stop();return;}
        // Stop at arm's length instead of walking into the fighter (they no
        // longer physically collide, so this keeps sausages from overlapping).
        if(delta.Length<48){Agent.Stop();return;}
        var destination=Scene.NavMesh.GetClosestPoint(target.WorldPosition,150);
        if(destination.HasValue)Agent.MoveTo(destination.Value);else Agent.Stop();
    }
    public int Zone(Vector3 point,string hitBone=null)
    {
        if(!string.IsNullOrEmpty(hitBone))return OverdriveRobotRig.Part(hitBone);
        var p=WorldTransform.PointToLocal(point);
        return p.z>48 ? 1 : p.z<15 ? (p.y>0 ? 8 : 16) : MathF.Abs(p.y)>10 ? (p.y>0 ? 2 : 4) : 0;
    }
    public bool Damage(int damage,int part,Vector3 impulse,PaintballCombatant shooter,Vector3? hitPoint=null)
        =>ApplyDamage(damage,part,impulse,shooter,hitPoint,false);
    // Head detonations count as head hits; embedding does no damage. Limb/torso
    // blasts never turn accumulated projectile hits into a whole-body kill.
    public bool ExplodeBoltPart(int part,Vector3 impulse,PaintballCombatant shooter,Vector3 point)
    {
        if(part==0)BlastTorso(60);
        return ApplyDamage(40,part,impulse,shooter,point,part!=1);
    }
    private bool ApplyDamage(int damage,int part,Vector3 impulse,PaintballCombatant shooter,Vector3? hitPoint,bool localized)
    {
        var session=MultiplayerSession.Find(Scene);
        if(!MultiplayerSession.Authority || Dead || damage<=0 || session?.IsOverdrive!=true || !session.InRound || Round!=session.RoundId)return false;
        if(FrozenSolid)return ShatterFrozen(impulse,shooter,part==1);
        LastImpulse=impulse;ReactionSerial++;stagger=.28f;Windup=0;RangedWindup=false;ThrowRecovery=0;
        if(part==1)
        {
            // One call represents one trigger pull or one bolt detonation.
            // Keep the surviving half targetable until a second head hit.
            HeadHits=Math.Min(2,HeadHits+1);stagger=.55f;
            if(HeadHits==1)
            {
                HeadImpactLocal=WorldTransform.PointToLocal(hitPoint ?? WorldPosition+Vector3.Up*68);
                var incoming=WorldRotation.Inverse*impulse;
                HeadBreakVariant=MathF.Abs(HeadImpactLocal.y)>1 ? (HeadImpactLocal.y>0 ? 2 : 3) : incoming.y>0.2f ? 3 : 2;
                Health=Math.Max(1,Health-Math.Min(damage*2,Health/2));
            }
            else{MissingParts|=1;Health=0;}
        }
        else if(part!=0 && (MissingParts&part)==0)
        {
            limbDamage[part]=limbDamage.GetValueOrDefault(part)+damage;
            if(limbDamage[part]>=32)
            {
                MissingParts|=part;
                stagger=.55f;
                // Losing the arm that carries the stack drops and smashes it.
                if(part==2 && Carrying){PlatesCarried=0;EggsCarried=0;StackBreakPoint=AimPoint;StackBreakSerial++;}
            }
        }
        if(!localized && part!=1)
        {
            Health=Math.Max(0,Health-damage);
            if((MissingParts&1)!=0 || (MissingParts&24)==24)Health=0;
        }
        session.RiotHit(shooter);
        if(Health==0)
        {
            stumbleForward=WorldRotation.Forward.WithZ(0).Normal;
            CollapseRemaining=(MissingParts&1)!=0 && (MissingParts&24)==0 && SlipRemaining<=0 ? .95f : 0;
            Dead=true;SlipRemaining=0;Components.Get<BoxCollider>().Enabled=false;Agent?.Stop();
            if(shooter.IsValid())shooter.RegisterSausageKill(part==1);
            if(!MultiplayerSession.Online && session.LastSausageOfEncounter(this))FinalKillSlowMo.Start(Scene);
            session.OverdriveKill(shooter,part==1 ? 75 : 40,WorldPosition);
        }
        return true;
    }
    internal bool GrenadeHit(Vector3 impulse,PaintballCombatant shooter)
    {
        if(Dead || !MultiplayerSession.Authority)return false;
        // One authoritative death and score; the existing segmented rig and
        // textured fragments present the dismemberment on every peer.
        if(FrozenSolid)return ShatterFrozen(impulse,shooter,false);
        if(!Damage(Health,0,impulse,shooter))return false;
        MissingParts|=31;CollapseRemaining=0;return true;
    }
    internal bool KnockOut(Vector3 impulse,PaintballCombatant shooter)
        =>!Dead && Damage(Health,0,impulse,shooter);
    internal bool TrySlip(bool bowled=false)
    {
        var s=MultiplayerSession.Find(Scene);
        if(!MultiplayerSession.Authority || Dead || SlipRemaining>0 || slipImmunity>0 || (MissingParts&24)!=0 || s?.IsOverdrive!=true || !s.InRound || Round!=s.RoundId)return false;
        // One comic accident at a time; other enemies keep pressure on the team.
        if(!bowled && Scene.GetAllComponents<OverdriveEnemy>().Count(e=>e!=this && e.SlipRemaining>0)>=2)return false;
        SlipRemaining=SlipDuration;slipImmunity=5;SlipSerial++;Slips++;
        Windup=0;RangedWindup=false;ThrowRecovery=0;attackClock=2;Agent?.Stop();return true;
    }
    protected override void OnDestroy(){sputter?.Stop();}
}