StaticPropReview.cs
using Sandbox;
namespace PaintballBaddies;

/// <summary>Temporary native material/scale review; never saved in playable scenes.</summary>
public sealed class StaticPropReview : Component
{
    [Property] public string ModelPath { get; set; } = "models/foundry/upright_spool/upright_spool.vmdl";
    [Property] public bool SmallProp {get;set;}
    [Property] public bool VisualOnly { get; set; }
    [Property] public bool TallProp { get; set; }
    [Property] public bool CheckNet { get; set; }
    [Property] public bool CheckTankCollision { get; set; }
    [Property] public bool CheckTankVault { get; set; }
    [Property] public string CandidateCharacterModel { get; set; }
    [Property] public bool CheckPlanner { get; set; }
    [Property] public int CharacterIndex { get; set; }
    private int headSamples, shelteredHeadSamples;
    private float maxHeadHeight;
    [Property] public bool UsePlaced { get; set; }
    [Property] public string ReviewObjectName { get; set; } = "Cable spool - west approach";
    [Property] public bool CheckCover { get; set; }
    private PlayerController player;
    private CoverController cover;
    private int coverStage;
    private Vector3 slideStart;
    private float maxLift;
    private GameObject subject;
    private float elapsed;
    private bool checkedCollision;
    protected override void OnStart()
    {
        player=Scene.GetAllComponents<PlayerController>().First();
        player.UseCameraControls=false;player.UseInputControls=false;
        ModelRenderer model;
        if(UsePlaced)
        {
            model=Scene.GetAllComponents<ModelRenderer>().First(x=>x.GameObject.Name==ReviewObjectName);subject=model.GameObject;
        }
        else
        {
            subject=new GameObject(true,"Static prop review");subject.WorldPosition=new Vector3(-850,-400,0);
            model=subject.Components.Create<ModelRenderer>();model.Model=Model.Load(ModelPath);
            var collider=subject.Components.Create<ModelCollider>();collider.Model=model.Model;
            var surface=subject.Components.Create<CoverSurface>();surface.Curved=!CheckTankCollision;surface.ModelHeight=CheckTankCollision ? 52.36f : 48.47f;
        }
        WorldPosition=subject.WorldPosition+(CheckTankVault ? new Vector3(170,-220,160) : new Vector3(95,-125,90));
        WorldRotation=Rotation.LookAt(subject.WorldPosition+Vector3.Up*(CheckTankVault ? 60 : 21)-WorldPosition);
        if(SmallProp){WorldPosition=subject.WorldPosition+new Vector3(35,-40,30);WorldRotation=Rotation.LookAt(subject.WorldPosition+Vector3.Up*8-WorldPosition);}
        if(TallProp)
        {
            WorldPosition=subject.WorldPosition+new Vector3(150,-135,85);
            WorldRotation=Rotation.LookAt(subject.WorldPosition+Vector3.Up*50-WorldPosition);
        }
        if(CheckCover)
        {
            cover=player.Components.Get<CoverController>();cover.TestInput=true;
            player.UseLookControls=false;
            player.WorldPosition=subject.WorldPosition+new Vector3(-55,CheckTankCollision ? 25 : 0,2);
            player.EyeAngles=new Angles(0,0,0);player.Body.Velocity=Vector3.Zero;
        }
        Log.Info($"PROP_REVIEW loaded={model.Model.IsValid()} model={model.Model?.Name} bounds={model.Model?.Bounds}");
    }
    protected override void OnUpdate()
    {
        elapsed+=Time.Delta;
        if(CheckNet && !checkedCollision && elapsed>.3f)
        {
            checkedCollision=true;
            foreach(var height in new[]{5f,50f,100f})
            {
                var from=subject.WorldPosition+new Vector3(40,0,height);
                var hit=Scene.Trace.Ray(from,from-Vector3.Forward*80).Run();
                Log.Info($"NET_CHECK {(hit.Hit && hit.GameObject==subject ? "PASS" : "FAIL")} barrier at height={height}");
            }
            foreach(var netSide in new[]{-85f,85f})
            {
                var from=subject.WorldPosition+new Vector3(40,netSide,35);
                var hit=Scene.Trace.Ray(from,from-Vector3.Forward*80).Run();
                Log.Info($"NET_CHECK {(!hit.Hit ? "PASS" : "FAIL")} side access y={netSide}");
            }
            var noAttach=subject.Components.Get<CoverSurface>() is null;
            Log.Info($"NET_CHECK {(noAttach ? "PASS" : "FAIL")} no cover attachment on net");
        }
        if(CheckCover)
        {
            if(coverStage==0 && elapsed>.7f)
            {
                player.Components.Get<RosterSelection>().Select(CharacterIndex);
                if(!string.IsNullOrEmpty(CandidateCharacterModel))player.Renderer.Model=Model.Load(CandidateCharacterModel);
                var entered=cover.TryEnter();slideStart=player.WorldPosition;coverStage=1;
                Log.Info($"SPOOL_COVER {(entered && cover.LowCover ? "PASS" : "FAIL")} low cover attachment");
            }
            if(coverStage==1)
            {
                if(elapsed>1.2f && player.Renderer.TryGetBoneTransform("Head",out var head))
                {
                    headSamples++;maxHeadHeight=System.MathF.Max(maxHeadHeight,head.Position.z-subject.WorldPosition.z);
                    var outward=(player.WorldPosition-subject.WorldPosition).WithZ(0).Normal;
                    var from=(subject.WorldPosition-outward*100).WithZ(head.Position.z);
                    var hit=Scene.Trace.Ray(from,head.Position).IgnoreGameObjectHierarchy(player.GameObject).Run();
                    if(hit.Hit && hit.GameObject==subject)shelteredHeadSamples++;
                }
                maxLift=System.MathF.Max(maxLift,player.WorldPosition.z-subject.WorldPosition.z);
                cover.TestMovement=Vector3.Cross(Vector3.Up,cover.Normal).Normal;
                if(elapsed>3.2f)
                {
                    var moved=(player.WorldPosition-slideStart).WithZ(0).Length;
                    Log.Info($"SPOOL_COVER {(cover.Attached && moved>45 && maxLift<8 ? "PASS" : "FAIL")} cover slide distance={moved} max_lift={maxLift}");
                    Log.Info($"SPOOL_COVER {(headSamples>30 && shelteredHeadSamples>=headSamples*.95f ? "PASS" : "FAIL")} head shelter samples={shelteredHeadSamples}/{headSamples} max_head={maxHeadHeight} character={CharacterIndex}");
                    cover.TestMovement=Vector3.Zero;cover.TestAim=true;coverStage=2;
                }
            }
            if(coverStage==2 && elapsed>4.2f)
            {
                Log.Info($"SPOOL_COVER {(cover.Attached && cover.Peeking ? "PASS" : "FAIL")} low cover peek");
                if(CheckTankVault)
                {
                    cover.TestAim=false;
                    var vault=player.Components.Get<VaultController>();
                    Log.Info($"TANK_VAULT {(vault.TryBegin() ? "PASS" : "FAIL")} begins from tank side cover");
                }
                else cover.Leave();
                cover.TestInput=false;coverStage=3;
            }
            if(CheckTankVault && coverStage==3 && elapsed>5.4f)
            {
                var vault=player.Components.Get<VaultController>();
                bool landed=vault.Outcome=="Landed" && !vault.IsVaulting && player.WorldPosition.x>subject.WorldPosition.x+20 && player.Body.MotionEnabled;
                Log.Info($"TANK_VAULT {(landed ? "PASS" : "FAIL")} far-side landing outcome={vault.Outcome} relative={player.WorldPosition-subject.WorldPosition} model={player.Renderer.Model?.Name}");
                coverStage=4;
            }
        }
        if(CheckTankCollision && CheckPlanner && !checkedCollision && elapsed>.3f)
        {
            checkedCollision=true;
            var actor=new GameObject(true,"Tank planner probe");
            for(int i=0;i<8;i++)
            {
                var away=Rotation.FromYaw(i*45).Forward;
                var threat=subject.WorldPosition-away*200;
                actor.WorldPosition=subject.WorldPosition+away*120+Vector3.Up;
                bool found=OpponentCoverPlanner.TryFind(actor,player.GameObject,threat,out var destination);
                var ray=Scene.Trace.Ray(threat+Vector3.Up*55,destination+Vector3.Up*30).IgnoreGameObjectHierarchy(actor).IgnoreGameObjectHierarchy(player.GameObject).Run();
                bool tankShelters=found && ray.Hit && ray.GameObject==subject;
                Log.Info($"TANK_PLANNER {(tankShelters ? "PASS" : "FAIL")} approach={i*45} found={found} tankShelter={tankShelters} destination={destination}");
            }
            actor.Destroy();
        }
        if(CheckTankCollision && !checkedCollision && elapsed>.3f)
        {
            checkedCollision=true;
            foreach(var test in new[]{(0f,30f,true),(0f,5f,false),(-29.53f,5f,true),(29.53f,5f,true)})
            {
                var from=subject.WorldPosition+new Vector3(-60,test.Item1,test.Item2);
                var to=subject.WorldPosition+new Vector3(60,test.Item1,test.Item2);
                var trace=Scene.Trace.Ray(from,to).IgnoreGameObjectHierarchy(player.GameObject).Run();
                bool hitSubject=trace.Hit && trace.GameObject==subject;
                Log.Info($"TANK_COLLISION {(hitSubject==test.Item3 ? "PASS" : "FAIL")} y={test.Item1} z={test.Item2} expected={test.Item3} actual={hitSubject}");
            }
        }
        if(VisualOnly || checkedCollision || elapsed<.3f)return;
        checkedCollision=true;
        if(CheckPlanner)
        {
            var actor=new GameObject(true,"Spool planner probe");
            for(int i=0;i<8;i++)
            {
                var away=Rotation.FromYaw(i*45).Forward;
                var threat=subject.WorldPosition-away*200;
                actor.WorldPosition=subject.WorldPosition+away*110+Vector3.Up;
                bool found=OpponentCoverPlanner.TryFind(actor,player.GameObject,threat,out var destination);
                bool near=found && (destination-subject.WorldPosition).WithZ(0).Length<70;
                bool sheltered=found && OpponentCoverPlanner.IsSheltered(actor,player.GameObject,threat,destination);
                Log.Info($"SPOOL_PLANNER {(found && near && sheltered ? "PASS" : "FAIL")} approach={i*45} found={found} near={near} shelter={sheltered} destination={destination}");
            }
            actor.Destroy();
        }
        foreach(var height in new[]{2f,28f,47f})
        {
            var origin=subject.WorldPosition+new Vector3(-60,0,height);
            var hit=Scene.Trace.Ray(origin,origin+Vector3.Forward*120).Run();
            Log.Info($"PROP_COLLISION {(hit.Hit && hit.GameObject==subject ? "PASS" : "FAIL")} solid at height={height} point={hit.EndPosition}");
        }
        var side=subject.WorldPosition+new Vector3(-60,24,28);
        var gap=Scene.Trace.Ray(side,side+Vector3.Forward*120).Run();
        Log.Info($"PROP_COLLISION {(!gap.Hit ? "PASS" : "FAIL")} open waist below flange");
    }
    protected override void OnDestroy()
    {
        if(!UsePlaced && subject.IsValid())subject.Destroy();
    }
}