Files
beyond/Assets/AI/Demon/CrystalShooterAI.cs
2025-08-20 14:28:49 +02:00

379 lines
9.3 KiB
C#

using Lean.Pool;
using System.Collections;
using UnityEngine;
namespace DemonBoss.Magic
{
/// <summary>
/// AI component for crystal turret spawned by boss
/// Rotates towards player and shoots fireballs for specified time
/// </summary>
public class CrystalShooterAI : MonoBehaviour
{
[Header("Shooting Configuration")]
[Tooltip("Transform point from which projectiles are fired")]
public Transform muzzle;
[Tooltip("Fireball prefab")]
public GameObject fireballPrefab;
[Tooltip("Fireball speed in m/s")]
public float fireballSpeed = 28f;
[Tooltip("Time between shots in seconds")]
public float fireRate = 0.7f;
[Tooltip("Maximum number of shots before auto-despawn")]
public int maxShots = 10;
[Tooltip("Wait time after shooting before despawn")]
public float despawnDelay = 3f;
[Header("Rotation Configuration")]
[Tooltip("Target rotation speed in degrees/s")]
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 value = more accurate)")]
public float aimTolerance = 5f;
[Header("Targeting")]
[Tooltip("Automatically find player on start")]
public bool autoFindPlayer = true;
[Tooltip("Player tag to search for")]
public string playerTag = "Player";
[Tooltip("Maximum shooting range")]
public float maxShootingRange = 50f;
[Header("Effects")]
[Tooltip("Particle effect at shot")]
public GameObject muzzleFlashPrefab;
[Tooltip("Shoot sound")]
public AudioClip shootSound;
[Header("Debug")]
[Tooltip("Enable debug logging")]
public bool enableDebug = false;
[Tooltip("Show gizmos in Scene View")]
public bool showGizmos = true;
// Private variables
private Transform target;
private AudioSource audioSource;
private Coroutine shootingCoroutine;
private bool isActive = false;
private int shotsFired = 0;
private float lastShotTime = 0f;
private Transform crystalTransform;
/// <summary>
/// Component initialization
/// </summary>
private void Awake()
{
crystalTransform = transform;
audioSource = GetComponent<AudioSource>();
if (audioSource == null && shootSound != null)
{
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 1f;
}
if (muzzle == null)
{
Transform muzzleChild = crystalTransform.Find("muzzle");
if (muzzleChild != null)
{
muzzle = muzzleChild;
if (enableDebug) Debug.Log("[CrystalShooterAI] Found muzzle child");
}
else
{
muzzle = crystalTransform;
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Using crystal center as muzzle");
}
}
}
/// <summary>
/// Start - begin crystal operation
/// </summary>
private void Start()
{
if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated");
if (autoFindPlayer && target == null)
{
FindPlayer();
}
StartShooting();
}
/// <summary>
/// Update - rotate crystal towards target
/// </summary>
private void Update()
{
if (!isActive) return;
RotateTowardsTarget();
}
/// <summary>
/// Find player automatically
/// </summary>
private void FindPlayer()
{
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
if (player != null)
{
SetTarget(player.transform);
if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player");
}
else
{
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag);
}
}
/// <summary>
/// Set target for crystal
/// </summary>
/// <param name="newTarget">Target transform</param>
public void SetTarget(Transform newTarget)
{
target = newTarget;
if (enableDebug && target != null)
{
Debug.Log($"[CrystalShooterAI] Set target: {target.name}");
}
}
/// <summary>
/// Start shooting cycle
/// </summary>
public void StartShooting()
{
if (isActive) return;
isActive = true;
shotsFired = 0;
shootingCoroutine = StartCoroutine(ShootingCoroutine());
if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting");
}
/// <summary>
/// Stop shooting
/// </summary>
public void StopShooting()
{
isActive = false;
if (shootingCoroutine != null)
{
StopCoroutine(shootingCoroutine);
shootingCoroutine = null;
}
if (enableDebug) Debug.Log("[CrystalShooterAI] Stopped shooting");
}
/// <summary>
/// Main coroutine handling shooting cycle
/// </summary>
private IEnumerator ShootingCoroutine()
{
while (shotsFired < maxShots && isActive)
{
if (CanShoot())
{
FireFireball();
shotsFired++;
lastShotTime = Time.time;
}
yield return new WaitForSeconds(fireRate);
}
if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)");
yield return new WaitForSeconds(despawnDelay);
DespawnCrystal();
}
/// <summary>
/// Checks if crystal can shoot
/// </summary>
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;
}
/// <summary>
/// Fires fireball towards target
/// </summary>
private void FireFireball()
{
if (fireballPrefab == null || muzzle == null)
{
if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Missing fireball prefab or muzzle");
return;
}
Vector3 shootDirection = crystalTransform.forward;
if (target != null)
{
Vector3 targetCenter = target.position + Vector3.up * 1f;
shootDirection = (targetCenter - muzzle.position).normalized;
}
GameObject fireball = LeanPool.Spawn(fireballPrefab, muzzle.position,
Quaternion.LookRotation(shootDirection));
Rigidbody fireballRb = fireball.GetComponent<Rigidbody>();
if (fireballRb != null)
{
fireballRb.linearVelocity = shootDirection * fireballSpeed;
}
PlayShootEffects();
if (enableDebug)
{
Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} in direction: {shootDirection}");
}
}
/// <summary>
/// Plays shooting effects
/// </summary>
private void PlayShootEffects()
{
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);
}
}
/// <summary>
/// Rotates crystal towards target or performs idle spin
/// </summary>
private void RotateTowardsTarget()
{
if (target != null)
{
Vector3 directionToTarget = target.position - crystalTransform.position;
directionToTarget.y = 0;
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);
}
}
/// <summary>
/// Despawns crystal from map
/// </summary>
public void DespawnCrystal()
{
if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal");
StopShooting();
LeanPool.Despawn(gameObject);
}
/// <summary>
/// Forces immediate despawn (e.g. on boss death)
/// </summary>
public void ForceDespawn()
{
if (enableDebug) Debug.Log("[CrystalShooterAI] Forced despawn");
DespawnCrystal();
}
/// <summary>
/// Returns crystal state information
/// </summary>
public bool IsActive() => isActive;
public int GetShotsFired() => shotsFired;
public int GetRemainingShots() => Mathf.Max(0, maxShots - shotsFired);
public float GetTimeSinceLastShot() => Time.time - lastShotTime;
/// <summary>
/// Draws gizmos in Scene View
/// </summary>
private void OnDrawGizmosSelected()
{
if (!showGizmos) return;
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, maxShootingRange);
if (target != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, target.position);
if (muzzle != null)
{
Gizmos.color = Color.green;
Vector3 forward = transform.forward * 5f;
Vector3 right = Quaternion.AngleAxis(aimTolerance, transform.up) * forward;
Vector3 left = Quaternion.AngleAxis(-aimTolerance, transform.up) * forward;
Gizmos.DrawLine(muzzle.position, muzzle.position + right);
Gizmos.DrawLine(muzzle.position, muzzle.position + left);
}
}
if (muzzle != null)
{
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(muzzle.position, 0.2f);
}
}
}
}