ai/ai_blackboard.cs
// AiBlackboard.cs
using Sandbox;

public sealed class AiBlackboard : Component
{
    [Property, Group( "Targeting" )] public GameObject Target { get; set; }
    [Property, Group( "Targeting" )] public float TargetScanInterval { get; set; } = 0.35f;
    [Property, Group( "Targeting" )] public float WeaponScanInterval { get; set; } = 0.40f;
    [Property, Group( "Targeting" )] public float MaxEngageDistance { get; set; } = 1500f;

    public Fighter TargetFighter { get; private set; }
    public float DistanceToTarget { get; private set; }
    public Vector3 DirectionToTarget { get; private set; }

    public GameObject NearestWeaponDrop { get; private set; }
    public GameObject NearestShieldDrop { get; private set; }

    public GameObject DesiredEquipmentDrop
    {
        get
        {
            var combat = Components.Get<FighterCombat>();
            if ( combat == null ) return NearestWeaponDrop ?? NearestShieldDrop;

            if ( combat.MainWeapon == null )
                return NearestWeaponDrop ?? NearestShieldDrop;

            if ( combat.ShieldItem == null )
                return NearestShieldDrop;

            return null;
        }
    }

    public bool HasValidTarget => Target != null && Target.IsValid && TargetFighter != null && !TargetFighter.IsDead;

    private Fighter _myFighter;
    private TimeSince _targetScanTimer = 0f;
    private TimeSince _weaponScanTimer = 0f;

    protected override void OnAwake()
    {
        _myFighter = Components.Get<Fighter>();
    }

    protected override void OnUpdate()
    {
        // 1. Scan et mise à jour de la cible prioritaire en mêlée
        if ( _targetScanTimer >= TargetScanInterval || !HasValidTarget )
        {
            _targetScanTimer = 0f;
            SelectBestMeleeTarget();
        }

        // 2. Vecteurs vers la cible
        if ( HasValidTarget )
        {
            var delta = Target.WorldPosition - WorldPosition;
            DistanceToTarget = delta.Length;
            DirectionToTarget = DistanceToTarget > 0.001f ? delta / DistanceToTarget : Vector3.Forward;
        }
        else
        {
            Target = null;
            TargetFighter = null;
            DistanceToTarget = float.MaxValue;
            DirectionToTarget = Vector3.Forward;
        }

        // 3. Scan des armes au sol
        if ( _weaponScanTimer >= WeaponScanInterval )
        {
            _weaponScanTimer = 0f;

            var nearestWeapon = SpatialQuery.GetNearest<Weapon>(
                WorldPosition,
                maxDistance: 1200f,
                filter: w => !w.IsEquipped && w.GameObject.IsValid && w.Type != WeaponType.Shield
            );
            NearestWeaponDrop = nearestWeapon?.GameObject;

            var nearestShield = SpatialQuery.GetNearest<Weapon>(
                WorldPosition,
                maxDistance: 1200f,
                filter: w => !w.IsEquipped && w.GameObject.IsValid && w.Type == WeaponType.Shield
            );
            NearestShieldDrop = nearestShield?.GameObject;
        }
    }

    private void SelectBestMeleeTarget()
    {
        Fighter bestCandidate = null;
        float highestScore = float.MinValue;

        foreach ( var candidate in Scene.GetAllComponents<Fighter>() )
        {
            if ( !candidate.IsValid || candidate == _myFighter || candidate.IsDead )
                continue;

            var dist = (candidate.WorldPosition - WorldPosition).Length;
            if ( dist > MaxEngageDistance )
                continue;

            // Système de score d'opportunité
            float score = 1000f - dist;

            // Bonus de persistance (évite de changer de cible à chaque coup)
            if ( TargetFighter == candidate )
                score += 220f;

            // Cible au sol ou étourdie = proie prioritaire
            if ( candidate.Status == FighterStatus.KnockedDown || candidate.Status == FighterStatus.Staggered )
                score += 350f;

            // Cible dont la posture vacille
            if ( candidate.BalanceRatio < 0.35f )
                score += 150f;

            if ( score > highestScore )
            {
                highestScore = score;
                bestCandidate = candidate;
            }
        }

        TargetFighter = bestCandidate;
        Target = bestCandidate?.GameObject;
    }
}