From 081c0a0e27f6dde75e3b7911cc40c36088fb24e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Mi=C5=9B?= <>
Date: Mon, 25 Aug 2025 12:57:15 +0200
Subject: [PATCH] Turret refactor
---
Assets/AI/Demon/CrystalShooterAI.cs | 113 +++--
Assets/AI/Demon/FireBall.prefab | 26 +-
Assets/AI/Demon/FireballDamage.cs | 386 ------------------
Assets/AI/Demon/FireballProjectile.cs | 314 ++++++++++++++
...age.cs.meta => FireballProjectile.cs.meta} | 0
Assets/AI/Demon/Turet.prefab | 8 +-
6 files changed, 370 insertions(+), 477 deletions(-)
delete mode 100644 Assets/AI/Demon/FireballDamage.cs
create mode 100644 Assets/AI/Demon/FireballProjectile.cs
rename Assets/AI/Demon/{FireballDamage.cs.meta => FireballProjectile.cs.meta} (100%)
diff --git a/Assets/AI/Demon/CrystalShooterAI.cs b/Assets/AI/Demon/CrystalShooterAI.cs
index b0d5370d3..8188bf83a 100644
--- a/Assets/AI/Demon/CrystalShooterAI.cs
+++ b/Assets/AI/Demon/CrystalShooterAI.cs
@@ -1,59 +1,55 @@
-using Lean.Pool;
+using Lean.Pool;
using System.Collections;
using UnityEngine;
namespace DemonBoss.Magic
{
- ///
- /// AI component for crystal turret spawned by boss
- /// Rotates towards player and shoots fireballs for specified time
- ///
public class CrystalShooterAI : MonoBehaviour
{
[Header("Shooting Configuration")]
[Tooltip("Transform point from which projectiles are fired")]
public Transform muzzle;
- [Tooltip("Fireball prefab")]
+ [Tooltip("Fireball prefab (projectile with its own targeting logic)")]
public GameObject fireballPrefab;
- [Tooltip("Fireball speed in m/s")]
- public float fireballSpeed = 28f;
-
- [Tooltip("Time between shots in seconds")]
+ [Tooltip("Seconds between shots")]
public float fireRate = 0.7f;
[Tooltip("Maximum number of shots before auto-despawn")]
public int maxShots = 10;
- [Tooltip("Wait time after shooting before despawn")]
+ [Tooltip("Wait time after last shot before despawn")]
public float despawnDelay = 3f;
[Header("Rotation Configuration")]
- [Tooltip("Target rotation speed in degrees/s")]
+ [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 value = more accurate)")]
+ [Tooltip("Aiming accuracy in degrees (smaller = stricter)")]
public float aimTolerance = 5f;
- [Header("Targeting")]
- [Tooltip("Automatically find player on start")]
+ [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("Maximum shooting range")]
+ [Tooltip("Max range for allowing shots")]
public float maxShootingRange = 50f;
[Header("Effects")]
- [Tooltip("Particle effect at shot")]
+ [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")]
+ [Tooltip("Shoot sound (played on AudioSource)")]
public AudioClip shootSound;
[Header("Debug")]
@@ -63,20 +59,14 @@ namespace DemonBoss.Magic
[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;
- ///
- /// Component initialization
- ///
private void Awake()
{
crystalTransform = transform;
@@ -91,6 +81,7 @@ namespace DemonBoss.Magic
if (muzzle == null)
{
+ // Try to find a child named "muzzle"; fallback to self
Transform muzzleChild = crystalTransform.Find("muzzle");
if (muzzleChild != null)
{
@@ -105,33 +96,27 @@ namespace DemonBoss.Magic
}
}
- ///
- /// Start - begin crystal operation
- ///
private void Start()
{
if (enableDebug) Debug.Log("[CrystalShooterAI] Crystal activated");
if (autoFindPlayer && target == null)
- {
FindPlayer();
- }
StartShooting();
}
///
- /// Update - rotate crystal towards target
+ /// Update tick: rotate towards target or idle spin.
///
private void Update()
{
if (!isActive) return;
-
RotateTowardsTarget();
}
///
- /// Find player automatically
+ /// Attempts to find the player by tag (for turret-only aiming).
///
private void FindPlayer()
{
@@ -141,42 +126,37 @@ namespace DemonBoss.Magic
SetTarget(player.transform);
if (enableDebug) Debug.Log("[CrystalShooterAI] Automatically found player");
}
- else
+ else if (enableDebug)
{
- if (enableDebug) Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag);
+ Debug.LogWarning("[CrystalShooterAI] Cannot find player with tag: " + playerTag);
}
}
///
- /// Set target for crystal
+ /// Sets the turret's aiming target (does NOT propagate to projectiles).
///
- /// Target transform
public void SetTarget(Transform newTarget)
{
target = newTarget;
if (enableDebug && target != null)
- {
Debug.Log($"[CrystalShooterAI] Set target: {target.name}");
- }
}
///
- /// Start shooting cycle
+ /// Starts the timed shooting routine (fires until maxShots, then despawns).
///
public void StartShooting()
{
if (isActive) return;
-
isActive = true;
shotsFired = 0;
shootingCoroutine = StartCoroutine(ShootingCoroutine());
-
if (enableDebug) Debug.Log("[CrystalShooterAI] Starting shooting");
}
///
- /// Stop shooting
+ /// Stops the shooting routine immediately.
///
public void StopShooting()
{
@@ -192,7 +172,8 @@ namespace DemonBoss.Magic
}
///
- /// Main coroutine handling shooting cycle
+ /// Main shooting loop: checks aim/range → spawns fireball → waits fireRate.
+ /// After finishing, waits a short delay and despawns the turret.
///
private IEnumerator ShootingCoroutine()
{
@@ -211,12 +192,11 @@ namespace DemonBoss.Magic
if (enableDebug) Debug.Log($"[CrystalShooterAI] Finished shooting ({shotsFired} shots)");
yield return new WaitForSeconds(despawnDelay);
-
DespawnCrystal();
}
///
- /// Checks if crystal can shoot
+ /// Aiming/range gate for firing.
///
private bool CanShoot()
{
@@ -232,7 +212,7 @@ namespace DemonBoss.Magic
}
///
- /// Fires fireball towards target
+ /// Spawns a fireball oriented towards the turret's current aim direction.
///
private void FireFireball()
{
@@ -242,39 +222,41 @@ namespace DemonBoss.Magic
return;
}
- Vector3 shootDirection = crystalTransform.forward;
+ Vector3 shootDirection;
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();
- if (fireballRb != null)
+ else
{
- fireballRb.linearVelocity = shootDirection * fireballSpeed;
+ shootDirection = crystalTransform.forward;
}
+ Vector3 spawnPosition = muzzle.position;
+ Quaternion spawnRotation = Quaternion.LookRotation(shootDirection);
+
+ LeanPool.Spawn(fireballPrefab, spawnPosition, spawnRotation);
+
PlayShootEffects();
if (enableDebug)
{
- Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} in direction: {shootDirection}");
+ Debug.Log($"[CrystalShooterAI] Shot #{shotsFired + 1} at {spawnPosition} dir: {shootDirection}");
+ Debug.DrawRay(spawnPosition, shootDirection * 8f, Color.red, 2f);
}
}
///
- /// Plays shooting effects
+ /// 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);
}
@@ -285,14 +267,14 @@ namespace DemonBoss.Magic
}
///
- /// Rotates crystal towards target or performs idle spin
+ /// 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 = 0;
+ directionToTarget.y = 0f;
if (directionToTarget != Vector3.zero)
{
@@ -311,19 +293,17 @@ namespace DemonBoss.Magic
}
///
- /// Despawns crystal from map
+ /// Despawns the turret via Lean Pool.
///
public void DespawnCrystal()
{
if (enableDebug) Debug.Log("[CrystalShooterAI] Despawning crystal");
-
StopShooting();
-
LeanPool.Despawn(gameObject);
}
///
- /// Forces immediate despawn (e.g. on boss death)
+ /// Forces immediate despawn (e.g., boss death).
///
public void ForceDespawn()
{
@@ -331,9 +311,7 @@ namespace DemonBoss.Magic
DespawnCrystal();
}
- ///
- /// Returns crystal state information
- ///
+ /// Returns crystal state information.
public bool IsActive() => isActive;
public int GetShotsFired() => shotsFired;
@@ -343,7 +321,7 @@ namespace DemonBoss.Magic
public float GetTimeSinceLastShot() => Time.time - lastShotTime;
///
- /// Draws gizmos in Scene View
+ /// Gizmos for range and aim visualization.
///
private void OnDrawGizmosSelected()
{
@@ -366,6 +344,7 @@ namespace DemonBoss.Magic
Gizmos.DrawLine(muzzle.position, muzzle.position + right);
Gizmos.DrawLine(muzzle.position, muzzle.position + left);
+ Gizmos.DrawLine(muzzle.position, muzzle.position + forward);
}
}
diff --git a/Assets/AI/Demon/FireBall.prefab b/Assets/AI/Demon/FireBall.prefab
index 72febf200..8a9f0d738 100644
--- a/Assets/AI/Demon/FireBall.prefab
+++ b/Assets/AI/Demon/FireBall.prefab
@@ -9618,7 +9618,7 @@ CapsuleCollider:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
- m_IsTrigger: 0
+ m_IsTrigger: 1
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
@@ -9638,25 +9638,11 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 1a863cb5e6092ec4a936c4eb21bb9166, type: 3}
m_Name:
m_EditorClassIdentifier:
+ targetTag: Player
+ speed: 6
+ lockTime: 15
+ maxLifeTime: 30
+ arrivalTolerance: 0.25
damage: 25
- damageOnce: 1
- targetLayerMask:
- serializedVersion: 2
- m_Bits: 256
- enableHoming: 1
- homingStrength: 1.5
- maxHomingDistance: 15
- homingDuration: 3
- continueAfterHoming: 1
- fireballSpeed: 10
- maxTurnRate: 90
- impactEffectPrefab: {fileID: 132858, guid: c5cabec71c815b64ab6e2547b594ec81, type: 3}
- impactSound: {fileID: 8300000, guid: 5dd60193d2ea89e47ae309eec2c3852e, type: 3}
knockbackForce: 5
- maxLifetime: 5
- destroyOnTerrain: 1
- terrainLayerMask:
- serializedVersion: 2
- m_Bits: 1
enableDebug: 0
- showGizmos: 1
diff --git a/Assets/AI/Demon/FireballDamage.cs b/Assets/AI/Demon/FireballDamage.cs
deleted file mode 100644
index 0dc6f697a..000000000
--- a/Assets/AI/Demon/FireballDamage.cs
+++ /dev/null
@@ -1,386 +0,0 @@
-using Invector;
-using Invector.vCharacterController;
-using Lean.Pool;
-using UnityEngine;
-
-namespace DemonBoss.Magic
-{
- ///
- /// Component handling damage and collisions for fireball fired by crystals
- /// Deals damage on collision with player and automatically despawns
- ///
- [RequireComponent(typeof(Collider))]
- public class FireballDamage : MonoBehaviour
- {
- [Header("Damage Configuration")]
- [Tooltip("Damage dealt by fireball")]
- public float damage = 25f;
-
- [Tooltip("Whether fireball can deal damage only once")]
- public bool damageOnce = true;
-
- [Tooltip("Layer mask for targets that can be hit")]
- public LayerMask targetLayerMask = -1;
-
- [Header("Impact Effects")]
- [Tooltip("Explosion effect prefab on hit")]
- public GameObject impactEffectPrefab;
-
- [Tooltip("Impact sound")]
- public AudioClip impactSound;
-
- [Tooltip("Knockback force on hit")]
- public float knockbackForce = 5f;
-
- [Header("Lifetime")]
- [Tooltip("Maximum fireball lifetime in seconds (failsafe)")]
- public float maxLifetime = 5f;
-
- [Tooltip("Whether to destroy fireball on terrain collision")]
- public bool destroyOnTerrain = true;
-
- [Tooltip("Layer mask for terrain/obstacles")]
- public LayerMask terrainLayerMask = 1;
-
- [Header("Debug")]
- [Tooltip("Enable debug logging")]
- public bool enableDebug = false;
-
- // Private variables
- private bool hasDealtDamage = false;
-
- private float spawnTime;
- private AudioSource audioSource;
- private Collider fireballCollider;
- private Rigidbody fireballRigidbody;
-
- ///
- /// Component initialization
- ///
- private void Awake()
- {
- fireballCollider = GetComponent();
- fireballRigidbody = GetComponent();
-
- if (fireballCollider != null && !fireballCollider.isTrigger)
- {
- fireballCollider.isTrigger = true;
- if (enableDebug) Debug.Log("[FireballDamage] Set collider as trigger");
- }
-
- audioSource = GetComponent();
- if (audioSource == null && impactSound != null)
- {
- audioSource = gameObject.AddComponent();
- audioSource.playOnAwake = false;
- audioSource.spatialBlend = 1f;
- }
- }
-
- ///
- /// Start - note spawn time and start failsafe timer
- ///
- private void Start()
- {
- spawnTime = Time.time;
- hasDealtDamage = false;
-
- Invoke(nameof(FailsafeDespawn), maxLifetime);
-
- if (enableDebug) Debug.Log("[FireballDamage] Fireball initialized");
- }
-
- ///
- /// Trigger collision handling - dealing damage
- ///
- /// Collider that fireball collides with
- private void OnTriggerEnter(Collider other)
- {
- if (enableDebug) Debug.Log($"[FireballDamage] Collision with: {other.name}");
-
- if (IsValidTarget(other))
- {
- DealDamageToTarget(other);
- CreateImpactEffect(other.transform.position);
- DespawnFireball();
- return;
- }
-
- if (destroyOnTerrain && IsTerrainCollision(other))
- {
- if (enableDebug) Debug.Log("[FireballDamage] Terrain collision");
- CreateImpactEffect(transform.position);
- DespawnFireball();
- return;
- }
- }
-
- ///
- /// Checks if collider is valid target
- ///
- /// Collider to check
- /// True if it's valid target
- private bool IsValidTarget(Collider other)
- {
- if ((targetLayerMask.value & (1 << other.gameObject.layer)) == 0)
- return false;
-
- if (damageOnce && hasDealtDamage)
- return false;
-
- var damageReceiver = other.GetComponent();
- var healthController = other.GetComponent();
- var character = other.GetComponent();
- var thirdPersonController = other.GetComponent();
-
- if (damageReceiver == null) damageReceiver = other.GetComponentInParent();
- if (healthController == null) healthController = other.GetComponentInParent();
- if (character == null) character = other.GetComponentInParent();
- if (thirdPersonController == null) thirdPersonController = other.GetComponentInParent();
-
- return damageReceiver != null || healthController != null || character != null || thirdPersonController != null;
- }
-
- ///
- /// Checks if it's terrain collision
- ///
- /// Collider to check
- /// True if it's terrain
- private bool IsTerrainCollision(Collider other)
- {
- return (terrainLayerMask.value & (1 << other.gameObject.layer)) != 0;
- }
-
- ///
- /// Deals damage to target - ulepszona wersja
- ///
- /// Target collider
- private void DealDamageToTarget(Collider targetCollider)
- {
- if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
-
- Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
- Vector3 hitDirection = GetHitDirection(targetCollider);
-
- vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage));
- damageInfo.sender = transform;
- damageInfo.hitPosition = hitPoint;
-
- if (knockbackForce > 0f)
- {
- damageInfo.force = hitDirection * knockbackForce;
- }
-
- bool damageDealt = false;
-
- var damageReceiver = targetCollider.GetComponent();
- if (damageReceiver == null) damageReceiver = targetCollider.GetComponentInParent();
-
- if (damageReceiver != null && !damageDealt)
- {
- damageReceiver.TakeDamage(damageInfo);
- damageDealt = true;
- hasDealtDamage = true;
- if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
- }
-
- if (!damageDealt)
- {
- var healthController = targetCollider.GetComponent();
- if (healthController == null) healthController = targetCollider.GetComponentInParent();
-
- if (healthController != null)
- {
- healthController.TakeDamage(damageInfo);
- damageDealt = true;
- hasDealtDamage = true;
- if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
- }
- }
-
- if (!damageDealt)
- {
- var thirdPersonController = targetCollider.GetComponent();
- if (thirdPersonController == null) thirdPersonController = targetCollider.GetComponentInParent();
-
- if (thirdPersonController != null)
- {
- if (thirdPersonController is Beyond.bThirdPersonController beyondController)
- {
- if (!beyondController.GodMode && !beyondController.isImmortal)
- {
- thirdPersonController.TakeDamage(damageInfo);
- damageDealt = true;
- hasDealtDamage = true;
- if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
- }
- else
- {
- if (enableDebug) Debug.Log("[FireballDamage] Player is immortal or in God Mode - no damage dealt");
- }
- }
- else
- {
- thirdPersonController.TakeDamage(damageInfo);
- damageDealt = true;
- hasDealtDamage = true;
- if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
- }
- }
- }
-
- if (!damageDealt)
- {
- if (enableDebug) Debug.LogWarning("[FireballDamage] Could not deal damage - no valid damage receiver found!");
- }
- }
-
- ///
- /// Takes the closest point on the collider as the point of impact
- ///
- private Vector3 GetClosestPointOnCollider(Collider targetCollider)
- {
- return targetCollider.ClosestPoint(transform.position);
- }
-
- ///
- /// Calculates the direction of impact for knockback
- ///
- private Vector3 GetHitDirection(Collider targetCollider)
- {
- Vector3 direction;
-
- if (fireballRigidbody != null && fireballRigidbody.linearVelocity.magnitude > 0.1f)
- {
- direction = fireballRigidbody.linearVelocity.normalized;
- }
- else
- {
- direction = (targetCollider.transform.position - transform.position).normalized;
- }
-
- direction.y = Mathf.Max(0.2f, direction.y);
- return direction.normalized;
- }
-
- ///
- /// Creates impact effect
- ///
- /// Impact position
- private void CreateImpactEffect(Vector3 impactPosition)
- {
- if (impactEffectPrefab != null)
- {
- GameObject impact = LeanPool.Spawn(impactEffectPrefab, impactPosition, Quaternion.identity);
-
- LeanPool.Despawn(impact, 3f);
-
- if (enableDebug) Debug.Log("[FireballDamage] Impact effect created");
- }
-
- if (audioSource != null && impactSound != null)
- {
- audioSource.PlayOneShot(impactSound);
- }
- }
-
- ///
- /// Despawns fireball from map
- ///
- private void DespawnFireball()
- {
- if (enableDebug) Debug.Log("[FireballDamage] Despawning fireball");
-
- CancelInvoke(nameof(FailsafeDespawn));
-
- LeanPool.Despawn(gameObject);
- }
-
- ///
- /// Failsafe despawn - removes fireball after maximum lifetime
- ///
- private void FailsafeDespawn()
- {
- if (enableDebug) Debug.Log("[FireballDamage] Failsafe despawn - lifetime exceeded");
- DespawnFireball();
- }
-
- ///
- /// Forces immediate despawn
- ///
- public void ForceDespawn()
- {
- if (enableDebug) Debug.Log("[FireballDamage] Forced despawn");
- DespawnFireball();
- }
-
- ///
- /// Checks if fireball has already dealt damage
- ///
- /// True if damage was already dealt
- public bool HasDealtDamage()
- {
- return hasDealtDamage;
- }
-
- ///
- /// Returns fireball lifetime
- ///
- /// Time in seconds since spawn
- public float GetLifetime()
- {
- return Time.time - spawnTime;
- }
-
- ///
- /// Sets new damage (e.g. for different difficulty levels)
- ///
- /// New damage
- public void SetDamage(float newDamage)
- {
- damage = newDamage;
- if (enableDebug) Debug.Log($"[FireballDamage] Set new damage: {damage}");
- }
-
- ///
- /// Sets new knockback
- ///
- /// New knockback force
- public void SetKnockback(float newKnockback)
- {
- knockbackForce = newKnockback;
- if (enableDebug) Debug.Log($"[FireballDamage] Set new knockback: {knockbackForce}");
- }
-
- ///
- /// Resets fireball state (useful for pooling)
- ///
- public void ResetFireball()
- {
- hasDealtDamage = false;
- spawnTime = Time.time;
-
- CancelInvoke();
-
- Invoke(nameof(FailsafeDespawn), maxLifetime);
-
- if (enableDebug) Debug.Log("[FireballDamage] Fireball reset");
- }
-
- ///
- /// Called on spawn by Lean Pool
- ///
- private void OnSpawn()
- {
- ResetFireball();
- }
-
- ///
- /// Called on despawn by Lean Pool
- ///
- private void OnDespawn()
- {
- CancelInvoke();
- }
- }
-}
\ No newline at end of file
diff --git a/Assets/AI/Demon/FireballProjectile.cs b/Assets/AI/Demon/FireballProjectile.cs
new file mode 100644
index 000000000..e87e2a9ac
--- /dev/null
+++ b/Assets/AI/Demon/FireballProjectile.cs
@@ -0,0 +1,314 @@
+using Invector;
+using Invector.vCharacterController;
+using Lean.Pool;
+using UnityEngine;
+
+namespace DemonBoss.Magic
+{
+ public class FireballProjectile : MonoBehaviour
+ {
+ #region Configuration
+
+ [Header("Targeting")]
+ [Tooltip("Tag of the target to chase while tracking phase is active.")]
+ public string targetTag = "Player";
+
+ [Tooltip("Vertical offset added to the target position (e.g., aim at chest/head instead of feet).")]
+ public float targetHeightOffset = 1.0f;
+
+ [Header("Movement")]
+ [Tooltip("Units per second.")]
+ public float speed = 6f;
+
+ [Tooltip("Seconds to track the player before locking to last known position.")]
+ public float lockTime = 15f;
+
+ [Tooltip("Seconds before the projectile auto-despawns regardless of state.")]
+ public float maxLifeTime = 30f;
+
+ [Tooltip("Distance threshold to consider we arrived at the locked position.")]
+ public float arrivalTolerance = 0.25f;
+
+ [Header("Damage")]
+ [Tooltip("Base damage dealt to the target.")]
+ public int damage = 20;
+
+ [Tooltip("Optional knockback impulse magnitude applied along hit direction.")]
+ public float knockbackForce = 0f;
+
+ [Header("Debug")]
+ [Tooltip("Enable verbose debug logs.")]
+ public bool enableDebug = false;
+
+ #endregion Configuration
+
+ #region Runtime State
+
+ private Transform player;
+ private Vector3 lockedTargetPos;
+ private bool isLocked = false;
+ private bool hasDealtDamage = false;
+ private float timer = 0f;
+
+ #endregion Runtime State
+
+ #region Unity Lifecycle
+
+ ///
+ /// Called when the object becomes enabled (including when spawned from a pool).
+ /// Resets runtime state and acquires the target by tag.
+ ///
+ private void OnEnable()
+ {
+ ResetRuntimeState();
+ AcquireTargetByTag();
+ }
+
+ ///
+ /// Per-frame update: handle tracking/lock movement and lifetime/despawn conditions.
+ ///
+ private void Update()
+ {
+ timer += Time.deltaTime;
+
+ // Transition to 'locked to last position' after lockTime
+ if (!isLocked && timer >= lockTime && player != null)
+ {
+ lockedTargetPos = player.position + Vector3.up * targetHeightOffset;
+ isLocked = true;
+ if (enableDebug) Debug.Log($"[Fireball] Locked to last known player position: {lockedTargetPos}");
+ }
+
+ // Determine current aim point
+ Vector3 aimPoint;
+ if (!isLocked && player != null)
+ {
+ // Tracking phase: follow live player position
+ aimPoint = player.position + Vector3.up * targetHeightOffset;
+ }
+ else if (isLocked)
+ {
+ // Locked phase: fly to memorized position
+ aimPoint = lockedTargetPos;
+ }
+ else
+ {
+ // Fallback: no player found, keep moving forward
+ aimPoint = transform.position + transform.forward * 1000f;
+ }
+
+ // Move towards aim point
+ Vector3 toTarget = aimPoint - transform.position;
+ Vector3 step = toTarget.normalized * speed * Time.deltaTime;
+ transform.position += step;
+
+ // Face movement direction (optional but nice for VFX)
+ if (step.sqrMagnitude > 0.0001f)
+ transform.forward = step.normalized;
+
+ // If locked and close enough to the locked point → despawn
+ if (isLocked && toTarget.magnitude <= arrivalTolerance)
+ {
+ if (enableDebug) Debug.Log("[Fireball] Arrived at locked position → Despawn");
+ Despawn();
+ return;
+ }
+
+ // Hard cap lifetime
+ if (timer >= maxLifeTime)
+ {
+ if (enableDebug) Debug.Log("[Fireball] Max lifetime reached → Despawn");
+ Despawn();
+ }
+ }
+
+ ///
+ /// Trigger hit handler – applies damage when colliding with the intended target tag and then despawns.
+ ///
+ private void OnTriggerEnter(Collider other)
+ {
+ if (hasDealtDamage) return; // guard against multiple triggers in a single frame
+
+ if (other.CompareTag(targetTag))
+ {
+ DealDamageToTarget(other);
+ Despawn();
+ }
+ }
+
+ #endregion Unity Lifecycle
+
+ #region Target Acquisition
+
+ ///
+ /// Finds the target by tag (e.g., "Player"). Called on enable and can be retried if needed.
+ ///
+ private void AcquireTargetByTag()
+ {
+ var playerObj = GameObject.FindGameObjectWithTag(targetTag);
+ player = playerObj ? playerObj.transform : null;
+
+ if (enableDebug)
+ {
+ if (player != null) Debug.Log($"[Fireball] Target found by tag '{targetTag}'.");
+ else Debug.LogWarning($"[Fireball] No target with tag '{targetTag}' found – moving forward fallback.");
+ }
+ }
+
+ #endregion Target Acquisition
+
+ #region Damage Pipeline (Invector-style)
+
+ ///
+ /// Applies damage using available receivers on the hit collider or its parents:
+ /// vIDamageReceiver → vHealthController → vThirdPersonController (with Beyond variant checks).
+ ///
+ private void DealDamageToTarget(Collider targetCollider)
+ {
+ if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
+
+ Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
+ Vector3 hitDirection = GetHitDirection(targetCollider);
+
+ // Build vDamage payload (Invector)
+ vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
+ {
+ sender = transform,
+ hitPosition = hitPoint
+ };
+
+ if (knockbackForce > 0f)
+ damageInfo.force = hitDirection * knockbackForce;
+
+ bool damageDealt = false;
+
+ // 1) Try generic vIDamageReceiver (collider or parent)
+ var damageReceiver = targetCollider.GetComponent() ??
+ targetCollider.GetComponentInParent();
+
+ if (damageReceiver != null && !damageDealt)
+ {
+ damageReceiver.TakeDamage(damageInfo);
+ damageDealt = true;
+ hasDealtDamage = true;
+ if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
+ }
+
+ // 2) Fallback to vHealthController
+ if (!damageDealt)
+ {
+ var healthController = targetCollider.GetComponent() ??
+ targetCollider.GetComponentInParent();
+
+ if (healthController != null)
+ {
+ healthController.TakeDamage(damageInfo);
+ damageDealt = true;
+ hasDealtDamage = true;
+ if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
+ }
+ }
+
+ // 3) Fallback to vThirdPersonController (handle Beyond variant)
+ if (!damageDealt)
+ {
+ var tpc = targetCollider.GetComponent() ??
+ targetCollider.GetComponentInParent();
+
+ if (tpc != null)
+ {
+ if (tpc is Beyond.bThirdPersonController beyond)
+ {
+ if (!beyond.GodMode && !beyond.isImmortal)
+ {
+ tpc.TakeDamage(damageInfo);
+ damageDealt = true;
+ hasDealtDamage = true;
+ if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
+ }
+ else
+ {
+ if (enableDebug) Debug.Log("[FireballDamage] Target is immortal / GodMode – no damage dealt");
+ }
+ }
+ else
+ {
+ tpc.TakeDamage(damageInfo);
+ damageDealt = true;
+ hasDealtDamage = true;
+ if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
+ }
+ }
+ }
+
+ if (!damageDealt && enableDebug)
+ Debug.LogWarning("[FireballDamage] Could not deal damage – no valid damage receiver found!");
+ }
+
+ ///
+ /// Computes the closest point on the target collider to this projectile position.
+ ///
+ private Vector3 GetClosestPointOnCollider(Collider col)
+ {
+ return col.ClosestPoint(transform.position);
+ }
+
+ ///
+ /// Computes a reasonable hit direction from projectile to target center (fallbacks to forward).
+ ///
+ private Vector3 GetHitDirection(Collider col)
+ {
+ Vector3 dir = (col.bounds.center - transform.position).normalized;
+ return dir.sqrMagnitude > 0.0001f ? dir : transform.forward;
+ }
+
+ #endregion Damage Pipeline (Invector-style)
+
+ #region Pooling & Utilities
+
+ ///
+ /// Returns this projectile to the Lean Pool (safe to call on non-pooled too).
+ ///
+ private void Despawn()
+ {
+ if (enableDebug) Debug.Log("[Fireball] Despawn via LeanPool");
+ LeanPool.Despawn(gameObject);
+ }
+
+ ///
+ /// Clears/initializes runtime variables so pooled instances behave like fresh spawns.
+ ///
+ private void ResetRuntimeState()
+ {
+ timer = 0f;
+ isLocked = false;
+ hasDealtDamage = false;
+ lockedTargetPos = Vector3.zero;
+ }
+
+ #endregion Pooling & Utilities
+
+#if UNITY_EDITOR
+
+ ///
+ /// Optional gizmo: shows lock radius and direction for quick debugging.
+ ///
+ private void OnDrawGizmosSelected()
+ {
+ Gizmos.color = Color.cyan;
+ Gizmos.DrawWireSphere(transform.position, 0.25f);
+
+ // Draw a small forward arrow
+ Gizmos.DrawLine(transform.position, transform.position + transform.forward * 1.0f);
+
+ // Arrival tolerance sphere (only meaningful when locked)
+ if (isLocked)
+ {
+ Gizmos.color = Color.yellow;
+ Gizmos.DrawWireSphere(lockedTargetPos, arrivalTolerance);
+ }
+ }
+
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Assets/AI/Demon/FireballDamage.cs.meta b/Assets/AI/Demon/FireballProjectile.cs.meta
similarity index 100%
rename from Assets/AI/Demon/FireballDamage.cs.meta
rename to Assets/AI/Demon/FireballProjectile.cs.meta
diff --git a/Assets/AI/Demon/Turet.prefab b/Assets/AI/Demon/Turet.prefab
index 548f44158..8b5ee6dad 100644
--- a/Assets/AI/Demon/Turet.prefab
+++ b/Assets/AI/Demon/Turet.prefab
@@ -127,16 +127,16 @@ MonoBehaviour:
muzzle: {fileID: 8411050209285398990}
fireballPrefab: {fileID: 1947871717301538, guid: 9591667a35466484096c6e63785e136c,
type: 3}
- fireballSpeed: 28
- fireRate: 0.7
- maxShots: 10
- despawnDelay: 3
+ fireRate: 5
+ maxShots: 3
+ despawnDelay: 10
turnSpeed: 120
idleSpinSpeed: 30
aimTolerance: 5
autoFindPlayer: 1
playerTag: Player
maxShootingRange: 50
+ useShootEffects: 0
muzzleFlashPrefab: {fileID: 0}
shootSound: {fileID: 8300000, guid: d44a96f293b66b74ea13a2e6fcc6c8fa, type: 3}
enableDebug: 0