using Invector;
using Invector.vCharacterController;
using Lean.Pool;
using UnityEngine;
using UnityEngine.Events;
namespace DemonBoss.Magic
{
public class MeteorProjectile : MonoBehaviour
{
#region Configuration
[Header("Targeting")]
[Tooltip("Tag of the target to pick impact point from on spawn")]
public string targetTag = "Player";
[Tooltip("If true, raycasts down from impact point to find ground (better visuals)")]
public bool snapImpactToGround = true;
[Tooltip("LayerMask used when snapping impact point to ground")]
public LayerMask groundMask = ~0;
[Header("Entry Geometry")]
[Tooltip("If > 0, teleports start Y above impact point by this height (keeps current XZ). 0 = keep original height.")]
public float spawnHeightAboveTarget = 12f;
[Tooltip("Entry angle measured DOWN from horizontal (e.g. 15° = shallow, 45° = steeper)")]
[Range(0f, 89f)]
public float entryAngleDownFromHorizontal = 20f;
[Tooltip("How closely to aim towards the target in XZ before tilting down (0..1). 1 = purely towards target, 0 = purely downward.")]
[Range(0f, 1f)]
public float azimuthAimWeight = 0.85f;
[Header("Movement")]
[Tooltip("Initial speed magnitude in units per second")]
public float speed = 30f;
[Tooltip("Extra downward acceleration to add a slight curve (0 = disabled)")]
public float gravityLikeAcceleration = 0f;
[Tooltip("Rotate the meteor mesh to face velocity")]
public bool rotateToVelocity = true;
[Tooltip("Seconds before the meteor auto-despawns")]
public float maxLifeTime = 12f;
[Header("Impact / AOE")]
[Tooltip("Explosion radius at impact (0 = no AOE, only single target)")]
public float explosionRadius = 0f;
[Tooltip("Layers that should receive AOE damage")]
public LayerMask explosionMask = ~0;
[Tooltip("If true, AOE will only damage colliders with the same tag as targetTag")]
public bool aoeOnlyTargetTag = false;
[Tooltip("Sphere radius for continuous collision checks")]
public float castRadius = 0.25f;
[Tooltip("Layers that block the meteor during flight (ground/walls/etc.)")]
public LayerMask collisionMask = ~0;
[Tooltip("Damage that will be dealt to Player")]
public float damage = 40.0f;
[Tooltip("Knockback that will be applied to Player")]
public float knockbackForce = 5.0f;
[Header("Debug")]
[Tooltip("Enable verbose logs and debug rays")]
public bool enableDebug = false;
[Header("Events")]
[Tooltip("Invoked once on any impact (use for VFX/SFX/CameraShake)")]
public UnityEvent onImpact;
#endregion Configuration
#region Runtime
private Vector3 impactPoint;
private Vector3 velocityDir;
private Vector3 velocity;
private float timer = 0f;
private bool hasDealtDamage = false;
private bool hasImpacted = false;
private Vector3 prevPos;
#endregion Runtime
#region Unity
///
/// Resets runtime state, locks the impact point, computes entry direction and initial velocity.
///
private void OnEnable()
{
timer = 0f;
hasDealtDamage = false;
hasImpacted = false;
// Acquire player and lock impact point
Vector3 targetPos = GetPlayerPosition();
impactPoint = snapImpactToGround ? SnapPointToGround(targetPos) : targetPos;
// Optionally start from above to guarantee top-down feel
if (spawnHeightAboveTarget > 0f)
{
Vector3 p = transform.position;
p.y = impactPoint.y + spawnHeightAboveTarget;
transform.position = p;
}
Vector3 toImpactXZ = new Vector3(impactPoint.x, transform.position.y, impactPoint.z) - transform.position;
Vector3 azimuthDir = toImpactXZ.sqrMagnitude > 0.0001f ? toImpactXZ.normalized : transform.forward;
Vector3 tiltAxis = Vector3.Cross(Vector3.up, azimuthDir);
if (tiltAxis.sqrMagnitude < 0.0001f) tiltAxis = Vector3.right;
Quaternion downTilt = Quaternion.AngleAxis(-entryAngleDownFromHorizontal, tiltAxis);
Vector3 tiltedDir = (downTilt * azimuthDir).normalized;
velocityDir = Vector3.Slerp(Vector3.down, tiltedDir, Mathf.Clamp01(azimuthAimWeight)).normalized;
velocity = velocityDir * Mathf.Max(0f, speed);
if (rotateToVelocity && velocity.sqrMagnitude > 0.0001f)
transform.rotation = Quaternion.LookRotation(velocity.normalized);
if (enableDebug)
{
Debug.Log($"[Meteor] Impact={impactPoint}, start={transform.position}, dir={velocityDir}");
Debug.DrawLine(transform.position, transform.position + velocityDir * 6f, Color.red, 3f);
Debug.DrawLine(transform.position, impactPoint, Color.yellow, 2f);
}
prevPos = transform.position;
}
///
/// Integrates motion with optional downward acceleration, performs a SphereCast for continuous collision,
/// rotates to face velocity, and handles lifetime expiry.
///
private void Update()
{
timer += Time.deltaTime;
// Downward "gravity-like" acceleration
if (gravityLikeAcceleration > 0f)
velocity += Vector3.down * gravityLikeAcceleration * Time.deltaTime;
Vector3 nextPos = transform.position + velocity * Time.deltaTime;
// Continuous collision: SphereCast from current position toward next
Vector3 castDir = nextPos - transform.position;
float castDist = castDir.magnitude;
if (castDist > 0.0001f)
{
if (Physics.SphereCast(transform.position, castRadius, castDir.normalized,
out RaycastHit hit, castDist, collisionMask, QueryTriggerInteraction.Ignore))
{
transform.position = hit.point;
Impact(hit.collider, hit.point, hit.normal);
return;
}
}
// No hit — apply movement
transform.position = nextPos;
// Face velocity if requested
if (rotateToVelocity && velocity.sqrMagnitude > 0.0001f)
transform.rotation = Quaternion.LookRotation(velocity.normalized);
prevPos = transform.position;
// Auto-despawn on lifetime cap
if (timer >= maxLifeTime)
{
if (enableDebug) Debug.Log("[Meteor] Max lifetime reached → Despawn");
Despawn();
}
}
///
/// Trigger-based contact: funnels into unified Impact().
///
private void OnTriggerEnter(Collider other)
{
// Even if not the intended target, a meteor typically impacts anything it touches
Impact(other, transform.position, -SafeNormal(velocity));
}
///
/// Collision-based contact: funnels into unified Impact().
///
private void OnCollisionEnter(Collision collision)
{
var cp = collision.contacts.Length > 0 ? collision.contacts[0] : default;
Impact(collision.collider, cp.point != default ? cp.point : transform.position, cp.normal != default ? cp.normal : Vector3.up);
}
#endregion Unity
#region Helpers: Target / Ground
///
/// Gets the target's current position (by tag). If not found, projects forward from current position.
///
private Vector3 GetPlayerPosition()
{
GameObject playerObj = GameObject.FindGameObjectWithTag(targetTag);
if (playerObj != null)
return playerObj.transform.position;
if (enableDebug) Debug.LogWarning($"[Meteor] No target with tag '{targetTag}' found, using forward guess.");
return transform.position + transform.forward * 10f;
}
///
/// Raycasts down from above the source point to find ground; returns original point if no hit.
///
private Vector3 SnapPointToGround(Vector3 source)
{
Vector3 start = source + Vector3.up * 50f;
if (Physics.Raycast(start, Vector3.down, out RaycastHit hit, 200f, groundMask, QueryTriggerInteraction.Ignore))
return hit.point;
return source;
}
///
/// Returns a safe normalized version of a vector (falls back to Vector3.down if tiny).
///
private static Vector3 SafeNormal(Vector3 v)
{
return v.sqrMagnitude > 0.0001f ? v.normalized : Vector3.down;
}
#endregion Helpers: Target / Ground
#region Impact & Damage
///
/// Unified impact handler (idempotent). Deals single-target damage if applicable,
/// optional AOE damage, invokes events, and despawns.
///
private void Impact(Collider hitCol, Vector3 hitPoint, Vector3 hitNormal)
{
if (hasImpacted) return;
hasImpacted = true;
if (enableDebug)
Debug.Log($"[Meteor] Impact with {(hitCol ? hitCol.name : "null")} at {hitPoint}");
// Single target (original logic)
if (!hasDealtDamage && hitCol != null && hitCol.CompareTag(targetTag))
DealDamageToTarget(hitCol);
// AOE (if enabled)
if (explosionRadius > 0f)
DealAreaDamage(hitPoint);
// Hook for VFX/SFX/CameraShake
onImpact?.Invoke();
Despawn();
}
///
/// Deals damage to the intended single target using Invector interfaces/components.
///
private void DealDamageToTarget(Collider targetCollider)
{
if (enableDebug) Debug.Log($"[MeteorDamage] Dealing {damage} damage to: {targetCollider.name}");
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
Vector3 hitDirection = GetHitDirection(targetCollider);
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
{
sender = transform,
hitPosition = hitPoint
};
if (knockbackForce > 0f)
damageInfo.force = hitDirection * knockbackForce;
bool damageDealt = false;
// vIDamageReceiver
var receiver = targetCollider.GetComponent() ??
targetCollider.GetComponentInParent();
if (receiver != null)
{
receiver.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[MeteorDamage] Damage via vIDamageReceiver");
}
// vHealthController
if (!damageDealt)
{
var health = targetCollider.GetComponent() ??
targetCollider.GetComponentInParent();
if (health != null)
{
health.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[MeteorDamage] Damage via vHealthController");
}
}
// vThirdPersonController (including 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("[MeteorDamage] Damage via bThirdPersonController");
}
else if (enableDebug)
{
Debug.Log("[MeteorDamage] Target is immortal/GodMode – no damage");
}
}
else
{
tpc.TakeDamage(damageInfo);
damageDealt = true;
hasDealtDamage = true;
if (enableDebug) Debug.Log("[MeteorDamage] Damage via vThirdPersonController");
}
}
}
if (!damageDealt && enableDebug)
Debug.LogWarning("[MeteorDamage] No valid damage receiver found!");
}
///
/// Deals AOE damage to all colliders within explosionRadius using Invector-compatible logic.
///
private void DealAreaDamage(Vector3 center)
{
Collider[] hits = Physics.OverlapSphere(center, explosionRadius, explosionMask, QueryTriggerInteraction.Ignore);
foreach (var col in hits)
{
if (col == null) continue;
if (aoeOnlyTargetTag && !col.CompareTag(targetTag)) continue;
// Avoid double-hitting the same single target if already processed
if (col.CompareTag(targetTag) && hasDealtDamage) continue;
Vector3 hp = col.ClosestPoint(center);
Vector3 dir = (col.bounds.center - center).normalized;
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage))
{
sender = transform,
hitPosition = hp,
force = knockbackForce > 0f ? dir * knockbackForce : Vector3.zero
};
bool dealt = false;
var receiver = col.GetComponent() ?? col.GetComponentInParent();
if (receiver != null) { receiver.TakeDamage(damageInfo); dealt = true; }
if (!dealt)
{
var health = col.GetComponent() ?? col.GetComponentInParent();
if (health != null) { health.TakeDamage(damageInfo); dealt = true; }
}
if (!dealt)
{
var tpc = col.GetComponent() ?? col.GetComponentInParent();
if (tpc != null)
{
if (tpc is Beyond.bThirdPersonController beyond)
{
if (!beyond.GodMode && !beyond.isImmortal) { tpc.TakeDamage(damageInfo); dealt = true; }
}
else { tpc.TakeDamage(damageInfo); dealt = true; }
}
}
}
// Consider AOE as the final damage application for this projectile
hasDealtDamage = true;
}
///
/// Gets the closest point on a collider to this projectile's position.
///
private Vector3 GetClosestPointOnCollider(Collider col)
{
return col.ClosestPoint(transform.position);
}
///
/// Computes a hit direction from projectile toward a collider's center; falls back to current velocity.
///
private Vector3 GetHitDirection(Collider col)
{
Vector3 dir = (col.bounds.center - transform.position).normalized;
return dir.sqrMagnitude > 0.0001f ? dir : SafeNormal(velocity);
}
#endregion Impact & Damage
#region Pooling
///
/// Returns this projectile to the LeanPool.
///
private void Despawn()
{
if (enableDebug) Debug.Log("[Meteor] Despawn via LeanPool");
LeanPool.Despawn(gameObject);
}
#endregion Pooling
#if UNITY_EDITOR
///
/// Editor-only gizmos to visualize entry direction and impact point when selected.
///
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, 0.25f);
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, transform.position + SafeNormal(velocity.sqrMagnitude > 0 ? velocity : velocityDir) * 3f);
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(impactPoint, 0.35f);
}
#endif
#region Validation
///
/// Clamps and validates configuration values in the inspector.
///
private void OnValidate()
{
speed = Mathf.Max(0f, speed);
maxLifeTime = Mathf.Max(0.01f, maxLifeTime);
explosionRadius = Mathf.Max(0f, explosionRadius);
castRadius = Mathf.Clamp(castRadius, 0.01f, 2f);
entryAngleDownFromHorizontal = Mathf.Clamp(entryAngleDownFromHorizontal, 0f, 89f);
azimuthAimWeight = Mathf.Clamp01(azimuthAimWeight);
}
#endregion Validation
}
}