272 lines
8.2 KiB
C#
272 lines
8.2 KiB
C#
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using System.Collections;
|
|
|
|
/// <summary>
|
|
/// TeamAIController - AI controller for FRIENDLY teammates on the player's team.
|
|
/// These AI characters fight FOR the player, targeting enemy team members.
|
|
/// Used in Cash System mode for AI teammates.
|
|
/// </summary>
|
|
[RequireComponent(typeof(TeamMember))]
|
|
public class TeamAIController : MonoBehaviour
|
|
{
|
|
[Header("AI State")]
|
|
[SerializeField] private AIState currentState = AIState.Idle;
|
|
|
|
[Header("Detection Settings")]
|
|
[SerializeField] private float detectionRange = 15f;
|
|
[SerializeField] private float attackRange = 2f;
|
|
[SerializeField] private float followDistance = 5f;
|
|
|
|
[Header("Behavior Settings")]
|
|
[SerializeField] private float decisionInterval = 0.5f;
|
|
[SerializeField] private bool followPlayer = true;
|
|
[SerializeField] private float maxDistanceFromPlayer = 20f;
|
|
|
|
[Header("References")]
|
|
private TeamMember teamMember;
|
|
private NavMeshAgent navAgent;
|
|
private Animator animator;
|
|
private CharacterAttacks characterAttacks;
|
|
|
|
// Combat
|
|
private TeamMember currentTarget;
|
|
private float lastAttackTime;
|
|
private float attackCooldown = 1.5f;
|
|
|
|
// Cash System integration
|
|
private CashCarrier cashCarrier;
|
|
private CashSystemAI cashAI;
|
|
|
|
public enum AIState
|
|
{
|
|
Idle,
|
|
FollowPlayer,
|
|
ChaseEnemy,
|
|
Attack,
|
|
CollectCash,
|
|
DepositCash,
|
|
Retreat
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
teamMember = GetComponent<TeamMember>();
|
|
navAgent = GetComponent<NavMeshAgent>();
|
|
animator = GetComponent<Animator>();
|
|
characterAttacks = GetComponent<CharacterAttacks>();
|
|
cashCarrier = GetComponent<CashCarrier>();
|
|
cashAI = GetComponent<CashSystemAI>();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
Debug.Log($"[TeamAIController] Enabled on {gameObject.name}");
|
|
StartCoroutine(AIDecisionLoop());
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
Debug.Log($"[TeamAIController] Disabled on {gameObject.name}");
|
|
StopAllCoroutines();
|
|
}
|
|
|
|
private IEnumerator AIDecisionLoop()
|
|
{
|
|
yield return new WaitForSeconds(0.5f); // Initial delay
|
|
|
|
while (enabled)
|
|
{
|
|
MakeDecision();
|
|
yield return new WaitForSeconds(decisionInterval);
|
|
}
|
|
}
|
|
|
|
private void MakeDecision()
|
|
{
|
|
// Lazy re-cache: CashSystemAI is added AFTER our Awake() by ConfigureCharacterControllers,
|
|
// so the Awake-time GetComponent returns null. Re-check here once.
|
|
if (cashAI == null) cashAI = GetComponent<CashSystemAI>();
|
|
|
|
// If CashSystemAI is attached and active, delegate ALL decisions to it
|
|
if (cashAI != null && cashAI.enabled)
|
|
{
|
|
// CashSystemAI handles the high-level decision making via its own Update()
|
|
return;
|
|
}
|
|
|
|
// Standard team AI decision making
|
|
TeamMember nearestEnemy = teamMember.FindNearestEnemy();
|
|
TeamMember playerControlled = TeamMember.GetPlayerControlledMember();
|
|
|
|
// Priority 1: Attack nearby enemies
|
|
if (nearestEnemy != null)
|
|
{
|
|
float distanceToEnemy = Vector3.Distance(transform.position, nearestEnemy.transform.position);
|
|
|
|
if (distanceToEnemy <= attackRange)
|
|
{
|
|
SetState(AIState.Attack);
|
|
currentTarget = nearestEnemy;
|
|
PerformAttack();
|
|
return;
|
|
}
|
|
else if (distanceToEnemy <= detectionRange)
|
|
{
|
|
SetState(AIState.ChaseEnemy);
|
|
currentTarget = nearestEnemy;
|
|
MoveToTarget(nearestEnemy.transform.position);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Priority 2: Follow the player-controlled character
|
|
if (followPlayer && playerControlled != null && playerControlled != teamMember)
|
|
{
|
|
float distanceToPlayer = Vector3.Distance(transform.position, playerControlled.transform.position);
|
|
|
|
if (distanceToPlayer > followDistance)
|
|
{
|
|
SetState(AIState.FollowPlayer);
|
|
MoveToTarget(playerControlled.transform.position);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Default: Idle
|
|
SetState(AIState.Idle);
|
|
StopMovement();
|
|
}
|
|
|
|
private void SetState(AIState newState)
|
|
{
|
|
if (currentState != newState)
|
|
{
|
|
currentState = newState;
|
|
Debug.Log($"[TeamAIController] {gameObject.name} -> {newState}");
|
|
}
|
|
}
|
|
|
|
private void MoveToTarget(Vector3 position)
|
|
{
|
|
if (navAgent != null && navAgent.isActiveAndEnabled && navAgent.isOnNavMesh)
|
|
{
|
|
navAgent.isStopped = false;
|
|
navAgent.SetDestination(position);
|
|
|
|
// Update animator
|
|
if (animator != null)
|
|
{
|
|
float speed = navAgent.velocity.magnitude / navAgent.speed;
|
|
animator.SetFloat("Speed", speed);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StopMovement()
|
|
{
|
|
if (navAgent != null && navAgent.isActiveAndEnabled && navAgent.isOnNavMesh)
|
|
{
|
|
navAgent.isStopped = true;
|
|
navAgent.velocity = Vector3.zero;
|
|
}
|
|
|
|
if (animator != null)
|
|
{
|
|
animator.SetFloat("Speed", 0f);
|
|
}
|
|
}
|
|
|
|
private void PerformAttack()
|
|
{
|
|
if (Time.time - lastAttackTime < attackCooldown) return;
|
|
|
|
// Face the target
|
|
if (currentTarget != null)
|
|
{
|
|
Vector3 direction = (currentTarget.transform.position - transform.position).normalized;
|
|
direction.y = 0;
|
|
if (direction.sqrMagnitude > 0.001f)
|
|
{
|
|
transform.rotation = Quaternion.LookRotation(direction);
|
|
}
|
|
}
|
|
|
|
// Perform attack using CharacterAttacks if available
|
|
if (characterAttacks != null)
|
|
{
|
|
// Choose a random attack
|
|
string[] attacks = { AnimationNames.Jab, AnimationNames.LowKick, AnimationNames.Haymaker };
|
|
string randomAttack = attacks[Random.Range(0, attacks.Length)];
|
|
|
|
characterAttacks.PunchAttacksDirectly(randomAttack);
|
|
lastAttackTime = Time.time;
|
|
}
|
|
else if (animator != null)
|
|
{
|
|
// Fallback to direct animation trigger
|
|
animator.SetTrigger("attack");
|
|
lastAttackTime = Time.time;
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
// When CashSystemAI is driving, don't interfere with NavMeshAgent
|
|
if (cashAI != null && cashAI.enabled) return;
|
|
|
|
// Continuous state updates
|
|
switch (currentState)
|
|
{
|
|
case AIState.Attack:
|
|
if (currentTarget != null)
|
|
{
|
|
float distance = Vector3.Distance(transform.position, currentTarget.transform.position);
|
|
if (distance > attackRange * 1.5f)
|
|
{
|
|
// Move closer
|
|
MoveToTarget(currentTarget.transform.position);
|
|
}
|
|
else
|
|
{
|
|
StopMovement();
|
|
PerformAttack();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case AIState.ChaseEnemy:
|
|
if (currentTarget != null)
|
|
{
|
|
MoveToTarget(currentTarget.transform.position);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set a specific target for this AI to attack
|
|
/// </summary>
|
|
public void SetTarget(TeamMember target)
|
|
{
|
|
currentTarget = target;
|
|
if (target != null)
|
|
{
|
|
SetState(AIState.ChaseEnemy);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Command this AI to move to a specific position
|
|
/// </summary>
|
|
public void MoveTo(Vector3 position)
|
|
{
|
|
MoveToTarget(position);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get current AI state
|
|
/// </summary>
|
|
public AIState GetCurrentState() => currentState;
|
|
}
|