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;
}
}