using UnityEngine;
using System.Collections.Generic;
///
/// AIBlackboard — Per-AI knowledge store.
/// Holds perception results, targets, and state without allocations.
/// Updated by AIPerceptionSystem and read by AIActions/UtilityScorer.
///
[System.Serializable]
public class AIBlackboard
{
// ─── Identity ────────────────────────────────────────────
public string teamTag;
public TeamMember.Team team;
// ─── Current Targets ─────────────────────────────────────
public Transform currentTarget;
public CashMag targetBag;
public TeamVault myVault;
public TeamVault enemyVault;
public CashCarrier nearestEnemyCarrier; // Priority target
public Transform escortTarget; // Ally to escort
// ─── Self State ──────────────────────────────────────────
public float currentHealth;
public float maxHealth;
public float healthPercent;
public bool isDead;
public int carriedBagCount;
public float totalCarriedValue;
public bool isCarrying;
// ─── Perception Results (updated by AIPerceptionSystem) ──
public readonly List perceivedEnemies = new List(8);
public readonly List perceivedAllies = new List(8);
public readonly List nearbyAvailableBags = new List(8);
public readonly List nearbyDroppedBags = new List(4);
public CashCarrier nearestAllyCarrier;
public int nearbyEnemyCount;
public int nearbyAllyCount;
public float nearestEnemyDistance;
public float nearestBagDistance;
// ─── Team State (from TeamBlackboard) ────────────────────
public float myTeamScore;
public float enemyTeamScore;
public bool myVaultUnderAttack;
public float scoreDifference; // positive = winning
// ─── Navigation ──────────────────────────────────────────
public Vector3 patrolDestination;
public bool hasPatrolDestination;
public float timeAtCurrentState;
/// Clear all perception data (called before perception update).
public void ClearPerception()
{
perceivedEnemies.Clear();
perceivedAllies.Clear();
nearbyAvailableBags.Clear();
nearbyDroppedBags.Clear();
nearestEnemyCarrier = null;
nearestAllyCarrier = null;
nearbyEnemyCount = 0;
nearbyAllyCount = 0;
nearestEnemyDistance = float.MaxValue;
nearestBagDistance = float.MaxValue;
}
}
///
/// Data about a perceived entity (enemy or ally).
/// Reused from pool to avoid allocation.
///
[System.Serializable]
public struct PerceivedEntity
{
public Transform transform;
public GameObject gameObject;
public float distance;
public float sqrDistance;
public float healthPercent;
public bool isCarryingBag;
public int carriedBagCount;
public bool isVisible; // In line of sight
public Vector3 lastKnownPosition;
public float timeSinceLastSeen;
}
///
/// TeamBlackboard — Shared knowledge between all AI on a team.
/// Prevents clustering and enables coordination.
/// Singleton per team — accessed via TeamBlackboard.Get(teamTag).
///
public class TeamBlackboard : MonoBehaviour
{
// ─── Static Access ───────────────────────────────────────
private static TeamBlackboard _playerTeam;
private static TeamBlackboard _enemyTeam;
public static TeamBlackboard Get(string teamTag)
{
if (teamTag == "Player")
{
if (_playerTeam == null)
{
var go = new GameObject("TeamBlackboard_Player");
_playerTeam = go.AddComponent();
_playerTeam._teamTag = "Player";
}
return _playerTeam;
}
else
{
if (_enemyTeam == null)
{
var go = new GameObject("TeamBlackboard_Enemy");
_enemyTeam = go.AddComponent();
_enemyTeam._teamTag = "Enemy";
}
return _enemyTeam;
}
}
public static void ClearAll()
{
if (_playerTeam != null) { Destroy(_playerTeam.gameObject); _playerTeam = null; }
if (_enemyTeam != null) { Destroy(_enemyTeam.gameObject); _enemyTeam = null; }
}
// ─── Team Data ───────────────────────────────────────────
[SerializeField] private string _teamTag;
/// Which bags are claimed by AI (prevents all AI chasing same bag).
public readonly Dictionary claimedBags = new Dictionary(8);
/// Which enemies are being targeted (limit engagement per enemy).
public readonly Dictionary targetedEnemies = new Dictionary(8);
/// Who on our team is carrying bags?
public readonly List teamCarriers = new List(4);
/// Is our vault under attack?
public bool vaultUnderAttack;
/// Our team's score.
public float teamScore;
// ─── Claim System ────────────────────────────────────────
/// Try to claim a bag for this AI. Returns true if claimed successfully.
public bool TryClaimBag(CashMag bag, AIBrain claimant)
{
if (bag == null) return false;
// Clean stale claims
if (claimedBags.TryGetValue(bag, out AIBrain existing))
{
if (existing == null || !existing.gameObject.activeInHierarchy)
{
claimedBags[bag] = claimant;
return true;
}
return existing == claimant; // Already claimed by us
}
claimedBags[bag] = claimant;
return true;
}
/// Release claim on a bag.
public void ReleaseBagClaim(AIBrain claimant)
{
CashMag toRemove = null;
foreach (var kvp in claimedBags)
{
if (kvp.Value == claimant) { toRemove = kvp.Key; break; }
}
if (toRemove != null) claimedBags.Remove(toRemove);
}
/// Is this bag already claimed by a teammate?
public bool IsBagClaimed(CashMag bag, AIBrain excludeSelf = null)
{
if (!claimedBags.TryGetValue(bag, out AIBrain claimant)) return false;
if (claimant == null || !claimant.gameObject.activeInHierarchy)
{
claimedBags.Remove(bag);
return false;
}
return claimant != excludeSelf;
}
/// Register an enemy as being targeted. Returns current count.
public int AddEnemyTarget(Transform enemy)
{
if (enemy == null) return 0;
if (!targetedEnemies.ContainsKey(enemy))
targetedEnemies[enemy] = 0;
targetedEnemies[enemy]++;
return targetedEnemies[enemy];
}
/// Unregister from targeting an enemy.
public void RemoveEnemyTarget(Transform enemy)
{
if (enemy == null) return;
if (targetedEnemies.ContainsKey(enemy))
{
targetedEnemies[enemy]--;
if (targetedEnemies[enemy] <= 0)
targetedEnemies.Remove(enemy);
}
}
/// How many teammates are targeting this enemy?
public int GetEnemyTargetCount(Transform enemy)
{
if (enemy == null) return 0;
return targetedEnemies.TryGetValue(enemy, out int count) ? count : 0;
}
/// Update team carriers list.
public void RefreshCarriers()
{
teamCarriers.Clear();
var allCarriers = EntityRegistry.All;
for (int i = 0; i < allCarriers.Count; i++)
{
if (allCarriers[i] != null && allCarriers[i].TeamTag == _teamTag && allCarriers[i].IsCarrying)
teamCarriers.Add(allCarriers[i]);
}
}
/// Clean stale entries (call periodically).
public void CleanStaleEntries()
{
// Clean bag claims
var staleBags = new List();
foreach (var kvp in claimedBags)
{
if (kvp.Key == null || kvp.Value == null || !kvp.Value.gameObject.activeInHierarchy)
staleBags.Add(kvp.Key);
}
for (int i = 0; i < staleBags.Count; i++)
claimedBags.Remove(staleBags[i]);
// Clean enemy targets
var staleTargets = new List();
foreach (var kvp in targetedEnemies)
{
if (kvp.Key == null) staleTargets.Add(kvp.Key);
}
for (int i = 0; i < staleTargets.Count; i++)
targetedEnemies.Remove(staleTargets[i]);
}
private float _lastCleanTime;
private void Update()
{
if (Time.time - _lastCleanTime > 3f)
{
CleanStaleEntries();
RefreshCarriers();
_lastCleanTime = Time.time;
}
}
private void OnDestroy()
{
if (_playerTeam == this) _playerTeam = null;
if (_enemyTeam == this) _enemyTeam = null;
}
}