353 lines
12 KiB
C#
353 lines
12 KiB
C#
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
|
|
/// <summary>
|
|
/// AIBrain — Central AI orchestrator. Replaces CashSystemAI.
|
|
/// Runs perception → utility scoring → action execution on a configurable tick.
|
|
///
|
|
/// Features:
|
|
/// - AI LOD (distance-based tick scaling)
|
|
/// - Zero FindObjectOfType calls
|
|
/// - Modular scoring via UtilityScorer
|
|
/// - Delegates combat to existing CharacterAIController
|
|
/// - Delegates navigation to AINavigationHelper
|
|
///
|
|
/// Attach to all AI characters (both player-team AI and enemy-team AI).
|
|
/// </summary>
|
|
public class AIBrain : MonoBehaviour
|
|
{
|
|
[Header("AI Settings")]
|
|
[SerializeField] private UtilityScorer.AIRole role = UtilityScorer.AIRole.Balanced;
|
|
[SerializeField] private bool autoAssignRole = true;
|
|
|
|
[Header("Tick Rate (AI LOD)")]
|
|
[SerializeField] private float baseTick = 0.3f; // Tier 0: on-screen, close
|
|
[SerializeField] private float midTick = 0.5f; // Tier 1: on-screen, far
|
|
[SerializeField] private float farTick = 1.0f; // Tier 2: off-screen
|
|
[SerializeField] private float lodMidDistance = 15f;
|
|
[SerializeField] private float lodFarDistance = 30f;
|
|
|
|
[Header("Behavior")]
|
|
[SerializeField] private float minActionDuration = 1.5f; // Minimum time before switching actions
|
|
|
|
// ─── Components ──────────────────────────────────────────
|
|
private AIPerceptionSystem _perception;
|
|
private AINavigationHelper _navigation;
|
|
private CharacterAIController _aiController;
|
|
private CashCarrier _carrier;
|
|
private HealthNew _health;
|
|
private TeamMember _teamMember;
|
|
|
|
// ─── AI State ────────────────────────────────────────────
|
|
private AIBlackboard _blackboard;
|
|
private TeamBlackboard _teamBlackboard;
|
|
private UtilityScorer _scorer;
|
|
private AIAction _currentAction;
|
|
private float _lastTickTime;
|
|
private float _currentTick;
|
|
private Transform _mainCamera;
|
|
private bool _combatEnabled;
|
|
|
|
// ─── Properties ──────────────────────────────────────────
|
|
public AIBlackboard Blackboard => _blackboard;
|
|
public AIAction CurrentAction => _currentAction;
|
|
public UtilityScorer.AIRole Role => role;
|
|
public string CurrentActionName => _scorer?.CurrentActionName ?? "Initializing";
|
|
|
|
// ─── Lifecycle ───────────────────────────────────────────
|
|
|
|
private void Awake()
|
|
{
|
|
// Ensure components exist
|
|
_perception = GetComponent<AIPerceptionSystem>();
|
|
if (_perception == null) _perception = gameObject.AddComponent<AIPerceptionSystem>();
|
|
|
|
_navigation = GetComponent<AINavigationHelper>();
|
|
if (_navigation == null) _navigation = gameObject.AddComponent<AINavigationHelper>();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
|
|
{
|
|
enabled = false;
|
|
return;
|
|
}
|
|
|
|
// Cache components
|
|
_aiController = GetComponent<CharacterAIController>();
|
|
_carrier = GetComponent<CashCarrier>();
|
|
_health = GetComponent<HealthNew>();
|
|
_teamMember = GetComponent<TeamMember>();
|
|
|
|
// Find camera for LOD
|
|
if (Camera.main != null)
|
|
_mainCamera = Camera.main.transform;
|
|
|
|
// Initialize blackboard
|
|
_blackboard = new AIBlackboard();
|
|
_blackboard.teamTag = gameObject.tag;
|
|
_blackboard.team = _teamMember != null ? _teamMember.CurrentTeam : TeamMember.Team.EnemyTeam1;
|
|
|
|
// Initialize perception
|
|
_perception.Initialize(_blackboard.teamTag);
|
|
|
|
// Find vaults (one-time, cached)
|
|
FindVaults();
|
|
|
|
// Auto-assign role
|
|
if (autoAssignRole)
|
|
AssignRandomRole();
|
|
|
|
// Initialize scorer
|
|
_scorer = new UtilityScorer(role);
|
|
|
|
// Get team blackboard
|
|
_teamBlackboard = TeamBlackboard.Get(_blackboard.teamTag);
|
|
|
|
_currentTick = baseTick;
|
|
|
|
// Subscribe to events
|
|
GameEvents.OnCombatEnabled += OnCombatEnabled;
|
|
GameEvents.OnVaultUnderAttack += OnVaultAttacked;
|
|
GameEvents.OnMatchEnd += OnMatchEnd;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
GameEvents.OnCombatEnabled -= OnCombatEnabled;
|
|
GameEvents.OnVaultUnderAttack -= OnVaultAttacked;
|
|
GameEvents.OnMatchEnd -= OnMatchEnd;
|
|
|
|
// Release claims
|
|
_teamBlackboard?.ReleaseBagClaim(this);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!_combatEnabled) return;
|
|
if (_health != null && _health.isDead) return;
|
|
|
|
// AI LOD — adjust tick rate based on distance to camera
|
|
UpdateAILOD();
|
|
|
|
// Tick check
|
|
if (Time.time - _lastTickTime < _currentTick) return;
|
|
_lastTickTime = Time.time;
|
|
|
|
// 1. Update self state on blackboard
|
|
UpdateSelfState();
|
|
|
|
// 2. Run perception
|
|
_perception.UpdatePerception(_blackboard);
|
|
|
|
// 3. Update team state
|
|
UpdateTeamState();
|
|
|
|
// 4. Score and select action
|
|
AIAction bestAction = _scorer.Evaluate(this, _blackboard, _teamBlackboard);
|
|
|
|
// 5. Execute action
|
|
if (bestAction != null)
|
|
{
|
|
_currentAction = bestAction;
|
|
_currentAction.Execute(this, _blackboard, _teamBlackboard);
|
|
}
|
|
}
|
|
|
|
// ─── State Updates ───────────────────────────────────────
|
|
|
|
private void UpdateSelfState()
|
|
{
|
|
if (_health != null)
|
|
{
|
|
_blackboard.currentHealth = _health.currentHealth;
|
|
_blackboard.maxHealth = _health.maxHealth;
|
|
_blackboard.healthPercent = _health.maxHealth > 0 ? _health.currentHealth / _health.maxHealth : 1f;
|
|
_blackboard.isDead = _health.isDead;
|
|
}
|
|
|
|
if (_carrier != null)
|
|
{
|
|
_blackboard.carriedBagCount = _carrier.BagCount;
|
|
_blackboard.totalCarriedValue = _carrier.TotalValue;
|
|
_blackboard.isCarrying = _carrier.IsCarrying;
|
|
}
|
|
}
|
|
|
|
private void UpdateTeamState()
|
|
{
|
|
if (_teamBlackboard != null)
|
|
{
|
|
_blackboard.myVaultUnderAttack = _teamBlackboard.vaultUnderAttack;
|
|
_blackboard.myTeamScore = _teamBlackboard.teamScore;
|
|
}
|
|
|
|
// Get enemy team score
|
|
string enemyTag = _blackboard.teamTag == "Player" ? "Enemy" : "Player";
|
|
TeamBlackboard enemyBoard = TeamBlackboard.Get(enemyTag);
|
|
if (enemyBoard != null)
|
|
{
|
|
_blackboard.enemyTeamScore = enemyBoard.teamScore;
|
|
}
|
|
|
|
_blackboard.scoreDifference = _blackboard.myTeamScore - _blackboard.enemyTeamScore;
|
|
}
|
|
|
|
private void UpdateAILOD()
|
|
{
|
|
if (_mainCamera == null)
|
|
{
|
|
_currentTick = baseTick;
|
|
return;
|
|
}
|
|
|
|
float distToCamera = Vector3.Distance(transform.position, _mainCamera.position);
|
|
|
|
if (distToCamera > lodFarDistance)
|
|
_currentTick = farTick;
|
|
else if (distToCamera > lodMidDistance)
|
|
_currentTick = midTick;
|
|
else
|
|
_currentTick = baseTick;
|
|
}
|
|
|
|
private void FindVaults()
|
|
{
|
|
var vaults = EntityRegistry<TeamVault>.All;
|
|
for (int i = 0; i < vaults.Count; i++)
|
|
{
|
|
if (vaults[i] == null) continue;
|
|
if (vaults[i].TeamTag == _blackboard.teamTag)
|
|
_blackboard.myVault = vaults[i];
|
|
else
|
|
_blackboard.enemyVault = vaults[i];
|
|
}
|
|
|
|
// Fallback to FindObjectsOfType (only at initialization)
|
|
if (_blackboard.myVault == null || _blackboard.enemyVault == null)
|
|
{
|
|
TeamVault[] allVaults = FindObjectsOfType<TeamVault>();
|
|
foreach (var vault in allVaults)
|
|
{
|
|
if (vault.TeamTag == _blackboard.teamTag)
|
|
_blackboard.myVault = vault;
|
|
else
|
|
_blackboard.enemyVault = vault;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AssignRandomRole()
|
|
{
|
|
float rand = Random.value;
|
|
if (rand < 0.35f) role = UtilityScorer.AIRole.Balanced;
|
|
else if (rand < 0.60f) role = UtilityScorer.AIRole.Collector;
|
|
else if (rand < 0.80f) role = UtilityScorer.AIRole.Defender;
|
|
else role = UtilityScorer.AIRole.Aggressor;
|
|
}
|
|
|
|
// ─── Public API (called by AIActions) ────────────────────
|
|
|
|
/// <summary>Navigate to a world position.</summary>
|
|
public void NavigateTo(Vector3 position)
|
|
{
|
|
_navigation.SetDestination(position);
|
|
}
|
|
|
|
/// <summary>Stop all navigation.</summary>
|
|
public void StopNavigation()
|
|
{
|
|
_navigation.Stop();
|
|
}
|
|
|
|
/// <summary>Has the AI reached its current navigation destination?</summary>
|
|
public bool HasReachedDestination()
|
|
{
|
|
return _navigation.HasReachedDestination();
|
|
}
|
|
|
|
/// <summary>Engage a target in combat (delegates to CharacterAIController).</summary>
|
|
public void EngageTarget(Transform target)
|
|
{
|
|
if (_aiController != null)
|
|
{
|
|
_aiController.SetTarget(target.gameObject);
|
|
_aiController.NavigateTo(target.position);
|
|
_aiController.attack = true;
|
|
}
|
|
else
|
|
{
|
|
// Fallback: just navigate toward
|
|
NavigateTo(target.position);
|
|
}
|
|
}
|
|
|
|
/// <summary>Disengage from combat.</summary>
|
|
public void DisengageTarget()
|
|
{
|
|
if (_aiController != null)
|
|
{
|
|
_aiController.Stop();
|
|
}
|
|
}
|
|
|
|
// ─── Event Handlers ──────────────────────────────────────
|
|
|
|
private void OnCombatEnabled(bool enabled)
|
|
{
|
|
_combatEnabled = enabled;
|
|
}
|
|
|
|
private void OnVaultAttacked(TeamVault vault)
|
|
{
|
|
if (vault.TeamTag == _blackboard.teamTag)
|
|
{
|
|
_blackboard.myVaultUnderAttack = true;
|
|
if (_teamBlackboard != null)
|
|
_teamBlackboard.vaultUnderAttack = true;
|
|
|
|
// Force immediate decision (bypass tick timer)
|
|
_lastTickTime = 0;
|
|
}
|
|
}
|
|
|
|
private void OnMatchEnd(string winnerTag)
|
|
{
|
|
_combatEnabled = false;
|
|
StopNavigation();
|
|
}
|
|
|
|
// ─── Debug ───────────────────────────────────────────────
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
if (_blackboard == null) return;
|
|
|
|
// Show current action
|
|
UnityEditor.Handles.Label(transform.position + Vector3.up * 2.5f,
|
|
$"AI: {CurrentActionName}\n" +
|
|
$"Role: {role}\n" +
|
|
$"HP: {_blackboard.healthPercent:P0}\n" +
|
|
$"Bags: {_blackboard.carriedBagCount}\n" +
|
|
$"Enemies: {_blackboard.nearbyEnemyCount}\n" +
|
|
$"Tick: {_currentTick:F2}s");
|
|
|
|
// Show target
|
|
if (_blackboard.currentTarget != null)
|
|
{
|
|
Gizmos.color = Color.red;
|
|
Gizmos.DrawLine(transform.position + Vector3.up, _blackboard.currentTarget.position + Vector3.up);
|
|
}
|
|
|
|
// Show target bag
|
|
if (_blackboard.targetBag != null)
|
|
{
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawLine(transform.position + Vector3.up, _blackboard.targetBag.transform.position);
|
|
}
|
|
}
|
|
#endif
|
|
}
|