using UnityEngine; using UnityEngine.AI; using ArcadeVP; public class AIController : MonoBehaviour { public enum AIState { Patrol, Pursue, Searching, Unstuck, Destroyed } [Header("Target")] public ArcadeVehicleController player; private CarHealth playerHealth; private ArcadeVehicleController aiVehicle; [Header("Current State (Debug)")] public AIState currentState = AIState.Patrol; [Header("Vision & Senses")] [Tooltip("Maximum distance the AI can spot the player.")] public float visionRange = 120f; [Tooltip("Field of view angle. E.g. 120 means 60 degrees left and right.")] [Range(0, 360)] public float visionAngle = 120f; [Tooltip("Layers that block Line of Sight (buildings, walls).")] public LayerMask obstacleLayers; [Header("Search System")] [Tooltip("How long the player must be hidden before AI switches to Search mode.")] public float timeToLoseTarget = 1.5f; [Tooltip("How long to search the last known area before giving up and patrolling.")] public float searchDuration = 10.0f; private float sightLostTimer = 0f; private float searchTimer = 0f; private Vector3 lastKnownPosition; [Header("Navigation (NavMesh)")] [Tooltip("How often the AI recalculates its path around buildings.")] public float pathRecalculationRate = 0.5f; private float pathTimer = 0f; private NavMeshPath currentPath; [Header("Aggression Settings (Pursuit)")] public float forcedTurnSpeed = 8f; public float minRamSpeed = 10f; [Header("Unstuck System")] private float stuckTimer = 0f; private float unstuckReverseTimer = 0f; [Header("Patrol Settings")] private Vector3 currentPatrolPoint; private bool hasPatrolPoint = false; void Start() { aiVehicle = GetComponent(); currentPath = new NavMeshPath(); if (player == null) { ArcadeVehicleController[] cars = FindObjectsByType(FindObjectsSortMode.None); foreach (ArcadeVehicleController car in cars) { if (car.CompareTag("Player")) { player = car; break; } } } if (player != null) { playerHealth = player.GetComponent(); lastKnownPosition = player.transform.position; // "scanner" start } } void FixedUpdate() { if (player == null || aiVehicle == null || aiVehicle.carBody == null) return; // If player is destroyed, ignore them and just patrol if (playerHealth != null && playerHealth.isDestroyed) { currentState = AIState.Patrol; ExecutePatrol(aiVehicle.carBody.transform, aiVehicle.carBody.transform.position, aiVehicle.carBody.linearVelocity.magnitude); return; } Transform aiTransform = aiVehicle.carBody.transform; Transform playerTransform = player.carBody != null ? player.carBody.transform : player.transform; Vector3 aiPos = aiTransform.position; Vector3 playerPos = playerTransform.position; float currentSpeed = aiVehicle.carBody.linearVelocity.magnitude; // --- 1. UNSTUCK RECOVERY --- if (unstuckReverseTimer > 0f) { unstuckReverseTimer -= Time.fixedDeltaTime; aiVehicle.ProvideInputs(0f, -1f, 0f); // Reverse straight back return; } if (currentSpeed < 1.0f && currentState != AIState.Patrol) { stuckTimer += Time.fixedDeltaTime; if (stuckTimer > 1.5f) { unstuckReverseTimer = 1.5f; stuckTimer = 0f; return; } } else { stuckTimer = 0f; } // --- 2. VISION & STATE MACHINE --- bool hasLoS = CheckLineOfSight(aiTransform, playerTransform); if (hasLoS) { currentState = AIState.Pursue; sightLostTimer = 0f; lastKnownPosition = playerPos; } else if (currentState == AIState.Pursue) { // Grace period before completely losing the player (e.g. driving behind a single tree) sightLostTimer += Time.fixedDeltaTime; if (sightLostTimer >= timeToLoseTarget) { currentState = AIState.Searching; searchTimer = 0f; } } else if (currentState == AIState.Searching) { searchTimer += Time.fixedDeltaTime; if (searchTimer >= searchDuration) { currentState = AIState.Patrol; // Give up } } // --- 3. EXECUTE BEHAVIOR BASED ON STATE --- if (currentState == AIState.Pursue) { ExecutePursuit(aiTransform, aiPos, playerPos, currentSpeed); } else if (currentState == AIState.Searching) { ExecuteSearch(aiTransform, aiPos, currentSpeed); } else if (currentState == AIState.Patrol) { ExecutePatrol(aiTransform, aiPos, currentSpeed); } } private bool CheckLineOfSight(Transform aiTransform, Transform playerTransform) { Vector3 dirToPlayer = playerTransform.position - aiTransform.position; float dist = dirToPlayer.magnitude; // Out of absolute range if (dist > visionRange) return false; // Check Field of View float angle = Vector3.Angle(aiTransform.forward, dirToPlayer); if (angle > visionAngle * 0.5f) { // Proximity sense (hearing/radar scanner): Even if behind them, if very close they detect you if (dist > 15f) return false; } // Raycast to check for buildings/walls blocking the view Vector3 rayStart = aiTransform.position + Vector3.up * 1f; Vector3 rayEnd = playerTransform.position + Vector3.up * 1f; Vector3 rayDir = (rayEnd - rayStart).normalized; if (Physics.Raycast(rayStart, rayDir, out RaycastHit hit, dist, obstacleLayers)) { if (!hit.collider.CompareTag("Player") && !hit.collider.transform.IsChildOf(playerTransform)) { return false; } } return true; } private Vector3 GetNavMeshWaypoint(Vector3 start, Vector3 destination) { pathTimer += Time.fixedDeltaTime; if (pathTimer >= pathRecalculationRate || currentPath == null || currentPath.corners.Length == 0) { pathTimer = 0f; NavMesh.CalculatePath(start, destination, NavMesh.AllAreas, currentPath); } if (currentPath != null && currentPath.corners.Length > 1) { // corners[0] is our position. corners[1] is the next turn/waypoint Vector3 nextCorner = currentPath.corners[1]; // If we are very close to the next corner, aim for the one after it to smooth steering if (Vector3.Distance(start, nextCorner) < 8f && currentPath.corners.Length > 2) { nextCorner = currentPath.corners[2]; } return nextCorner; } // Fallback to straight line if NavMesh fails return destination; } private void ExecutePursuit(Transform aiTransform, Vector3 aiPos, Vector3 playerPos, float currentSpeed) { Vector3 playerVelocity = player.carBody != null ? player.carBody.linearVelocity : Vector3.zero; float distance = Vector3.Distance(aiPos, playerPos); // If close and have line of sight, bypass NavMesh and attack directly! bool isCloseAndVisible = distance < 30f && CheckLineOfSight(aiTransform, player.carBody != null ? player.carBody.transform : player.transform); if (isCloseAndVisible) { Vector3 targetPoint = playerPos; Transform playerTransform = player.carBody != null ? player.carBody.transform : player.transform; Vector3 localAIPos = playerTransform.InverseTransformPoint(aiPos); // PIT Maneuver Check if (localAIPos.z < 0f && localAIPos.z > -6f && Mathf.Abs(localAIPos.x) > 1.2f && distance < 12f) { Transform targetWheel = player.rearWheels[0]; if (player.rearWheels.Length > 1 && Vector3.Distance(aiPos, player.rearWheels[1].position) < Vector3.Distance(aiPos, targetWheel.position)) { targetWheel = player.rearWheels[1]; } Vector3 pushDirection = (playerPos - targetWheel.position).normalized; targetPoint = targetWheel.position + (playerTransform.forward * 0.5f) + (pushDirection * 0.5f); } else { // Ramming Check float leadTime = Mathf.Clamp(distance / Mathf.Max(currentSpeed, minRamSpeed), 0f, 1.0f); targetPoint = playerPos + (playerVelocity * leadTime); // Offset the target so they don't all aim for the exact same pixel (Flocking/V-Formation) float sideOffset = (GetInstanceID() % 3 == 0) ? 0f : (GetInstanceID() % 2 == 0 ? -2.5f : 2.5f); targetPoint += playerTransform.right * sideOffset; } DriveTowardsPoint(aiTransform, aiPos, targetPoint, currentSpeed, true); } else { // Distant pursuit: Use NavMesh to navigate city streets without hitting buildings Vector3 waypoint = GetNavMeshWaypoint(aiPos, playerPos); DriveTowardsPoint(aiTransform, aiPos, waypoint, currentSpeed, true); // Draw path for debugging if (currentPath != null && currentPath.corners.Length > 1) { for (int i = 0; i < currentPath.corners.Length - 1; i++) Debug.DrawLine(currentPath.corners[i], currentPath.corners[i+1], Color.magenta); } } lastKnownPosition = playerPos; } private void ExecuteSearch(Transform aiTransform, Vector3 aiPos, float currentSpeed) { // Use NavMesh to safely drive to the last known position Vector3 waypoint = GetNavMeshWaypoint(aiPos, lastKnownPosition); DriveTowardsPoint(aiTransform, aiPos, waypoint, currentSpeed, false); } private void ExecutePatrol(Transform aiTransform, Vector3 aiPos, float currentSpeed) { if (!hasPatrolPoint) { hasPatrolPoint = FindNewPatrolPoint(aiPos, aiTransform); } if (hasPatrolPoint) { Vector3 waypoint = GetNavMeshWaypoint(aiPos, currentPatrolPoint); DriveTowardsPoint(aiTransform, aiPos, waypoint, currentSpeed, false); // Reached point or stuck if (Vector3.Distance(aiPos, currentPatrolPoint) < 15f || stuckTimer > 1.0f) { if (PatrolManager.Instance != null) PatrolManager.Instance.UnregisterTarget(this); hasPatrolPoint = false; } } else { // Fallback Cruise float maxSpeedMs = 30f / 3.6f; float throttle = currentSpeed > maxSpeedMs ? 0f : 0.4f; float brake = currentSpeed > maxSpeedMs + 2f ? 0.5f : 0f; aiVehicle.ProvideInputs(0.1f, throttle, brake); } } private bool FindNewPatrolPoint(Vector3 aiPos, Transform aiTransform) { // Pick a completely random point in front of us using NavMesh Vector3 randomDir = Quaternion.Euler(0, Random.Range(-90f, 90f), 0) * aiTransform.forward; Vector3 randomPos = aiPos + (randomDir * Random.Range(60f, 150f)); // Snap it to the nearest valid NavMesh location (road, drivable, etc) if (NavMesh.SamplePosition(randomPos, out NavMeshHit hit, 50f, NavMesh.AllAreas)) { if (PatrolManager.Instance != null) { if (!PatrolManager.Instance.IsPointCrowded(hit.position, this)) { currentPatrolPoint = hit.position; PatrolManager.Instance.RegisterTarget(this, hit.position); return true; } } else { currentPatrolPoint = hit.position; return true; } } return false; } private void DriveTowardsPoint(Transform aiTransform, Vector3 aiPos, Vector3 targetPoint, float currentSpeed, bool aggressive) { Vector3 dirToTarget = (targetPoint - aiPos).normalized; dirToTarget.y = 0; if (dirToTarget.sqrMagnitude < 0.01f) { aiVehicle.ProvideInputs(0f, 0.5f, 0f); return; } // --- 1. ANTI-CRASH AVOIDANCE (Avoid other cops!) --- Vector3 avoidanceOffset = Vector3.zero; bool needsToBrakeForAlly = false; float avoidDist = aggressive ? 12f : 20f; // Ray Center if (Physics.Raycast(aiPos + Vector3.up, aiTransform.forward, out RaycastHit hitC, avoidDist)) { if (hitC.collider.GetComponentInParent() != null) { needsToBrakeForAlly = true; avoidanceOffset += aiTransform.right * (Random.value > 0.5f ? 1.5f : -1.5f); } } // Ray Left if (Physics.Raycast(aiPos + Vector3.up - aiTransform.right * 1.5f, aiTransform.forward, out RaycastHit hitL, avoidDist)) { if (hitL.collider.GetComponentInParent() != null) { avoidanceOffset += aiTransform.right * 1.5f; if (hitL.distance < 8f) needsToBrakeForAlly = true; } } // Ray Right if (Physics.Raycast(aiPos + Vector3.up + aiTransform.right * 1.5f, aiTransform.forward, out RaycastHit hitR, avoidDist)) { if (hitR.collider.GetComponentInParent() != null) { avoidanceOffset -= aiTransform.right * 1.5f; if (hitR.distance < 8f) needsToBrakeForAlly = true; } } if (avoidanceOffset != Vector3.zero) { dirToTarget = (dirToTarget + avoidanceOffset).normalized; } // --- 2. STEERING & THROTTLE --- float angleToTarget = Vector3.SignedAngle(aiTransform.forward, dirToTarget, Vector3.up); float steerInput = Mathf.Clamp(angleToTarget / 40f, -1f, 1f); float throttleInput = aggressive ? 1f : 0.6f; float brakeInput = 0f; if (Mathf.Abs(angleToTarget) > 100f) { if (currentSpeed > 5f) { throttleInput = 0f; brakeInput = 1f; } else { throttleInput = -1f; } } else if (Mathf.Abs(angleToTarget) > 60f) { throttleInput = aggressive ? 0.5f : 0.3f; } // --- 3. SPEED LIMITERS & AVOIDANCE BRAKING --- if (needsToBrakeForAlly) { throttleInput = 0f; brakeInput = aggressive ? 0.4f : 0.8f; // Try not to rear-end the guy in front } else if (!aggressive) { float maxSpeedMs = 30f / 3.6f; if (currentSpeed > maxSpeedMs) { throttleInput = 0f; if (currentSpeed > maxSpeedMs + 2f) brakeInput = 0.5f; } } aiVehicle.ProvideInputs(steerInput, throttleInput, brakeInput); // --- 4. FORCED ROTATION --- // Don't force rotation if we are actively swerving to avoid a buddy if (!needsToBrakeForAlly && (aggressive || currentSpeed < 2f) && Mathf.Abs(angleToTarget) > 2f) { Quaternion targetRotation = Quaternion.LookRotation(dirToTarget, Vector3.up); Vector3 currentEuler = aiTransform.rotation.eulerAngles; float newY = Mathf.LerpAngle(currentEuler.y, targetRotation.eulerAngles.y, forcedTurnSpeed * Time.fixedDeltaTime); aiVehicle.carBody.MoveRotation(Quaternion.Euler(currentEuler.x, newY, currentEuler.z)); } } }