275 lines
9.7 KiB
C#
275 lines
9.7 KiB
C#
using UnityEngine;
|
|
using System.Collections.Generic;
|
|
|
|
/// <summary>
|
|
/// AIBlackboard — Per-AI knowledge store.
|
|
/// Holds perception results, targets, and state without allocations.
|
|
/// Updated by AIPerceptionSystem and read by AIActions/UtilityScorer.
|
|
/// </summary>
|
|
[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<PerceivedEntity> perceivedEnemies = new List<PerceivedEntity>(8);
|
|
public readonly List<PerceivedEntity> perceivedAllies = new List<PerceivedEntity>(8);
|
|
public readonly List<CashMag> nearbyAvailableBags = new List<CashMag>(8);
|
|
public readonly List<CashMag> nearbyDroppedBags = new List<CashMag>(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;
|
|
|
|
/// <summary>Clear all perception data (called before perception update).</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Data about a perceived entity (enemy or ally).
|
|
/// Reused from pool to avoid allocation.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// TeamBlackboard — Shared knowledge between all AI on a team.
|
|
/// Prevents clustering and enables coordination.
|
|
/// Singleton per team — accessed via TeamBlackboard.Get(teamTag).
|
|
/// </summary>
|
|
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<TeamBlackboard>();
|
|
_playerTeam._teamTag = "Player";
|
|
}
|
|
return _playerTeam;
|
|
}
|
|
else
|
|
{
|
|
if (_enemyTeam == null)
|
|
{
|
|
var go = new GameObject("TeamBlackboard_Enemy");
|
|
_enemyTeam = go.AddComponent<TeamBlackboard>();
|
|
_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;
|
|
|
|
/// <summary>Which bags are claimed by AI (prevents all AI chasing same bag).</summary>
|
|
public readonly Dictionary<CashMag, AIBrain> claimedBags = new Dictionary<CashMag, AIBrain>(8);
|
|
|
|
/// <summary>Which enemies are being targeted (limit engagement per enemy).</summary>
|
|
public readonly Dictionary<Transform, int> targetedEnemies = new Dictionary<Transform, int>(8);
|
|
|
|
/// <summary>Who on our team is carrying bags?</summary>
|
|
public readonly List<CashCarrier> teamCarriers = new List<CashCarrier>(4);
|
|
|
|
/// <summary>Is our vault under attack?</summary>
|
|
public bool vaultUnderAttack;
|
|
|
|
/// <summary>Our team's score.</summary>
|
|
public float teamScore;
|
|
|
|
// ─── Claim System ────────────────────────────────────────
|
|
|
|
/// <summary>Try to claim a bag for this AI. Returns true if claimed successfully.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Release claim on a bag.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Is this bag already claimed by a teammate?</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Register an enemy as being targeted. Returns current count.</summary>
|
|
public int AddEnemyTarget(Transform enemy)
|
|
{
|
|
if (enemy == null) return 0;
|
|
if (!targetedEnemies.ContainsKey(enemy))
|
|
targetedEnemies[enemy] = 0;
|
|
targetedEnemies[enemy]++;
|
|
return targetedEnemies[enemy];
|
|
}
|
|
|
|
/// <summary>Unregister from targeting an enemy.</summary>
|
|
public void RemoveEnemyTarget(Transform enemy)
|
|
{
|
|
if (enemy == null) return;
|
|
if (targetedEnemies.ContainsKey(enemy))
|
|
{
|
|
targetedEnemies[enemy]--;
|
|
if (targetedEnemies[enemy] <= 0)
|
|
targetedEnemies.Remove(enemy);
|
|
}
|
|
}
|
|
|
|
/// <summary>How many teammates are targeting this enemy?</summary>
|
|
public int GetEnemyTargetCount(Transform enemy)
|
|
{
|
|
if (enemy == null) return 0;
|
|
return targetedEnemies.TryGetValue(enemy, out int count) ? count : 0;
|
|
}
|
|
|
|
/// <summary>Update team carriers list.</summary>
|
|
public void RefreshCarriers()
|
|
{
|
|
teamCarriers.Clear();
|
|
var allCarriers = EntityRegistry<CashCarrier>.All;
|
|
for (int i = 0; i < allCarriers.Count; i++)
|
|
{
|
|
if (allCarriers[i] != null && allCarriers[i].TeamTag == _teamTag && allCarriers[i].IsCarrying)
|
|
teamCarriers.Add(allCarriers[i]);
|
|
}
|
|
}
|
|
|
|
/// <summary>Clean stale entries (call periodically).</summary>
|
|
public void CleanStaleEntries()
|
|
{
|
|
// Clean bag claims
|
|
var staleBags = new List<CashMag>();
|
|
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<Transform>();
|
|
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;
|
|
}
|
|
}
|