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
}
}