using UnityEngine; using UnityEngine.AI; /// /// 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). /// 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(); if (_perception == null) _perception = gameObject.AddComponent(); _navigation = GetComponent(); if (_navigation == null) _navigation = gameObject.AddComponent(); } private void Start() { if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected) { enabled = false; return; } // Cache components _aiController = GetComponent(); _carrier = GetComponent(); _health = GetComponent(); _teamMember = GetComponent(); // 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.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(); 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) ──────────────────── /// Navigate to a world position. public void NavigateTo(Vector3 position) { _navigation.SetDestination(position); } /// Stop all navigation. public void StopNavigation() { _navigation.Stop(); } /// Has the AI reached its current navigation destination? public bool HasReachedDestination() { return _navigation.HasReachedDestination(); } /// Engage a target in combat (delegates to CharacterAIController). 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); } } /// Disengage from combat. 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 }