using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// AI controller for enemy dinosaurs.
/// Handles patrol, chase, attack, and flee behaviors.
/// </summary>
public class AIController : MonoBehaviour
{
[Header("AI Settings")]
[Tooltip("Detection range for spotting player")]
public float detectionRange = 15f;
[Tooltip("Aggro range - will chase if player enters")]
public float aggroRange = 10f;
[Tooltip("Distance to start attacking")]
public float attackRange = 3.5f;
[Tooltip("Distance to flee when low health")]
public float fleeRange = 5f;
[Tooltip("Health percentage to start fleeing")]
[Range(0f, 1f)]
public float fleeHealthPercent = 0.3f;
[Tooltip("Time to lose interest in player (seconds)")]
public float loseInterestTime = 5f;
[Header("Movement")]
[Tooltip("Walking speed")]
public float walkSpeed = 3f;
[Tooltip("Running speed when chasing")]
public float chaseSpeed = 6f;
[Tooltip("Turning speed")]
public float rotationSpeed = 5f;
[Header("Patrol")]
[Tooltip("Patrol waypoints to follow")]
public Transform[] patrolPoints;
[Tooltip("Time to wait at each waypoint")]
public float patrolWaitTime = 3f;
[Tooltip("Random offset for patrol points")]
public float patrolRandomOffset = 2f;
[Header("State")]
[Tooltip("Current AI state")]
public AIState currentState = AIState.Idle;
[Tooltip("Target to pursue (usually player)")]
public Transform target;
[Tooltip("Current patrol point index")]
public int currentPatrolIndex = 0;
[Tooltip("Timer for various states")]
public float stateTimer = 0f;
// Components
private Animator animator;
private CombatSystem combat;
private UnityEngine.AI.NavMeshAgent navAgent;
private float lastSeenPlayer;
// Player reference
private Transform player;
void Start()
{
// Get components
animator = GetComponent<Animator>();
combat = GetComponent<CombatSystem>();
navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
if (navAgent == null)
{
Debug.LogWarning("No NavMeshAgent on " + gameObject.name + ". Adding one.");
navAgent = gameObject.AddComponent<UnityEngine.AI.NavMeshAgent>();
navAgent.speed = walkSpeed;
navAgent.acceleration = 10f;
navAgent.angularSpeed = 180f;
navAgent.stoppingDistance = attackRange * 0.8f;
}
// Find player
player = FindPlayer();
Debug.Log($"🦕 AI initialized on {gameObject.name} - State: {currentState}");
}
void Update()
{
// Update state timer
stateTimer += Time.deltaTime;
// State machine
switch (currentState)
{
case AIState.Idle:
UpdateIdle();
break;
case AIState.Patrol:
UpdatePatrol();
break;
case AIState.Chase:
UpdateChase();
break;
case AIState.Attack:
UpdateAttack();
break;
case AIState.Flee:
UpdateFlee();
break;
case AIState.Dead:
break;
}
// Check for player detection
CheckPlayerDetection();
// Check if should flee
CheckFleeCondition();
// Update animations
UpdateAnimations();
}
void UpdateIdle()
{
// After idle, start patrolling
if (stateTimer > 2f)
{
currentState = AIState.Patrol;
stateTimer = 0f;
navAgent.destination = GetNextPatrolPoint();
}
// Play idle animation
if (animator != null)
{
animator.SetFloat("Speed", 0f);
}
}
void UpdatePatrol()
{
// Check if reached patrol point
if (navAgent.remainingDistance <= navAgent.stoppingDistance)
{
stateTimer += Time.deltaTime;
if (stateTimer >= patrolWaitTime)
{
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
navAgent.destination = GetNextPatrolPoint();
stateTimer = 0f;
}
}
if (animator != null)
{
animator.SetFloat("Speed", 1f);
}
}
void UpdateChase()
{
if (target == null)
{
currentState = AIState.Patrol;
return;
}
// Set destination to target
navAgent.destination = target.position;
navAgent.speed = chaseSpeed;
// Face target
Quaternion lookRotation = Quaternion.LookRotation(target.position - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, rotationSpeed * Time.deltaTime);
// Check if in attack range
float distanceToTarget = Vector3.Distance(transform.position, target.position);
if (distanceToTarget <= attackRange)
{
currentState = AIState.Attack;
stateTimer = 0f;
navAgent.speed = walkSpeed;
}
// Lose interest if player is out of range for too long
if (distanceToTarget > aggroRange * 1.5f)
{
if (stateTimer >= loseInterestTime)
{
currentState = AIState.Patrol;
navAgent.speed = walkSpeed;
}
}
else
{
stateTimer = 0f;
}
if (animator != null)
{
animator.SetFloat("Speed", 2f);
}
}
void UpdateAttack()
{
if (target == null)
{
currentState = AIState.Patrol;
return;
}
// Face target
Quaternion lookRotation = Quaternion.LookRotation(target.position - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, rotationSpeed * Time.deltaTime);
// Attack using combat system
if (combat != null && stateTimer >= combat.biteCooldown)
{
// Trigger bite attack manually for AI
combat.PerformBite();
stateTimer = 0f;
}
// Check if should stop attacking
float distanceToTarget = Vector3.Distance(transform.position, target.position);
if (distanceToTarget > attackRange * 1.5f)
{
currentState = AIState.Chase;
stateTimer = 0f;
}
}
void UpdateFlee()
{
if (target == null)
{
currentState = AIState.Patrol;
return;
}
// Run away from target
Vector3 fleeDirection = (transform.position - target.position).normalized;
Vector3 fleeDestination = transform.position + fleeDirection * fleeRange;
navAgent.destination = fleeDestination;
navAgent.speed = chaseSpeed;
// Stop fleeing if health is recovered or player is far away
float distanceToTarget = Vector3.Distance(transform.position, target.position);
if (combat.HealthPercent > fleeHealthPercent + 0.2f || distanceToTarget > fleeRange * 2f)
{
currentState = AIState.Patrol;
navAgent.speed = walkSpeed;
}
}
void CheckPlayerDetection()
{
if (player == null) return;
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// Detect player in range
if (distanceToPlayer <= detectionRange)
{
// Raycast to check line of sight
Vector3 directionToPlayer = (player.position - transform.position).normalized;
if (Physics.Raycast(transform.position + Vector3.up * 2f, directionToPlayer, out RaycastHit hit, detectionRange))
{
if (hit.transform == player)
{
if (distanceToPlayer <= aggroRange)
{
target = player;
currentState = AIState.Chase;
stateTimer = 0f;
lastSeenPlayer = Time.time;
}
}
}
}
}
void CheckFleeCondition()
{
if (combat != null && combat.HealthPercent <= fleeHealthPercent && currentState != AIState.Dead)
{
if (target != null)
{
currentState = AIState.Flee;
stateTimer = 0f;
}
}
}
void UpdateAnimations()
{
if (animator == null) return;
float speed = 0f;
switch (currentState)
{
case AIState.Idle:
speed = 0f;
break;
case AIState.Patrol:
speed = 1f;
break;
case AIState.Chase:
speed = 2f;
break;
case AIState.Attack:
speed = 0.5f;
break;
case AIState.Flee:
speed = 2.5f;
break;
}
animator.SetFloat("Speed", speed);
animator.SetFloat("Health", combat.HealthPercent);
}
Vector3 GetNextPatrolPoint()
{
if (patrolPoints.Length == 0)
{
// Random direction using Unity 6 compatible method
return transform.position + Quaternion.Euler(0f, Random.Range(0f, 360f), 0f) * Vector3.forward * 10f;
}
Vector3 point = patrolPoints[currentPatrolIndex].position;
// Add random offset
point += new Vector3(
Random.Range(-patrolRandomOffset, patrolRandomOffset),
0f,
Random.Range(-patrolRandomOffset, patrolRandomOffset)
);
return point;
}
Transform FindPlayer()
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
return playerObj?.transform;
}
/// <summary>
/// Called when AI dies.
/// </summary>
public void Die()
{
currentState = AIState.Dead;
if (navAgent != null)
{
navAgent.enabled = false;
}
}
void OnDrawGizmosSelected()
{
// Draw detection range
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, detectionRange);
// Draw aggro range
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, aggroRange);
// Draw patrol points
if (patrolPoints != null)
{
Gizmos.color = Color.blue;
foreach (Transform point in patrolPoints)
{
if (point != null)
{
Gizmos.DrawSphere(point.position, 0.5f);
}
}
}
}
}
public enum AIState
{
Idle,
Patrol,
Chase,
Attack,
Flee,
Dead
}