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