Files
DeviantMobile-Rohan/Assets/Scripts/CashSystem/CashSystemAI.cs.bak

741 lines
23 KiB
C#

using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;
/// <summary>
/// CashSystemAI - AI decision layer for Cash System mode.
/// Works alongside CharacterAIController for combat.
/// </summary>
public class CashSystemAI : MonoBehaviour
{
public enum AIState
{
Idle,
CollectCash,
DepositCash,
AttackEnemy,
StealFromVault,
DefendVault,
Retreat,
Patrol // NEW: wander when nothing to do
}
public enum AIRole
{
Balanced,
Collector,
Defender,
Aggressor
}
[Header("AI Settings")]
[SerializeField] private AIState currentState = AIState.Idle;
[SerializeField] private AIRole role = AIRole.Balanced;
[Header("Decision Weights")]
[SerializeField] private float collectWeight = 1f;
[SerializeField] private float depositWeight = 1.5f;
[SerializeField] private float attackWeight = 1f;
[SerializeField] private float stealWeight = 0.8f;
[SerializeField] private float defendWeight = 1.2f;
[Header("Thresholds")]
[SerializeField] private float depositCashThreshold = 100f;
[SerializeField] private float retreatHealthThreshold = 30f;
[SerializeField] private float nearbyEnemyDistance = 15f;
[SerializeField] private float nearbyBoxDistance = 50f;
[Header("Patrol Settings")]
[SerializeField] private float patrolRadius = 12f;
[SerializeField] private float patrolPauseMin = 1f;
[SerializeField] private float patrolPauseMax = 3f;
[Header("Timing")]
[SerializeField] private float decisionInterval = 0.3f;
[SerializeField] private float stateMinDuration = 0.8f;
[Header("Difficulty")]
[SerializeField] private int aiLevel = 1; // 1 = normal, higher = harder/faster
// Components
private CashCarrier cashCarrier;
private HealthNew health;
private CharacterAIController aiController;
private PlayerScript playerScript;
// Runtime state
private float lastDecisionTime;
private float stateStartTime;
private Transform currentTarget;
private TeamVault myVault;
private TeamVault enemyVault;
private string myTeamTag;
// Patrol state
private Vector3 patrolDestination;
private bool hasPatrolDestination;
private float patrolPauseEndTime;
private NavMeshAgent navAgent;
public AIState CurrentState => currentState;
public AIRole CurrentRole => role;
private void Start()
{
// Only activate in Cash System mode
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
{
enabled = false;
return;
}
// Get components
cashCarrier = GetComponent<CashCarrier>();
health = GetComponent<HealthNew>();
aiController = GetComponent<CharacterAIController>();
playerScript = GetComponent<PlayerScript>();
navAgent = GetComponent<NavMeshAgent>();
myTeamTag = gameObject.tag;
// Assign random role
AssignRole();
// Apply simple difficulty scaling
if (aiLevel > 1)
{
float scale = 1f + (aiLevel - 1) * 0.15f;
collectWeight *= scale;
depositWeight *= scale;
attackWeight *= scale;
stealWeight *= scale;
defendWeight *= scale;
decisionInterval = Mathf.Max(0.2f, decisionInterval / (1f + (aiLevel - 1) * 0.2f));
if (navAgent != null)
{
navAgent.speed *= 1f + (aiLevel - 1) * 0.1f;
}
}
// Find vaults
FindVaults();
Debug.Log($"[CashSystemAI] Initialized on {gameObject.name}, Team: {myTeamTag}, Role: {role}");
}
private void AssignRole()
{
// Random role assignment with some bias based on character
float rand = Random.value;
if (rand < 0.4f) role = AIRole.Balanced;
else if (rand < 0.65f) role = AIRole.Collector;
else if (rand < 0.85f) role = AIRole.Defender;
else role = AIRole.Aggressor;
// Adjust weights based on role
switch (role)
{
case AIRole.Collector:
collectWeight *= 1.5f;
depositWeight *= 1.3f;
attackWeight *= 0.7f;
break;
case AIRole.Defender:
defendWeight *= 2f;
attackWeight *= 1.2f;
collectWeight *= 0.6f;
break;
case AIRole.Aggressor:
attackWeight *= 2f;
stealWeight *= 1.5f;
collectWeight *= 0.5f;
break;
}
}
private void FindVaults()
{
// Use EntityRegistry (zero-alloc) instead of FindObjectsOfType
var vaults = EntityRegistry<TeamVault>.GetAll();
for (int i = 0; i < vaults.Count; i++)
{
if (vaults[i] == null) continue;
if (vaults[i].TeamTag == myTeamTag)
myVault = vaults[i];
else
enemyVault = vaults[i];
}
// Fallback if EntityRegistry empty (vaults not yet registered)
if (myVault == null || enemyVault == null)
{
TeamVault[] fallback = FindObjectsOfType<TeamVault>();
foreach (TeamVault vault in fallback)
{
if (vault.TeamTag == myTeamTag) myVault = vault;
else enemyVault = vault;
}
}
}
private void Update()
{
if (Time.time - lastDecisionTime < decisionInterval)
return;
lastDecisionTime = Time.time;
// Lazy re-find vaults if they weren't found at Start
if (myVault == null || enemyVault == null)
FindVaults();
// Check health for retreat (only when combat enabled)
if (IsCombatEnabled() && health != null && health.currentHealth < retreatHealthThreshold)
{
SetState(AIState.Retreat);
ExecuteState();
return;
}
// Don't switch state too quickly
if (Time.time - stateStartTime < stateMinDuration && currentState != AIState.Idle)
{
// But always re-execute current state so movement continues
ExecuteState();
return;
}
MakeDecision();
ExecuteState();
}
/// <summary>
/// Check if this AI is allowed to take combat actions (after countdown)
/// </summary>
private bool IsCombatEnabled()
{
// Check CharacterAIController's attack flag
if (aiController != null)
{
return aiController.attack;
}
// Check PlayerScript's attack flag (for AI teammates)
if (playerScript != null)
{
return playerScript.attack;
}
// Default to true if no scripts found
return true;
}
private void MakeDecision()
{
float bestScore = 0f;
AIState bestState = AIState.Idle;
bool isCarrying = cashCarrier != null && cashCarrier.CarriedCash >= 0.01f;
// Evaluate: Deposit (if carrying ANY cash and vault exists)
if (isCarrying && myVault != null)
{
// Higher urgency the more cash we carry
float score = depositWeight * Mathf.Max(1f, cashCarrier.CarriedCash / depositCashThreshold);
// Extra urgency if carrying max bags
if (cashCarrier.BagCount >= 3) score *= 2f;
if (score > bestScore)
{
bestScore = score;
bestState = AIState.DepositCash;
}
}
// Evaluate: Defend (if vault under attack)
if (myVault != null && myVault.IsBeingStolen)
{
float score = defendWeight * 3f; // High priority
if (score > bestScore)
{
bestScore = score;
bestState = AIState.DefendVault;
}
}
// Evaluate: Attack (if enemy nearby AND combat enabled)
if (IsCombatEnabled())
{
GameObject nearestEnemy = FindNearestEnemy();
if (nearestEnemy != null)
{
float dist = Vector3.Distance(transform.position, nearestEnemy.transform.position);
if (dist < nearbyEnemyDistance)
{
float score = attackWeight * (1f - dist / nearbyEnemyDistance);
// Higher score if enemy is carrying cash
CashCarrier enemyCarrier = nearestEnemy.GetComponent<CashCarrier>();
if (enemyCarrier != null && enemyCarrier.CarriedCash > 0)
{
score *= 1.5f;
}
if (score > bestScore)
{
bestScore = score;
bestState = AIState.AttackEnemy;
currentTarget = nearestEnemy.transform;
}
}
}
}
// Evaluate: Collect (if not carrying max bags and bag available)
if (cashCarrier != null && cashCarrier.CanPickUp())
{
CashBag nearestBag = FindNearestCashBag();
if (nearestBag != null)
{
float dist = Vector3.Distance(transform.position, nearestBag.transform.position);
// Always consider collecting if we can carry more
float score = collectWeight * (1f + nearestBag.CashValue / 100f);
// Slight distance penalty (prefer closer bags) but don't disqualify far bags
score *= Mathf.Clamp01(1f - dist / (nearbyBoxDistance * 2f)) + 0.5f;
if (score > bestScore)
{
bestScore = score;
bestState = AIState.CollectCash;
currentTarget = nearestBag.transform;
}
}
}
// Evaluate: Steal (if enemy vault has cash and we're close and combat enabled)
if (IsCombatEnabled() && enemyVault != null && enemyVault.StoredCash > 0 && !isCarrying)
{
float dist = Vector3.Distance(transform.position, enemyVault.transform.position);
if (dist < nearbyBoxDistance)
{
float score = stealWeight * (enemyVault.StoredCash / 500f);
if (score > bestScore)
{
bestScore = score;
bestState = AIState.StealFromVault;
currentTarget = enemyVault.transform;
}
}
}
// Default: Patrol if nothing else to do (NEVER stay idle)
if (bestState == AIState.Idle)
{
CashBag fallbackBag = FindNearestCashBag();
if (fallbackBag != null)
{
bestState = AIState.CollectCash;
currentTarget = fallbackBag.transform;
}
else
{
bestState = AIState.Patrol;
}
}
SetState(bestState);
}
private void SetState(AIState newState)
{
if (newState != currentState)
{
currentState = newState;
stateStartTime = Time.time;
Debug.Log($"[CashSystemAI] {gameObject.name} -> {newState}");
}
}
private void ExecuteState()
{
switch (currentState)
{
case AIState.Idle:
ExecuteIdle();
break;
case AIState.CollectCash:
ExecuteCollect();
break;
case AIState.DepositCash:
ExecuteDeposit();
break;
case AIState.AttackEnemy:
ExecuteAttack();
break;
case AIState.StealFromVault:
ExecuteSteal();
break;
case AIState.DefendVault:
ExecuteDefend();
break;
case AIState.Retreat:
ExecuteRetreat();
break;
case AIState.Patrol:
ExecutePatrol();
break;
}
}
private void ExecuteIdle()
{
// Try to find something to do
CashBag nearestBag = FindNearestCashBag();
if (nearestBag != null)
{
currentTarget = nearestBag.transform;
SetState(AIState.CollectCash);
}
else
{
// Nothing to do — patrol instead of standing still
SetState(AIState.Patrol);
}
}
/// <summary>
/// GTA-style patrol: pick random NavMesh point, walk there, pause briefly, repeat.
/// This ensures AI NEVER stands completely still.
/// </summary>
private void ExecutePatrol()
{
// If pausing at a patrol point, wait
if (Time.time < patrolPauseEndTime)
return;
// Check if we should switch to a real task
CashBag nearestBag = FindNearestCashBag();
if (nearestBag != null)
{
currentTarget = nearestBag.transform;
SetState(AIState.CollectCash);
hasPatrolDestination = false;
return;
}
// Check for nearby enemies while patrolling
GameObject nearestEnemy = FindNearestEnemy();
if (nearestEnemy != null)
{
float dist = Vector3.Distance(transform.position, nearestEnemy.transform.position);
if (dist < nearbyEnemyDistance * 0.5f) // React at half range while patrolling
{
currentTarget = nearestEnemy.transform;
SetState(AIState.AttackEnemy);
hasPatrolDestination = false;
return;
}
}
// Pick a new patrol destination if needed
if (!hasPatrolDestination || HasReachedDestination())
{
if (hasPatrolDestination)
{
// Just arrived — pause briefly before picking a new point
patrolPauseEndTime = Time.time + Random.Range(patrolPauseMin, patrolPauseMax);
hasPatrolDestination = false;
return;
}
// Pick random NavMesh point
Vector3 randomPoint = transform.position + Random.insideUnitSphere * patrolRadius;
randomPoint.y = transform.position.y;
NavMeshHit hit;
if (NavMesh.SamplePosition(randomPoint, out hit, patrolRadius, NavMesh.AllAreas))
{
patrolDestination = hit.position;
hasPatrolDestination = true;
MoveToward(patrolDestination);
}
}
else
{
// Continue moving toward current patrol point
MoveToward(patrolDestination);
}
}
private bool HasReachedDestination()
{
if (!hasPatrolDestination) return true;
float dist = Vector3.Distance(transform.position, patrolDestination);
return dist < 2f;
}
private void ExecuteCollect()
{
if (currentTarget == null)
{
// Target gone — find another bag
CashBag newBag = FindNearestCashBag();
if (newBag != null)
{
currentTarget = newBag.transform;
}
else
{
SetState(AIState.Patrol);
return;
}
}
// Validate target is still an available bag
var bag = currentTarget.GetComponent<CashBag>();
if (bag == null || !bag.IsAvailable)
{
// Bag was taken or despawned — find another
CashBag newBag = FindNearestCashBag();
if (newBag != null)
{
currentTarget = newBag.transform;
bag = newBag;
}
else
{
currentTarget = null;
SetState(AIState.Patrol);
return;
}
}
float dist = Vector3.Distance(transform.position, currentTarget.position);
// If we're close enough to the bag, attempt direct pickup
if (dist <= 2.5f)
{
if (cashCarrier != null && cashCarrier.CanPickUp())
{
bag.PickUp(cashCarrier);
cashCarrier.OnBagPickedUp(bag);
Debug.Log($"[CashSystemAI] {gameObject.name} picked up bag (${bag.CashValue}), now carrying ${cashCarrier.CarriedCash:F0}");
// After pickup, immediately decide next action
if (cashCarrier.ShouldDeposit() && myVault != null)
SetState(AIState.DepositCash);
else
{
// Try to find another bag
CashBag nextBag = FindNearestCashBag();
if (nextBag != null && cashCarrier.CanPickUp())
{
currentTarget = nextBag.transform;
SetState(AIState.CollectCash);
}
else
SetState(AIState.DepositCash);
}
return;
}
}
MoveToward(currentTarget.position);
}
private void ExecuteDeposit()
{
if (myVault == null)
{
FindVaults();
if (myVault == null)
{
SetState(AIState.Patrol);
return;
}
}
// Check if we still have cash to deposit
if (cashCarrier == null || cashCarrier.CarriedCash < 0.01f)
{
Debug.Log($"[CashSystemAI] {gameObject.name} has no cash to deposit, switching to collect");
SetState(AIState.Idle);
return;
}
float dist = Vector3.Distance(transform.position, myVault.transform.position);
// If close enough to vault, deposit directly
if (dist <= 3f)
{
float deposited = cashCarrier.DepositToVault(myVault);
Debug.Log($"[CashSystemAI] {gameObject.name} deposited ${deposited:F0} to vault!");
// Go find more bags after depositing
SetState(AIState.Idle);
return;
}
// Move toward vault
MoveToward(myVault.transform.position);
}
private void ExecuteAttack()
{
if (currentTarget == null)
{
SetState(AIState.Idle);
return;
}
// Let CharacterAIController handle combat
if (aiController != null)
{
aiController.SetTarget(currentTarget.gameObject);
aiController.NavigateTo(currentTarget.position);
}
if (aiController != null)
{
aiController.attack = true;
}
}
private void ExecuteSteal()
{
if (enemyVault == null)
{
SetState(AIState.Idle);
return;
}
MoveToward(enemyVault.transform.position);
}
private void ExecuteDefend()
{
if (myVault == null)
{
SetState(AIState.Idle);
return;
}
// Find the thief attacking our vault
GameObject nearestEnemy = FindNearestEnemy();
if (nearestEnemy != null)
{
currentTarget = nearestEnemy.transform;
if (aiController != null)
{
aiController.SetTarget(nearestEnemy);
aiController.NavigateTo(nearestEnemy.transform.position);
}
if (aiController != null)
{
aiController.attack = true;
}
}
else
{
MoveToward(myVault.transform.position);
}
}
private void ExecuteRetreat()
{
if (myVault == null)
{
SetState(AIState.Idle);
return;
}
// Run back to vault
MoveToward(myVault.transform.position);
// Deposit if we have cash
if (cashCarrier != null && cashCarrier.CarriedCash > 0)
{
float dist = Vector3.Distance(transform.position, myVault.transform.position);
if (dist < 3f)
{
cashCarrier.DepositToVault(myVault);
}
}
// Recover health (if we have regen)
if (health != null && health.currentHealth > retreatHealthThreshold * 1.5f)
{
SetState(AIState.Idle);
}
}
private void MoveToward(Vector3 position)
{
if (aiController != null)
{
aiController.NavigateTo(position);
}
else
{
// Direct movement if no AI controller
Vector3 dir = (position - transform.position).normalized;
transform.position += dir * 3f * Time.deltaTime;
transform.LookAt(position);
}
}
private GameObject FindNearestEnemy()
{
// Use TeamMember static registry (zero-allocation) instead of FindGameObjectsWithTag
GameObject nearest = null;
float nearestDist = float.MaxValue;
foreach (TeamMember member in TeamMember.AllMembers)
{
if (member == null || member.gameObject == gameObject) continue;
if (member.gameObject.CompareTag(myTeamTag)) continue; // Same team, skip
// Check if alive
HealthNew enemyHealth = member.GetComponent<HealthNew>();
if (enemyHealth != null && enemyHealth.currentHealth <= 0) continue;
float dist = Vector3.Distance(transform.position, member.transform.position);
if (dist < nearestDist)
{
nearestDist = dist;
nearest = member.gameObject;
}
}
return nearest;
}
private CashBag FindNearestCashBag()
{
// Use EntityRegistry (zero-alloc) instead of FindObjectsOfType
var bags = EntityRegistry<CashBag>.GetAll();
CashBag nearest = null;
float nearestDist = float.MaxValue;
for (int i = 0; i < bags.Count; i++)
{
CashBag bag = bags[i];
if (bag == null) continue;
// Accept Available or Dropped bags (not Carried)
if (!bag.IsAvailable && !bag.IsDropped) continue;
float dist = Vector3.Distance(transform.position, bag.transform.position);
if (dist < nearestDist)
{
nearestDist = dist;
nearest = bag;
}
}
return nearest;
}
}