CloseCombatReview.cs
using Sandbox;
namespace PaintballBaddies;

/// <summary>Opt-in native input/contact regression; removes its platform and restores player control.</summary>
public sealed class CloseCombatReview : Component
{
    public bool VisualOnly { get; set; }
    public bool PlayerFallVisual {get;set;}
    public bool ShieldMode {get;set;}
    private ArenaShield reviewShield;
    public float PauseAt { get; set; }=-1;
    public bool Done { get; private set; }
    public List<string> Results { get; }=new();
    public float Elapsed { get; private set; }
    private PlayerController player;
    private CloseCombat attacker,victim;
    private PaintballCombatant playerActor,botActor;
    private ArenaOpponent bot;
    private GameObject botObject,wall;
    private Vector3 oldPosition;
    private Angles oldAngles;
    private bool oldLook,oldCamera,button,previous;
    private System.IDisposable inputHook;
    private int stage;
    private float stageTime;
    private int initialContacts,initialSounds;
    private int initialPlayerHits,initialPlayerLanded;
    private bool secondAttackPlaced;
    public List<string> MissTrace { get; }=new();
    public string State=>Json.Serialize(new {stage,Elapsed,stageTime,Done,PlayerDown=attacker?.Downed,PlayerRecovering=attacker?.Recovering,
        Presses=attacker?.RecoveryPresses,Input=player?.UseInputControls,Contacts=attacker?.Contacts,Sounds=attacker?.ImpactSounds,
        VictimDown=victim?.Downed,VictimRecovering=victim?.Recovering,BotPresses=victim?.RecoveryPresses});
    protected override void OnStart()
    {
        Scene.GetAllComponents<PaintballControls>().First().StartPractice();
        player=Scene.GetAllComponents<PlayerController>().First();
        playerActor=player.Components.Get<PaintballCombatant>();attacker=player.Components.Get<CloseCombat>();
        initialContacts=attacker.Contacts;initialSounds=attacker.ImpactSounds;
        initialPlayerHits=playerActor.PaintHits;initialPlayerLanded=playerActor.HitsLanded;
        oldPosition=player.WorldPosition;oldAngles=player.EyeAngles;oldLook=player.UseLookControls;oldCamera=player.UseCameraControls;
        player.UseLookControls=false;player.UseCameraControls=false;
        player.WorldPosition=new(0,0,1202);player.EyeAngles=Angles.Zero;player.Body.Velocity=Vector3.Zero;
        TrainingRange.Box(GameObject,"Contact review floor",new(0,0,1198),new(420,420,4),Color.Gray,true);
        botObject=new GameObject(GameObject){Name="Elbow review opponent",WorldPosition=new(42,0,1200),WorldRotation=Rotation.FromYaw(180)};
        botObject.Components.Create<CitizenOpponentPresentation>();
        bot=botObject.Components.Create<ArenaOpponent>();bot.Character="imani";bot.CombatEnabled=false;
        botActor=botObject.Components.Create<PaintballCombatant>();botActor.Team=91;botActor.StayInMatch=true;
        var collider=botObject.Components.Create<BoxCollider>();collider.Scale=new(28,28,66);collider.Center=new(0,0,33);
        if(ShieldMode)reviewShield=new GameObject(GameObject){Name="Melee shield review panel"}.Components.Create<ArenaShield>();
        inputHook=Scene.AddHook(GameObjectSystem.Stage.StartUpdate,-40,InjectInput,nameof(CloseCombatReview),"Contact test action edges");
    }
    private void InjectInput()
    {
        Input.SetAction("attack2",button);Input.SetLastAction("attack2",previous);previous=button;
        Input.SetAction("attack1",false);Input.AnalogMove=Vector3.Zero;Input.AnalogLook=Angles.Zero;
    }
    protected override void OnPreRender()
    {
        if((!VisualOnly && !ShieldMode) || !player.IsValid())return;
        Scene.Camera.WorldPosition=ShieldMode ? new Vector3(-60,-95,1268) : new Vector3(65,-145,1265);
        Scene.Camera.WorldRotation=Rotation.LookAt(new Vector3(22,0,1235)-Scene.Camera.WorldPosition);
    }
    private void Check(bool ok,string name){var result=(ok ? "PASS " : "FAIL ")+name;Results.Add(result);Log.Info("ELBOW_REVIEW "+result);}
    private void Next(){stage++;stageTime=0;button=false;}
    private void PlaceBot(){botObject.WorldPosition=new(42,0,1200);botObject.WorldRotation=Rotation.FromYaw(180);}
    protected override void OnUpdate()
    {
        if(Done)return;
        Elapsed+=Time.Delta;stageTime+=Time.Delta;
        if(!bot.IsValid() || !botObject.Components.Get<CitizenOpponentPresentation>().IsReady)return;
        victim ??= botObject.Components.Get<CloseCombat>();if(!victim.IsValid())return;
        if(stage<15)
        {
            bot.Enabled=false;
            var agent=bot.Components.Get<NavMeshAgent>();
            agent?.Stop();
            // This fixture positions the victim explicitly. A stopped navigation
            // agent otherwise pulls a teleported victim back into elbow range.
            if(agent.IsValid())agent.UpdatePosition=false;
        }
        if(ShieldMode)
        {
            ShieldSequence();
            if(PauseAt>0 && Elapsed>=PauseAt){Scene.TimeScale=0;PauseAt=-1;}
            return;
        }
        if(VisualOnly){Visual();return;}
        if(Elapsed>28){Check(false,"timed out "+State);Done=true;button=false;return;}
        switch(stage)
        {
            case 0:
                if(stageTime<.7f)return;
                Check(attacker.CanContact(botActor),"near opponent is within elbow reach");
                botActor.Team=playerActor.Team;Check(!attacker.CanContact(botActor),"same team rejected");botActor.Team=91;
                botObject.WorldPosition=new(80,0,1200);Check(!attacker.CanContact(botActor),"distant opponent rejected");PlaceBot();
                wall=TrainingRange.Box(GameObject,"Contact test wall",new(21,0,1238),new(5,40,70),Color.Gray,true);
                Next();break;
            case 1:
                if(stageTime<.1f)return;
                Check(!attacker.TryAttack(),"wall blocks elbow");wall.Destroy();Next();break;
            case 2:
                botObject.WorldPosition=new(90,0,1200);
                button=stageTime<.35f;
                if(stageTime>.55f){Check(!attacker.Attacking && attacker.Contacts==initialContacts,"holding and releasing aim at range does not elbow");PlaceBot();Next();}
                break;
            case 3:
                button=stageTime<.55f;
                if(stageTime>.15f && stageTime<.25f && !Results.Any(x=>x.Contains("press starts")))
                    Check(attacker.Attacking,"native attack2 press starts elbow before release");
                if(stageTime>.6f){Check(attacker.Contacts==initialContacts+1 && victim.Downed,"quick native attack2 tap lands elbow and knocks down");
                    Check(attacker.ImpactSounds==initialSounds+1,"one impact sound for one contact");
                    Check(playerActor.HitsLanded==initialPlayerLanded && playerActor.PaintHits==initialPlayerHits && botActor.PaintHits==0,"elbow does not add paintball score");Next();}
                break;
            case 4:
                if(stageTime<.7f)return;
                if(!secondAttackPlaced)
                {
                    Check(player.UseInputControls,"attacker movement restored after elbow");
                    victim.ResetState();PlaceBot();secondAttackPlaced=true;return;
                }
                Check(attacker.TryAttack(),"second elbow starts after cooldown");botObject.WorldPosition=new(100,0,1200);Next();break;
            case 5:
                if(MissTrace.Count<7 && stageTime>MissTrace.Count*.12f)
                    MissTrace.Add($"t={stageTime:0.000} attacker={player.WorldPosition} victim={botObject.WorldPosition} down={victim.Downed}");
                if(stageTime<.9f)return;
                Check(attacker.Contacts==initialContacts+1 && attacker.ImpactSounds==initialSounds+1 && !victim.Downed,"opponent leaving reach avoids hit and sound");
                attacker.ReceiveContact(botObject.WorldPosition,true);Next();break;
            case 6:
                if(stageTime<1.2f)return;
                Check(attacker.Downed && !player.UseInputControls,"player knockdown locks movement");
                Check(!attacker.GetUp(),"cannot bypass required recovery presses");Next();break;
            case 7:
                button=stageTime<.8f;
                if(stageTime>1){Check(attacker.Downed && attacker.RecoveryPresses==0,"holding right mouse does not recover; stale press expires");Next();}
                break;
            case 8:
                button=(stageTime<.08f || (stageTime>.65f && stageTime<.73f));
                if(stageTime>.85f){Check(attacker.Downed && attacker.RecoveryPresses==1,"slow presses restart at one");Next();}
                break;
            case 9:
                if(stageTime<.5f)return;
                Check(attacker.RecoveryPresses==0,"pause breaks consecutive sequence");Next();break;
            case 10:
                button=stageTime<.72f && stageTime% .18f<.075f;
                if(stageTime>.71f){Check(attacker.Downed && attacker.RecoveryPresses==4,"four fast native taps keep player down");Next();}
                break;
            case 11:
                button=stageTime>.08f && stageTime<.16f;
                if(stageTime>.3f){Check(attacker.Recovering && !attacker.Downed && !player.UseInputControls,"fifth fast tap starts animated get-up");Next();}
                break;
            case 12:
                if(stageTime<1.45f)return;
                Check(!attacker.Incapacitated && player.UseInputControls,"get-up restores player movement");
                attacker.ReceiveContact(botObject.WorldPosition,true);Next();break;
            case 13:
                if(stageTime<1.2f)return;
                var controls=Scene.GetAllComponents<PaintballControls>().First();controls.Toggle();
                attacker.ObserveAimInput(true,true,false,.02f);
                Check(attacker.RecoveryPresses==0,"menu clicks cannot advance recovery");controls.Toggle();
                attacker.ResetState();Check(!attacker.Incapacitated && player.UseInputControls,"reset during knockdown restores controls");
                PlaceBot();victim.ReceiveContact(player.WorldPosition,true);bot.CombatEnabled=true;bot.Enabled=true;
                bot.Components.Get<NavMeshAgent>().UpdatePosition=true;stage=15;stageTime=0;break;
            case 15:
                if(stageTime<4.2f)return;
                Check(!victim.Incapacitated && victim.RecoveryPresses==5,"AI uses the same five-press recovery sequence");
                Done=true;button=false;break;
        }
    }
    private void Visual()
    {
        if(stage==0 && stageTime>.8f){if(PlayerFallVisual)attacker.ReceiveContact(player.WorldPosition+Vector3.Forward*50,true);else attacker.TryAttack();Next();}
        else if(stage==1 && stageTime>2.2f){Next();}
        else if(stage==2)
        {
            if(stageTime>.18f){victim.RegisterRecoveryPress();stageTime=0;}
            if(victim.Recovering)Next();
        }
        if(PauseAt>0 && Elapsed>=PauseAt){Scene.TimeScale=0;PauseAt=-1;}
    }
    private void ShieldSequence()
    {
        if(Elapsed>26){Check(false,"shield melee review timed out");Done=true;return;}
        switch(stage)
        {
            case 0:
                if(stageTime<.8f)return;
                Check(reviewShield.Collect(botActor),"shield collected at full strength");Next();break;
            case 1:
                if(stageTime<.3f)return;
                Check(attacker.TryAttack(),"first gun-butt strike starts against raised shield");Next();break;
            case 2:
                if(stageTime<.75f)return;
                Check(reviewShield.Remaining==2 && reviewShield.Owner==botActor,"first frontal strike leaves two shield charges");
                Check(!victim.Downed,"first shield strike is absorbed without knockdown");
                Check(attacker.Contacts==initialContacts+1 && attacker.ImpactSounds==initialSounds+1,"one confirmed shield contact and impact sound");
                Check(playerActor.HitsLanded==initialPlayerLanded && playerActor.PaintHits==initialPlayerHits && botActor.PaintHits==0,"shield melee adds no paint score");Next();break;
            case 3:
                if(stageTime<.65f)return;
                Check(attacker.TryAttack(),"second gun-butt strike starts after cooldown");Next();break;
            case 4:
                if(stageTime<.4f)return;
                Check(reviewShield.Remaining==0 && !reviewShield.Owner.IsValid(),"second strike breaks and releases shield");
                Check(victim.Downed,"shield-breaking strike knocks wearer down");
                Check(reviewShield.Shatters==1,"one shield break event");
                Check(Scene.GetAllComponents<ShieldBreakEffect>().Sum(x=>x.Fragments)==16,"native break emits all 16 curved fragments");
                Check(attacker.Contacts==initialContacts+2 && playerActor.HitsLanded==initialPlayerLanded && playerActor.PaintHits==initialPlayerHits && botActor.PaintHits==0,"two contacts without paint scoring");Next();break;
            case 5:
                if(stageTime<4.3f)return;
                Check(!Scene.GetAllComponents<ShieldBreakEffect>().Any(),"fragments expire without leaving colliders or objects");
                Check(reviewShield.Remaining==5 && !reviewShield.Owner.IsValid() && reviewShield.RespawnDelay<=0,"broken shield respawns at five charges");
                Check(reviewShield.Shatters==1,"respawn does not replay shatter");
                victim.ResetState();reviewShield.Collect(botActor);Next();break;
            case 6:
                if(stageTime<.25f)return;
                var front=botActor.WorldPosition+reviewShield.WorldRotation.Forward*40;
                var rear=botActor.WorldPosition-reviewShield.WorldRotation.Forward*40;
                Check(ArenaShield.TryAbsorbMelee(botActor,rear)==ArenaShield.MeleeResult.None && reviewShield.Remaining==5,"rear strike bypasses shield without damaging it");
                victim.ReceiveContact(rear,true);
                Check(victim.Downed && !reviewShield.Owner.IsValid() && reviewShield.Remaining==0,"AI knockdown immediately forfeits a full shield");
                Check(reviewShield.Shatters==1 && reviewShield.RespawnDelay>0,"forfeited shield uses respawn delay without an extra damage shatter");
                Check(ArenaShield.TryAbsorbMelee(botActor,front)==ArenaShield.MeleeResult.None,"lost shield provides no protection");
                Next();break;
            case 7:
                if(stageTime<4.3f)return;
                Check(reviewShield.Remaining==5 && !reviewShield.Owner.IsValid(),"forfeited shield returns as an unowned pickup");
                Check(!reviewShield.Collect(botActor),"downed AI cannot collect a fresh shield");
                victim.ResetState();
                Check(!reviewShield.Owner.IsValid(),"AI recovery does not restore lost shield");
                Check(reviewShield.Collect(playerActor),"standing player can collect the returned shield");
                attacker.ReceiveContact(botObject.WorldPosition,true);
                Check(attacker.Downed && !reviewShield.Owner.IsValid() && reviewShield.Remaining==0,"player knockdown also forfeits a full shield");
                Next();break;
            case 8:
                if(stageTime<4.3f)return;
                Check(!reviewShield.Collect(playerActor),"downed player cannot collect a fresh shield");
                attacker.ResetState();
                Check(!reviewShield.Owner.IsValid(),"player recovery does not restore lost shield");
                Check(reviewShield.Collect(botActor),"standing AI can collect another shield");
                reviewShield.Absorb();
                Check(reviewShield.Remaining==4,"paintball still consumes one shield charge");
                var raisedFront=botActor.WorldPosition+reviewShield.WorldRotation.Forward*40;
                Check(ArenaShield.TryAbsorbMelee(botActor,raisedFront)==ArenaShield.MeleeResult.Absorbed && reviewShield.Remaining==1,"melee and paintball shield damage combine");
                Check(ArenaShield.TryAbsorbMelee(botActor,raisedFront)==ArenaShield.MeleeResult.Broken && reviewShield.Remaining==0,"weakened shield breaks without negative durability");
                Done=true;break;
        }
        if(PauseAt>0 && Elapsed>=PauseAt){Scene.TimeScale=0;PauseAt=-1;}
    }
    protected override void OnDestroy()
    {
        if(reviewShield.IsValid())reviewShield.GameObject.Destroy();
        inputHook?.Dispose();Input.ReleaseActions();Scene.TimeScale=1;
        if(player.IsValid())
        {
            attacker?.ResetState();player.WorldPosition=oldPosition;player.EyeAngles=oldAngles;
            player.UseLookControls=oldLook;player.UseCameraControls=oldCamera;
            player.Body.Velocity=Vector3.Zero;player.WishVelocity=Vector3.Zero;
        }
    }
}