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