chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
276
Assets/Scripts/Character/Controllers/EnemyAIController.cs
Normal file
276
Assets/Scripts/Character/Controllers/EnemyAIController.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// EnemyAIController - AI controller for ENEMY team characters.
|
||||
/// These AI characters fight AGAINST the player, targeting player team members.
|
||||
/// Used in Cash System mode for enemy team AI.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(TeamMember))]
|
||||
public class EnemyAIController : 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 aggroRange = 10f;
|
||||
|
||||
[Header("Behavior Settings")]
|
||||
[SerializeField] private float decisionInterval = 0.5f;
|
||||
[SerializeField] private bool aggressive = true;
|
||||
|
||||
[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,
|
||||
Patrol,
|
||||
ChaseEnemy,
|
||||
Attack,
|
||||
CollectCash,
|
||||
DepositCash,
|
||||
StealCash,
|
||||
Retreat
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
teamMember = GetComponent<TeamMember>();
|
||||
navAgent = GetComponent<NavMeshAgent>();
|
||||
animator = GetComponent<Animator>();
|
||||
characterAttacks = GetComponent<CharacterAttacks>();
|
||||
cashCarrier = GetComponent<CashCarrier>();
|
||||
cashAI = GetComponent<CashSystemAI>();
|
||||
|
||||
// Ensure this character is on enemy team
|
||||
if (teamMember != null)
|
||||
{
|
||||
teamMember.CurrentTeam = TeamMember.Team.EnemyTeam;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Debug.Log($"[EnemyAIController] Enabled on {gameObject.name}");
|
||||
StartCoroutine(AIDecisionLoop());
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Debug.Log($"[EnemyAIController] 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 enemy AI decision making
|
||||
TeamMember nearestEnemy = teamMember.FindNearestEnemy(); // Player team
|
||||
|
||||
// Priority 1: Attack nearby player team members
|
||||
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 <= aggroRange && aggressive)
|
||||
{
|
||||
SetState(AIState.ChaseEnemy);
|
||||
currentTarget = nearestEnemy;
|
||||
MoveToTarget(nearestEnemy.transform.position);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Patrol or idle
|
||||
if (aggressive && nearestEnemy != null)
|
||||
{
|
||||
float distanceToEnemy = Vector3.Distance(transform.position, nearestEnemy.transform.position);
|
||||
if (distanceToEnemy <= detectionRange)
|
||||
{
|
||||
SetState(AIState.ChaseEnemy);
|
||||
currentTarget = nearestEnemy;
|
||||
MoveToTarget(nearestEnemy.transform.position);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: Idle
|
||||
SetState(AIState.Idle);
|
||||
StopMovement();
|
||||
}
|
||||
|
||||
private void SetState(AIState newState)
|
||||
{
|
||||
if (currentState != newState)
|
||||
{
|
||||
currentState = newState;
|
||||
Debug.Log($"[EnemyAIController] {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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30038cb6da0d4a575a78611ca2d78e3a
|
||||
271
Assets/Scripts/Character/Controllers/TeamAIController.cs
Normal file
271
Assets/Scripts/Character/Controllers/TeamAIController.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c790a89885188fadd8e16afaef7274f4
|
||||
Reference in New Issue
Block a user