using Lean.Pool; using UnityEngine; namespace ArcherEnemy { /// /// AI component for archer enemy that shoots arrows at target /// Should be attached to archer enemy prefab /// public class ArcherShootingAI : MonoBehaviour { [Header("References")] [Tooltip("Transform point from which arrows are shot (usually hand bone or weapon tip)")] public Transform shootPoint; [Tooltip("Arrow prefab with ArcherProjectile component")] public GameObject arrowPrefab; [Tooltip("Animator for triggering shoot animation")] public Animator animator; [Header("Targeting")] [Tooltip("Auto-find player on start")] public bool autoFindPlayer = true; [Tooltip("Player tag to search for")] public string playerTag = "Player"; [Tooltip("Height offset for aiming (aim at chest/head)")] public float targetHeightOffset = 1.2f; [Header("Shooting Parameters")] [Tooltip("Minimum distance to shoot from")] public float minShootDistance = 8f; [Tooltip("Maximum distance to shoot from")] public float maxShootDistance = 25f; [Tooltip("Time between shots (seconds)")] public float shootCooldown = 2f; [Tooltip("Arrow launch speed (m/s)")] public float arrowSpeed = 30f; [Tooltip("How much to lead the target (predict movement)")] [Range(0f, 1f)] public float leadTargetAmount = 0.5f; [Header("Animation")] [Tooltip("Animator trigger parameter for shooting")] public string shootTriggerName = "Shoot"; [Tooltip("Delay after animation starts before spawning arrow")] public float shootAnimationDelay = 0.3f; [Header("Aiming")] [Tooltip("How quickly archer rotates to face target (degrees/sec)")] public float turnSpeed = 180f; [Tooltip("Angle tolerance for shooting (degrees)")] public float aimTolerance = 15f; [Header("Effects")] [Tooltip("Muzzle flash effect at shoot point")] public GameObject muzzleFlashPrefab; [Tooltip("Shoot sound")] public AudioClip shootSound; [Header("Debug")] [Tooltip("Enable debug logging")] public bool enableDebug = false; [Tooltip("Show aiming gizmos")] public bool showGizmos = true; #region Private Fields private Transform target; private float lastShootTime = -999f; private bool isAiming = false; private AudioSource audioSource; private Vector3 lastKnownTargetVelocity; private Vector3 lastTargetPosition; #endregion Private Fields #region Unity Lifecycle private void Awake() { // Setup audio source if (shootSound != null) { audioSource = GetComponent(); if (audioSource == null) { audioSource = gameObject.AddComponent(); audioSource.playOnAwake = false; audioSource.spatialBlend = 1f; } } // Find animator if not assigned if (animator == null) { animator = GetComponent(); } // Find shoot point if not assigned if (shootPoint == null) { // Try to find "ShootPoint" child transform Transform shootPointChild = transform.Find("ShootPoint"); shootPoint = shootPointChild != null ? shootPointChild : transform; } } private void Start() { if (autoFindPlayer) { FindPlayer(); } } private void Update() { if (target != null && isAiming) { RotateTowardsTarget(); UpdateTargetVelocity(); } } #endregion Unity Lifecycle #region Target Management /// /// Finds player by tag /// private void FindPlayer() { GameObject player = GameObject.FindGameObjectWithTag(playerTag); if (player != null) { SetTarget(player.transform); } } /// /// Sets the target to shoot at /// public void SetTarget(Transform newTarget) { target = newTarget; if (target != null) { lastTargetPosition = target.position; } } /// /// Updates target velocity for prediction /// private void UpdateTargetVelocity() { if (target == null) return; Vector3 currentPosition = target.position; lastKnownTargetVelocity = (currentPosition - lastTargetPosition) / Time.deltaTime; lastTargetPosition = currentPosition; } #endregion Target Management #region Shooting Logic /// /// Checks if archer can shoot at target /// public bool CanShoot() { if (target == null) return false; float distance = Vector3.Distance(transform.position, target.position); // Check distance range if (distance < minShootDistance || distance > maxShootDistance) { return false; } // Check cooldown if (Time.time < lastShootTime + shootCooldown) { return false; } // Check if facing target (within tolerance) Vector3 directionToTarget = (target.position - transform.position).normalized; float angleToTarget = Vector3.Angle(transform.forward, directionToTarget); if (angleToTarget > aimTolerance) { return false; } return true; } /// /// Initiates shooting sequence /// public void StartShooting() { if (!CanShoot()) { if (enableDebug) Debug.Log("[ArcherShootingAI] Cannot shoot - conditions not met"); return; } isAiming = true; // Trigger animation if (animator != null && !string.IsNullOrEmpty(shootTriggerName)) { animator.SetTrigger(shootTriggerName); } // Schedule arrow spawn after animation delay Invoke(nameof(SpawnArrow), shootAnimationDelay); lastShootTime = Time.time; if (enableDebug) Debug.Log($"[ArcherShootingAI] Started shooting at {target.name}"); } /// /// Spawns arrow projectile /// private void SpawnArrow() { if (arrowPrefab == null) { Debug.LogError("[ArcherShootingAI] Arrow prefab not assigned!"); return; } if (shootPoint == null) { Debug.LogError("[ArcherShootingAI] Shoot point not assigned!"); return; } // Calculate aim point with prediction Vector3 aimPoint = CalculateAimPoint(); // Calculate shoot direction Vector3 shootDirection = (aimPoint - shootPoint.position).normalized; // Calculate rotation for arrow Quaternion arrowRotation = Quaternion.LookRotation(shootDirection); // Spawn arrow GameObject arrow = LeanPool.Spawn(arrowPrefab, shootPoint.position, arrowRotation); // Configure arrow projectile var projectile = arrow.GetComponent(); if (projectile != null) { projectile.initialSpeed = arrowSpeed; } // Play effects PlayShootEffects(); if (enableDebug) Debug.Log($"[ArcherShootingAI] Arrow spawned, direction: {shootDirection}"); } /// /// Calculates aim point with target prediction /// private Vector3 CalculateAimPoint() { if (target == null) return shootPoint.position + transform.forward * 10f; // Base aim point Vector3 targetPosition = target.position + Vector3.up * targetHeightOffset; // Add prediction based on target velocity if (leadTargetAmount > 0f && lastKnownTargetVelocity.sqrMagnitude > 0.1f) { // Estimate time to target float distance = Vector3.Distance(shootPoint.position, targetPosition); float timeToTarget = distance / arrowSpeed; // Predict future position Vector3 predictedOffset = lastKnownTargetVelocity * timeToTarget * leadTargetAmount; targetPosition += predictedOffset; } return targetPosition; } /// /// Stops shooting sequence /// public void StopShooting() { isAiming = false; CancelInvoke(nameof(SpawnArrow)); } #endregion Shooting Logic #region Effects private void PlayShootEffects() { // Spawn muzzle flash if (muzzleFlashPrefab != null && shootPoint != null) { GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, shootPoint.position, shootPoint.rotation); LeanPool.Despawn(flash, 2f); } // Play shoot sound if (audioSource != null && shootSound != null) { audioSource.PlayOneShot(shootSound); } } #endregion Effects #region Rotation /// /// Smoothly rotates archer to face target /// private void RotateTowardsTarget() { if (target == null) return; Vector3 directionToTarget = (target.position - transform.position); directionToTarget.y = 0f; // Keep rotation on Y axis only if (directionToTarget.sqrMagnitude > 0.001f) { Quaternion targetRotation = Quaternion.LookRotation(directionToTarget); transform.rotation = Quaternion.RotateTowards( transform.rotation, targetRotation, turnSpeed * Time.deltaTime ); } } /// /// Checks if archer is facing target within tolerance /// public bool IsFacingTarget() { if (target == null) return false; Vector3 directionToTarget = (target.position - transform.position).normalized; float angle = Vector3.Angle(transform.forward, directionToTarget); return angle <= aimTolerance; } #endregion Rotation #region Public Query Methods public Transform GetTarget() => target; public bool IsAiming() => isAiming; public float GetTimeSinceLastShot() => Time.time - lastShootTime; public float GetDistanceToTarget() => target != null ? Vector3.Distance(transform.position, target.position) : float.MaxValue; public bool IsInShootingRange() => GetDistanceToTarget() >= minShootDistance && GetDistanceToTarget() <= maxShootDistance; #endregion Public Query Methods #region Gizmos #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (!showGizmos) return; // Draw shooting range Gizmos.color = Color.yellow; UnityEditor.Handles.color = new Color(1f, 1f, 0f, 0.1f); UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, minShootDistance); Gizmos.color = Color.green; UnityEditor.Handles.color = new Color(0f, 1f, 0f, 0.1f); UnityEditor.Handles.DrawSolidDisc(transform.position, Vector3.up, maxShootDistance); // Draw shoot point if (shootPoint != null) { Gizmos.color = Color.red; Gizmos.DrawWireSphere(shootPoint.position, 0.1f); Gizmos.DrawLine(shootPoint.position, shootPoint.position + shootPoint.forward * 2f); } // Draw aim line to target if (target != null && Application.isPlaying) { Vector3 aimPoint = CalculateAimPoint(); Gizmos.color = Color.cyan; Gizmos.DrawLine(shootPoint != null ? shootPoint.position : transform.position, aimPoint); Gizmos.DrawWireSphere(aimPoint, 0.2f); } } #endif #endregion Gizmos } }