using UnityEngine; using System.Collections.Generic; public class UnitController : MonoBehaviour { [Header("Unit Configuration")] public UnitData unitData; public Team team; public int unitId; [Header("Current State")] public float currentHealth; public UnitState currentState = UnitState.Idle; public BattleStrategy assignedStrategy; [Header("Combat")] public Transform target; public float lastAttackTime; public int remainingAmmo; [Header("Movement & Formation")] public Vector3 formationPosition; public Vector3 rallyPoint; public bool isInFormation = true; // Private variables for AI behavior private Rigidbody rb; private UnityEngine.AI.NavMeshAgent navAgent; private UnitSightSystem sightSystem; private List nearbyAllies = new List(); private List nearbyEnemies = new List(); private float stateTimer = 0f; private bool hasChargeReady = true; private Vector3 lastKnownEnemyPosition; private float lastScanTime = 0f; private float scanInterval = 0.2f; // Fallback scanning if no sight system // Events public System.Action OnUnitDeath; public System.Action OnUnitDamaged; void Start() { InitializeUnit(); } void Update() { if (currentHealth <= 0) return; // Check if battle has started (add this debug check) BattleManager battleManager = FindFirstObjectByType(); if (battleManager != null && !battleManager.battleStarted) { return; // Don't execute AI until battle starts } else if (battleManager == null) { if (Time.frameCount % 300 == 0) // Log every 5 seconds Debug.LogWarning($"Unit {gameObject.name} cannot find BattleManager!"); } UpdateAI(); UpdateState(); stateTimer += Time.deltaTime; } private void InitializeUnit() { if (unitData == null) { Debug.LogError("UnitData is null for " + gameObject.name); return; } currentHealth = unitData.health; remainingAmmo = unitData.maxAmmo; rb = GetComponent(); // Initialize NavMesh Agent navAgent = GetComponent(); if (navAgent != null) { navAgent.speed = unitData.moveSpeed; navAgent.angularSpeed = unitData.rotationSpeed; navAgent.acceleration = 8f; navAgent.stoppingDistance = unitData.attackRange * 0.8f; Debug.Log($"{gameObject.name}: NavMeshAgent initialized"); } else { Debug.LogWarning($"{gameObject.name}: NavMeshAgent not found! Add one for proper pathfinding."); } // Initialize sight system sightSystem = GetComponent(); if (sightSystem != null) { sightSystem.OnEnemyDetected += OnEnemyDetected; sightSystem.OnEnemyLost += OnEnemyLost; sightSystem.OnAllyDetected += OnAllyDetected; sightSystem.OnAllyLost += OnAllyLost; Debug.Log($"{gameObject.name}: UnitSightSystem found and initialized"); } else { Debug.LogWarning($"UnitSightSystem not found on {gameObject.name}. Using fallback detection."); } // Set initial formation position formationPosition = transform.position; rallyPoint = transform.position; // Initialize based on unit type switch (unitData.unitType) { case UnitType.Cavalry: hasChargeReady = true; break; case UnitType.Archer: // Archers prefer to stay at range break; } } private void UpdateAI() { ScanForUnits(); ExecuteStrategy(); } private void ScanForUnits() { // Use RaycastPro sight system if available, otherwise fallback to sphere overlap if (sightSystem != null) { nearbyEnemies = sightSystem.GetDetectedEnemies(); nearbyAllies = sightSystem.GetDetectedAllies(); if (Time.frameCount % 120 == 0) // Debug every 2 seconds Debug.Log($"{gameObject.name}: Sight system found {nearbyEnemies.Count} enemies, {nearbyAllies.Count} allies"); } else { // Fallback method using sphere overlap (only scan periodically for performance) if (Time.time - lastScanTime > scanInterval) { ScanForUnitsManual(); lastScanTime = Time.time; if (Time.frameCount % 120 == 0) // Debug every 2 seconds Debug.Log($"{gameObject.name}: Manual scan found {nearbyEnemies.Count} enemies, {nearbyAllies.Count} allies"); } } } private void ScanForUnitsManual() { nearbyAllies.Clear(); nearbyEnemies.Clear(); Collider[] nearbyUnits = Physics.OverlapSphere(transform.position, unitData.detectionRange); foreach (Collider col in nearbyUnits) { UnitController unit = col.GetComponent(); if (unit != null && unit != this && unit.currentHealth > 0) { if (unit.team == this.team) { nearbyAllies.Add(unit); } else { nearbyEnemies.Add(unit); } } } } // Event handlers for sight system private void OnEnemyDetected(UnitController enemy) { if (!nearbyEnemies.Contains(enemy)) { nearbyEnemies.Add(enemy); } // React to enemy detection based on unit type and strategy ReactToEnemyDetection(enemy); } private void OnEnemyLost(UnitController enemy) { if (nearbyEnemies.Contains(enemy)) { nearbyEnemies.Remove(enemy); } // Store last known position for pursuit if (enemy != null) { lastKnownEnemyPosition = enemy.transform.position; } } private void OnAllyDetected(UnitController ally) { if (!nearbyAllies.Contains(ally)) { nearbyAllies.Add(ally); } } private void OnAllyLost(UnitController ally) { if (nearbyAllies.Contains(ally)) { nearbyAllies.Remove(ally); } } public void OnEnemyInSight(UnitController enemy) { // Called continuously while enemy is in sight // Update targeting information if (target == null || Vector3.Distance(transform.position, enemy.transform.position) < Vector3.Distance(transform.position, target.position)) { target = enemy.transform; } } private void ReactToEnemyDetection(UnitController enemy) { // Different reactions based on unit type and current strategy switch (unitData.unitType) { case UnitType.Archer: if (unitData.isRanged && remainingAmmo > 0) { // Archers should maintain distance and prepare to fire currentState = UnitState.Waiting; } break; case UnitType.Cavalry: if (assignedStrategy != null && assignedStrategy.strategyType == StrategyType.Charge) { // Cavalry charges immediately on enemy detection currentState = UnitState.Charging; } break; case UnitType.Infantry: // Infantry holds formation unless ordered otherwise if (assignedStrategy != null && assignedStrategy.strategyType == StrategyType.HoldPosition) { currentState = UnitState.Defending; } break; } } private void ExecuteStrategy() { if (assignedStrategy == null) { if (Time.frameCount % 300 == 0) // Log every 5 seconds at 60 FPS Debug.Log($"Unit {gameObject.name} has no assigned strategy!"); return; } switch (assignedStrategy.strategyType) { case StrategyType.HoldPosition: ExecuteHoldPosition(); break; case StrategyType.Flank: ExecuteFlank(); break; case StrategyType.Charge: ExecuteCharge(); break; case StrategyType.SpreadWide: ExecuteSpreadWide(); break; case StrategyType.WaitForClose: ExecuteWaitForClose(); break; } } private void ExecuteHoldPosition() { // Stay in formation and defend, but be more aggressive when enemies are nearby float distanceToFormation = Vector3.Distance(transform.position, formationPosition); // If enemies are nearby, prioritize combat over formation if (nearbyEnemies.Count > 0) { if (Time.frameCount % 180 == 0) // Debug every 3 seconds Debug.Log($"{gameObject.name}: Prioritizing combat - {nearbyEnemies.Count} enemies nearby"); currentState = UnitState.Defending; AttackNearestEnemy(); } else if (distanceToFormation > 3f) // Only return to formation if no enemies { if (Time.frameCount % 60 == 0) // Debug every second at 60 FPS Debug.Log($"{gameObject.name}: Returning to formation, distance: {distanceToFormation:F1}"); MoveTowards(formationPosition); currentState = UnitState.Moving; } else { // Stop moving and hold position if (navAgent != null && navAgent.hasPath) { navAgent.ResetPath(); } if (Time.frameCount % 180 == 0) // Debug every 3 seconds Debug.Log($"{gameObject.name}: Holding formation position"); currentState = UnitState.Defending; } } private void ExecuteFlank() { // Move to flank enemies, especially effective for cavalry if (nearbyEnemies.Count > 0) { Vector3 flankPosition = GetFlankPosition(); MoveTowards(flankPosition); currentState = UnitState.Flanking; if (Vector3.Distance(transform.position, flankPosition) < 3f) { AttackNearestEnemy(); } } else { MoveTowards(formationPosition); currentState = UnitState.Moving; } } private void ExecuteCharge() { // Aggressive charge towards enemies if (nearbyEnemies.Count > 0 && hasChargeReady) { currentState = UnitState.Charging; Transform closestEnemy = GetClosestEnemy(); if (closestEnemy != null) { MoveTowards(closestEnemy.position, unitData.moveSpeed * 1.5f); // Charge speed boost if (Vector3.Distance(transform.position, closestEnemy.position) < unitData.attackRange) { AttackTarget(closestEnemy.GetComponent()); hasChargeReady = false; // Charge cooldown Invoke(nameof(ResetCharge), 10f); } } } } private void ExecuteSpreadWide() { // Spread formation, good for archers Vector3 spreadPosition = formationPosition + GetSpreadOffset(); MoveTowards(spreadPosition); currentState = UnitState.Moving; if (nearbyEnemies.Count > 0) { AttackNearestEnemy(); } } private void ExecuteWaitForClose() { // Wait until enemies are close before attacking (archers) if (nearbyEnemies.Count > 0) { Transform closestEnemy = GetClosestEnemy(); if (closestEnemy != null) { float distance = Vector3.Distance(transform.position, closestEnemy.position); if (distance <= assignedStrategy.engagementRange) { AttackTarget(closestEnemy.GetComponent()); currentState = UnitState.Attacking; } else { currentState = UnitState.Waiting; // Face the enemy but don't attack yet FaceTarget(closestEnemy.position); } } } } private void UpdateState() { // Handle state-specific behaviors switch (currentState) { case UnitState.Retreating: if (currentHealth > unitData.health * unitData.retreatHealthThreshold) { currentState = UnitState.Idle; } break; } // Check for retreat condition if (currentHealth < unitData.health * unitData.retreatHealthThreshold && currentState != UnitState.Retreating) { currentState = UnitState.Retreating; MoveTowards(rallyPoint); } } private void AttackNearestEnemy() { Transform closestEnemy = GetClosestEnemy(); if (closestEnemy != null) { Debug.Log($"{gameObject.name}: Attempting to attack {closestEnemy.name}"); AttackTarget(closestEnemy.GetComponent()); } else { if (Time.frameCount % 180 == 0) // Debug every 3 seconds Debug.Log($"{gameObject.name}: No closest enemy found despite nearbyEnemies.Count = {nearbyEnemies.Count}"); } } private void AttackTarget(UnitController enemyUnit) { if (enemyUnit == null) return; float distance = Vector3.Distance(transform.position, enemyUnit.transform.position); // Check if target is in attack range bool inAttackRange = (sightSystem != null) ? sightSystem.IsTargetInAttackRange(enemyUnit.transform) : distance <= unitData.attackRange; // Check if we can see the target bool canSeeTarget = (sightSystem != null) ? sightSystem.CanSeeTarget(enemyUnit.transform) : true; // Fallback assumes line of sight if (Time.frameCount % 60 == 0) // Debug every second Debug.Log($"{gameObject.name}: Attacking {enemyUnit.name} - Distance: {distance:F1}, InRange: {inAttackRange}, CanSee: {canSeeTarget}, AttackRange: {unitData.attackRange}"); if (inAttackRange && canSeeTarget && Time.time - lastAttackTime >= 1f / unitData.attackSpeed) { // Face the target FaceTarget(enemyUnit.transform.position); // Perform attack if (unitData.isRanged && remainingAmmo > 0) { Debug.Log($"{gameObject.name}: Firing projectile at {enemyUnit.name}"); FireProjectile(enemyUnit); remainingAmmo--; } else if (!unitData.isRanged) { // Melee attack Debug.Log($"{gameObject.name}: Melee attack on {enemyUnit.name} for {unitData.damage} damage"); enemyUnit.TakeDamage(unitData.damage); } lastAttackTime = Time.time; currentState = UnitState.Attacking; } else if (!inAttackRange && canSeeTarget) { // Move closer to attack - be more aggressive if (Time.frameCount % 60 == 0) // Debug every second Debug.Log($"{gameObject.name}: Aggressively pursuing {enemyUnit.name} (distance: {distance:F1})"); // Set higher speed when pursuing enemies MoveTowards(enemyUnit.transform.position, 1.5f); // 50% speed boost currentState = UnitState.Moving; } } public void TakeDamage(float damage) { float actualDamage = Mathf.Max(0, damage - unitData.armor); currentHealth -= actualDamage; OnUnitDamaged?.Invoke(this, actualDamage); if (currentHealth <= 0) { Die(); } } private void Die() { currentState = UnitState.Dead; OnUnitDeath?.Invoke(this); // Disable or destroy the unit gameObject.SetActive(false); } private void MoveTowards(Vector3 targetPosition, float speedMultiplier = 1f) { if (navAgent != null) { // Use NavMesh Agent for pathfinding navAgent.speed = unitData.moveSpeed * speedMultiplier; navAgent.SetDestination(targetPosition); if (Time.frameCount % 180 == 0) // Debug every 3 seconds Debug.Log($"{gameObject.name}: NavAgent moving to {targetPosition}, distance: {Vector3.Distance(transform.position, targetPosition):F1}"); } else { // Fallback to direct movement (less ideal) Vector3 direction = (targetPosition - transform.position).normalized; if (rb != null) { rb.MovePosition(transform.position + direction * unitData.moveSpeed * speedMultiplier * Time.deltaTime); } else { transform.position += direction * unitData.moveSpeed * speedMultiplier * Time.deltaTime; } FaceTarget(targetPosition); } } private void FaceTarget(Vector3 targetPosition) { Vector3 direction = (targetPosition - transform.position).normalized; if (direction != Vector3.zero) { Quaternion targetRotation = Quaternion.LookRotation(direction); transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, unitData.rotationSpeed * Time.deltaTime); } } private Transform GetClosestEnemy() { // Use sight system if available for more accurate detection if (sightSystem != null) { UnitController closestEnemy = sightSystem.GetClosestEnemy(); return closestEnemy != null ? closestEnemy.transform : null; } // Fallback to manual search Transform closest = null; float closestDistance = float.MaxValue; foreach (UnitController enemy in nearbyEnemies) { if (enemy != null && enemy.currentHealth > 0) { float distance = Vector3.Distance(transform.position, enemy.transform.position); if (distance < closestDistance) { closest = enemy.transform; closestDistance = distance; } } } return closest; } private Vector3 GetFlankPosition() { if (nearbyEnemies.Count == 0) return formationPosition; Vector3 enemyCenter = Vector3.zero; foreach (UnitController enemy in nearbyEnemies) { enemyCenter += enemy.transform.position; } enemyCenter /= nearbyEnemies.Count; // Move to side of enemy formation Vector3 toEnemy = (enemyCenter - transform.position).normalized; Vector3 flankDirection = Vector3.Cross(toEnemy, Vector3.up).normalized; return enemyCenter + flankDirection * 10f; } private Vector3 GetSpreadOffset() { // Create spread formation based on unit ID float angle = (unitId * 360f / 10f) * Mathf.Deg2Rad; // Spread 10 units in circle float radius = unitData.formationSpacing * 2f; return new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius); } private void FireProjectile(UnitController target) { // This would instantiate a projectile - for now just apply damage directly // In a full implementation, you'd spawn a projectile GameObject target.TakeDamage(unitData.damage); } private void ResetCharge() { hasChargeReady = true; } public void SetStrategy(BattleStrategy strategy) { assignedStrategy = strategy; Debug.Log($"Unit {gameObject.name} received strategy: {(strategy != null ? strategy.strategyName : "NULL")}"); } public void SetFormationPosition(Vector3 position) { formationPosition = position; } public void SetRallyPoint(Vector3 position) { rallyPoint = position; } private void OnDestroy() { // Unsubscribe from sight system events if (sightSystem != null) { sightSystem.OnEnemyDetected -= OnEnemyDetected; sightSystem.OnEnemyLost -= OnEnemyLost; sightSystem.OnAllyDetected -= OnAllyDetected; sightSystem.OnAllyLost -= OnAllyLost; } } } public enum Team { Player, Enemy } public enum UnitState { Idle, Moving, Attacking, Defending, Charging, Flanking, Retreating, Waiting, Dead }