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 CashBag 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(CashBag 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) { CashBag 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(CashBag 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; } }