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