AudioPlaybackReview.cs
using Sandbox;
using Sandbox.Audio;
namespace PaintballBaddies;

/// <summary>Opt-in native audio observation; no microphones or system audio recording.</summary>
public sealed class AudioPlaybackReview : Component
{
    public bool Audition { get; set; }
    public bool Done { get; private set; }
    public sealed class Observation
    {
        public string File { get; set; }
        public string Name { get; set; }
        public float Volume { get; set; }
        public bool Played { get; set; }
        public float Amplitude { get; set; }
        public float MixerPeak { get; set; }
    }
    public List<Observation> Sounds { get; }=new();
    private readonly Dictionary<SoundHandle,Observation> observed=new();
    private readonly List<SoundHandle> handles=new();
    private readonly List<SoundHandle> auditionHandles=new();
    private float elapsed,next=.5f;
    private int index;
    private GameObject[] knockTargets;
    private float knockAt=-1;
    private int knockStage;
    public List<string> KnockdownChecks { get; }=new();
    public void CheckKnockdowns()
    {
        var camera=Scene.Camera;
        knockTargets=new[]{24f,900f}.Select(distance=>
        {
            var go=new GameObject(GameObject){Name="Temporary knockdown voice receiver",WorldPosition=camera.WorldPosition+camera.WorldRotation.Forward*distance};
            go.Components.Create<PaintballCombatant>();go.Components.Create<CharacterVoice>();go.Components.Create<CloseCombat>();
            return go;
        }).ToArray();
        knockAt=elapsed+.2f;knockStage=0;
    }
    private void Check(bool ok,string label)=>KnockdownChecks.Add((ok ? "PASS " : "FAIL ")+label);
    private void AdvanceKnockdownChecks()
    {
        if(knockAt<0 || elapsed<knockAt)return;
        var near=knockTargets[0].Components.Get<CharacterVoice>();var far=knockTargets[1].Components.Get<CharacterVoice>();
        var contact=knockTargets[0].Components.Get<CloseCombat>();
        if(knockStage==0)
        {
            Check(!near.TryPlay(),"ordinary voice respects initial cooldown");
            contact.ReceiveContact(knockTargets[0].WorldPosition-Vector3.Forward*35,true);
            Check(near.KnockdownPlays==1,"confirmed knockdown bypasses ordinary cooldown");
            knockTargets[1].Components.Get<CloseCombat>().ReceiveContact(knockTargets[1].WorldPosition-Vector3.Forward*35,true);
            Check(far.KnockdownPlays==1,"another knockdown bypasses shared gap and old 650-unit camera cutoff");
            contact.ReceiveContact(knockTargets[0].WorldPosition-Vector3.Forward*35,true);
            Check(near.KnockdownPlays==1,"already downed actor does not repeat voice");
            Check(!near.TryPlay(),"knockdown does not enable extra ordinary chatter");
            knockStage=1;knockAt=elapsed+.3f;
        }
        else
        {
            contact.ResetState();contact.ReceiveContact(knockTargets[0].WorldPosition-Vector3.Forward*35,true);
            Check(near.KnockdownPlays==2,"next confirmed knockdown speaks during long character cooldown");
            knockAt=-1;
        }
    }
    private PaintballCombatant scoreReceiver;
    private float scoringShotAt=-1;
    public bool ShotFired { get; private set; }
    public int ScoringHits=>scoreReceiver?.PaintHits ?? 0;
    public void CheckScoring()
    {
        var player=Scene.GetAllComponents<PlayerController>().First();
        var weapon=player.Components.Get<PaintballMarker>();
        if(scoreReceiver.IsValid())scoreReceiver.GameObject.Destroy();
        var target=TrainingRange.Box(GameObject,"Audio scoring receiver",weapon.Muzzle+player.EyeAngles.Forward*80,new Vector3(16,16,24),Color.Gray,true);
        scoreReceiver=target.Components.Create<PaintballCombatant>();scoreReceiver.Team=91;scoreReceiver.StayInMatch=true;
        scoringShotAt=elapsed+.2f;ShotFired=false;
    }
    private string[] Events=>new[]{
        CharacterMarkerSound.For(0),CharacterMarkerSound.For(1),CharacterMarkerSound.For(2),
        CharacterMarkerSound.For(3),CharacterMarkerSound.For(4),CharacterMarkerSound.For(5),
        "sounds/paintball/hit_confirm.sound",CloseCombat.ImpactSound,CloseCombat.ImpactSound,
        CharacterVoice.EffortSound,CharacterVoice.EffortSound,CharacterVoice.EffortSound,CharacterVoice.EffortSound,
        "sounds/paintball/shield_collect.sound","sounds/paintball/shield_break.sound"};
    protected override void OnUpdate()
    {
        elapsed+=Time.Delta;
        AdvanceKnockdownChecks();
        if(scoringShotAt>=0 && elapsed>=scoringShotAt)
        {
            scoringShotAt=-1;
            ShotFired=Scene.GetAllComponents<PlayerController>().First().Components.Get<PaintballMarker>().FireAt(scoreReceiver.WorldPosition);
        }
        if(Audition && elapsed>=next && !Done)
        {
            if(index>=Events.Length)Done=true;
            else
            {
                // Nearby unoccluded playback through the normal event and mixer.
                var camera=Scene.Camera;
                auditionHandles.Add(Sound.Play(Events[index++],camera.WorldPosition+camera.WorldRotation.Forward*24));
                next=elapsed+1.1f;
            }
        }
        handles.Clear();SoundHandle.GetActive(handles);
        foreach(var handle in handles)
        {
            var file=handle.SoundFile?.ResourcePath;
            if(file is null || (!file.StartsWith("sounds/paintball/") && !file.StartsWith("sounds/voices/") && !file.StartsWith("sounds/contact/")))continue;
            if(!observed.TryGetValue(handle,out var entry))
            {
                entry=new(){File=file,Name=handle.Name,Volume=handle.Volume};
                observed.Add(handle,entry);Sounds.Add(entry);
            }
            entry.Played|=handle.IsPlaying;
            entry.Amplitude=System.MathF.Max(entry.Amplitude,handle.Amplitude);
            var meter=(handle.TargetMixer ?? Mixer.Default).Meter;
            if(meter is not null)
                entry.MixerPeak=System.MathF.Max(entry.MixerPeak,System.MathF.Max(meter.Current.MaxLevelLeft,meter.Current.MaxLevelRight));
        }
    }
    protected override void OnDestroy(){foreach(var handle in auditionHandles)handle?.Stop();}
}