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