using Lean.Pool; using System.Collections; using UnityEngine; namespace DemonBoss.Magic { public class CrystalShooterAI : MonoBehaviour { [Header("Shooting Configuration")] [Tooltip("Transform point from which projectiles are fired")] public Transform muzzle; [Tooltip("Fireball prefab (projectile with its own targeting logic)")] public GameObject fireballPrefab; [Tooltip("Base seconds between shots")] public float fireRate = 0.7f; [Tooltip("Maximum number of shots before auto-despawn")] public int maxShots = 10; [Tooltip("Wait time after last shot before despawn")] public float despawnDelay = 3f; [Header("Randomization (Desync)")] [Tooltip("Random initial delay after spawn to desync turrets (seconds)")] public Vector2 initialStaggerRange = new Vector2(0.0f, 0.6f); [Tooltip("Per-shot random jitter added to fireRate (seconds). Range x means +/- x.")] public float fireRateJitter = 0.2f; [Tooltip("Aim wobble in degrees (0 = disabled). Small value adds natural dispersion.")] public float aimJitterDegrees = 0f; [Header("Rotation Configuration")] [Tooltip("Yaw rotation speed in degrees per second")] public float turnSpeed = 120f; [Tooltip("Idle spin speed when no target (degrees/s, 0 = disabled)")] public float idleSpinSpeed = 30f; [Tooltip("Aiming accuracy in degrees (smaller = stricter)")] public float aimTolerance = 5f; [Header("Targeting (Turret-Side Only)")] [Tooltip("Auto-find player on start by tag")] public bool autoFindPlayer = true; [Tooltip("Player tag to search for")] public string playerTag = "Player"; [Tooltip("Max range for allowing shots")] public float maxShootingRange = 50f; [Header("Effects")] [Tooltip("Enable or disable muzzle flash & sound effects when firing")] public bool useShootEffects = true; [Tooltip("Particle effect at shot (pooled)")] public GameObject muzzleFlashPrefab; [Tooltip("Shoot sound (played on AudioSource)")] public AudioClip shootSound; [Header("Debug")] [Tooltip("Enable debug logging")] public bool enableDebug = false; [Tooltip("Show gizmos in Scene View")] public bool showGizmos = true; private Transform target; private AudioSource audioSource; private Coroutine shootingCoroutine; private bool isActive = false; private int shotsFired = 0; private float lastShotTime = 0f; private Transform crystalTransform; private void Awake() { crystalTransform = transform; audioSource = GetComponent(); if (audioSource == null && shootSound != null) { audioSource = gameObject.AddComponent(); audioSource.playOnAwake = false; audioSource.spatialBlend = 1f; } if (muzzle == null) { Transform muzzleChild = crystalTransform.Find("muzzle"); muzzle = muzzleChild != null ? muzzleChild : crystalTransform; } } private void Start() { if (autoFindPlayer && target == null) FindPlayer(); StartShooting(); } /// Update tick: rotate towards target or idle spin. private void Update() { if (!isActive) return; RotateTowardsTarget(); } /// Attempts to find the player by tag (for turret-only aiming). private void FindPlayer() { GameObject player = GameObject.FindGameObjectWithTag(playerTag); if (player != null) SetTarget(player.transform); } /// Sets the turret's aiming target (does NOT propagate to projectiles). public void SetTarget(Transform newTarget) => target = newTarget; /// Starts the timed shooting routine (fires until maxShots, then despawns). public void StartShooting() { if (isActive) return; isActive = true; shotsFired = 0; shootingCoroutine = StartCoroutine(ShootingCoroutine()); } /// Stops the shooting routine immediately. public void StopShooting() { isActive = false; if (shootingCoroutine != null) { StopCoroutine(shootingCoroutine); shootingCoroutine = null; } } /// /// Main shooting loop with initial spawn stagger and per-shot jitter. /// private IEnumerator ShootingCoroutine() { // 1) Initial stagger so multiple crystals don't start at the same frame if (initialStaggerRange.y > 0f) { float stagger = Random.Range(initialStaggerRange.x, initialStaggerRange.y); if (stagger > 0f) yield return new WaitForSeconds(stagger); } // 2) Normal loop with CanShoot gate and per-shot jittered waits while (shotsFired < maxShots && isActive) { if (CanShoot()) { FireFireball(); shotsFired++; lastShotTime = Time.time; } float wait = fireRate + (fireRateJitter > 0f ? Random.Range(-fireRateJitter, fireRateJitter) : 0f); // Clamp wait to something safe so it never becomes non-positive if (wait < 0.05f) wait = 0.05f; yield return new WaitForSeconds(wait); } yield return new WaitForSeconds(despawnDelay); DespawnCrystal(); } /// Aiming/range gate for firing. private bool CanShoot() { if (target == null) return false; float distanceToTarget = Vector3.Distance(crystalTransform.position, target.position); if (distanceToTarget > maxShootingRange) return false; Vector3 directionToTarget = (target.position - crystalTransform.position).normalized; float angleToTarget = Vector3.Angle(crystalTransform.forward, directionToTarget); return angleToTarget <= aimTolerance; } /// Spawns a fireball oriented towards the turret's current aim direction with optional dispersion. private void FireFireball() { if (fireballPrefab == null || muzzle == null) return; Vector3 shootDirection; if (target != null) { Vector3 targetCenter = target.position + Vector3.up * 1f; shootDirection = (targetCenter - muzzle.position).normalized; } else shootDirection = crystalTransform.forward; // Apply small aim jitter (random yaw/pitch) to avoid perfect sync volleys if (aimJitterDegrees > 0f) { float yaw = Random.Range(-aimJitterDegrees, aimJitterDegrees); float pitch = Random.Range(-aimJitterDegrees * 0.5f, aimJitterDegrees * 0.5f); // usually less pitch dispersion shootDirection = Quaternion.Euler(pitch, yaw, 0f) * shootDirection; } Vector3 spawnPosition = muzzle.position; Quaternion spawnRotation = Quaternion.LookRotation(shootDirection); LeanPool.Spawn(fireballPrefab, spawnPosition, spawnRotation); PlayShootEffects(); } /// Plays muzzle VFX and shoot SFX (if enabled). private void PlayShootEffects() { if (!useShootEffects) return; if (muzzleFlashPrefab != null && muzzle != null) { GameObject flash = LeanPool.Spawn(muzzleFlashPrefab, muzzle.position, muzzle.rotation); LeanPool.Despawn(flash, 2f); } if (audioSource != null && shootSound != null) audioSource.PlayOneShot(shootSound); } /// Smooth yaw rotation towards target; idles by spinning when no target. private void RotateTowardsTarget() { if (target != null) { Vector3 directionToTarget = target.position - crystalTransform.position; directionToTarget.y = 0f; if (directionToTarget != Vector3.zero) { Quaternion targetRotation = Quaternion.LookRotation(directionToTarget); crystalTransform.rotation = Quaternion.RotateTowards( crystalTransform.rotation, targetRotation, turnSpeed * Time.deltaTime ); } } else if (idleSpinSpeed > 0f) { crystalTransform.Rotate(Vector3.up, idleSpinSpeed * Time.deltaTime); } } /// Despawns the turret via Lean Pool. public void DespawnCrystal() { StopShooting(); LeanPool.Despawn(gameObject); } /// Forces immediate despawn (e.g., boss death). public void ForceDespawn() => DespawnCrystal(); /// Returns crystal state information. public bool IsActive() => isActive; public int GetShotsFired() => shotsFired; public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired); public float GetTimeSinceLastShot() => Time.time - lastShotTime; private void OnDrawGizmosSelected() { if (!showGizmos) return; Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, maxShootingRange); if (muzzle != null) { Gizmos.color = Color.blue; Gizmos.DrawWireSphere(muzzle.position, 0.2f); } } } }