using Invector; using Invector.vCharacterController; using Lean.Pool; using UnityEngine; using UnityEngine.Events; namespace DemonBoss.Magic { /// /// Enhanced meteor projectile with fireball-like tracking mechanics /// Can either track player or fly to a locked impact point /// public class MeteorProjectile : MonoBehaviour { #region Inspector: Targeting [Header("Targeting")] [Tooltip("If true, use 'overrideImpactPoint' instead of tracking player")] public bool useOverrideImpactPoint = false; [Tooltip("Externally provided locked impact point")] public Vector3 overrideImpactPoint; [Tooltip("Tag to find and track if not using override point")] public string targetTag = "Player"; [Tooltip("Height offset for targeting (aim at chest/head)")] public float targetHeightOffset = 1.0f; [Tooltip("If true, raycast to ground from impact point")] public bool snapImpactToGround = true; [Tooltip("Layers considered 'ground'")] public LayerMask groundMask = ~0; [ReadOnlyInInspector] public Vector3 currentTargetPoint; #endregion Inspector: Targeting #region Inspector: Flight [Header("Flight")] [Tooltip("Movement speed in m/s")] public float speed = 25f; [Tooltip("Time to track player before locking to position")] public float trackingTime = 1.5f; [Tooltip("Distance threshold to consider arrived")] public float arriveEpsilon = 0.5f; [Tooltip("Max lifetime in seconds")] public float maxLifetime = 15f; #endregion Inspector: Flight #region Inspector: Collision & Damage [Header("Collision & Damage")] [Tooltip("Collision detection radius")] public float collisionRadius = 0.8f; [Tooltip("Layers that stop the meteor")] public LayerMask stopOnLayers = ~0; [Tooltip("Layers that take damage")] public LayerMask damageLayers = ~0; [Tooltip("Explosion damage radius")] public float explosionRadius = 4f; [Tooltip("Base damage")] public int damage = 35; [Tooltip("Knockback force")] public float knockbackForce = 12f; #endregion Inspector: Collision & Damage #region Inspector: Effects [Header("Effects & Events")] public GameObject impactVfxPrefab; public UnityEvent onSpawn; public UnityEvent onImpact; [Header("Debug")] public bool enableDebug = false; #endregion Inspector: Effects #region Runtime private Transform _player; private Vector3 _lockedTarget; private bool _isLocked = false; private bool _hasImpacted = false; private float _lifetime = 0f; private readonly Collider[] _overlapCache = new Collider[32]; #endregion Runtime #region Unity private void OnEnable() { ResetState(); InitializeTargeting(); onSpawn?.Invoke(); if (enableDebug) Debug.Log($"[MeteorProjectile] Spawned at {transform.position}"); } private void Update() { if (_hasImpacted) return; _lifetime += Time.deltaTime; // Check lifetime limit if (_lifetime >= maxLifetime) { if (enableDebug) Debug.Log("[MeteorProjectile] Lifetime expired"); DoImpact(transform.position); return; } // Handle tracking to lock transition if (!_isLocked && _lifetime >= trackingTime) { LockTarget(); } // Update target position and move UpdateTargetPosition(); MoveTowardsTarget(); CheckCollisions(); } #endregion Unity #region Targeting & Movement private void ResetState() { _hasImpacted = false; _isLocked = false; _lifetime = 0f; _lockedTarget = Vector3.zero; } private void InitializeTargeting() { if (useOverrideImpactPoint) { _lockedTarget = snapImpactToGround ? SnapToGround(overrideImpactPoint) : overrideImpactPoint; _isLocked = true; currentTargetPoint = _lockedTarget; if (enableDebug) Debug.Log($"[MeteorProjectile] Using override target: {_lockedTarget}"); } else { // Find player for tracking var playerGO = GameObject.FindGameObjectWithTag(targetTag); _player = playerGO ? playerGO.transform : null; if (enableDebug) { if (_player) Debug.Log($"[MeteorProjectile] Found player: {_player.name}"); else Debug.LogWarning($"[MeteorProjectile] No player found with tag: {targetTag}"); } } } private void LockTarget() { if (_isLocked) return; if (_player != null) { Vector3 targetPos = _player.position + Vector3.up * targetHeightOffset; _lockedTarget = snapImpactToGround ? SnapToGround(targetPos) : targetPos; } else { // Fallback: lock to current forward direction _lockedTarget = transform.position + transform.forward * 50f; } _isLocked = true; if (enableDebug) Debug.Log($"[MeteorProjectile] Target locked to: {_lockedTarget}"); } private void UpdateTargetPosition() { if (_isLocked) { currentTargetPoint = _lockedTarget; } else if (_player != null) { currentTargetPoint = _player.position + Vector3.up * targetHeightOffset; } else { // No player, keep going forward currentTargetPoint = transform.position + transform.forward * 100f; } } private void MoveTowardsTarget() { Vector3 direction = (currentTargetPoint - transform.position).normalized; Vector3 movement = direction * speed * Time.deltaTime; transform.position += movement; // Face movement direction if (movement.sqrMagnitude > 0.0001f) { transform.rotation = Quaternion.LookRotation(movement.normalized); } // Check if we've arrived (only when locked) if (_isLocked && Vector3.Distance(transform.position, currentTargetPoint) <= arriveEpsilon) { if (enableDebug) Debug.Log("[MeteorProjectile] Arrived at target"); DoImpact(transform.position); } } private void CheckCollisions() { // Use OverlapSphere for collision detection int hitCount = Physics.OverlapSphereNonAlloc(transform.position, collisionRadius, _overlapCache, stopOnLayers, QueryTriggerInteraction.Ignore); if (hitCount > 0) { if (enableDebug) Debug.Log($"[MeteorProjectile] Collision detected with {_overlapCache[0].name}"); DoImpact(transform.position); } } #endregion Targeting & Movement #region Impact & Damage private void DoImpact(Vector3 impactPos) { if (_hasImpacted) return; _hasImpacted = true; if (enableDebug) Debug.Log($"[MeteorProjectile] Impact at {impactPos}"); // Spawn VFX if (impactVfxPrefab != null) { var vfx = LeanPool.Spawn(impactVfxPrefab, impactPos, Quaternion.identity); // Auto-despawn VFX after 5 seconds LeanPool.Despawn(vfx, 5f); } onImpact?.Invoke(); // Deal area damage int damageTargets = Physics.OverlapSphereNonAlloc(impactPos, explosionRadius, _overlapCache, damageLayers, QueryTriggerInteraction.Ignore); for (int i = 0; i < damageTargets; i++) { var col = _overlapCache[i]; if (col != null) { DealDamageToTarget(col, impactPos); } } // Despawn meteor LeanPool.Despawn(gameObject); } private void DealDamageToTarget(Collider targetCollider, Vector3 hitPoint) { Vector3 hitDirection = (targetCollider.bounds.center - hitPoint).normalized; if (hitDirection.sqrMagnitude < 0.0001f) hitDirection = Vector3.up; vDamage damageInfo = new vDamage(damage) { sender = transform, hitPosition = hitPoint }; if (knockbackForce > 0f) damageInfo.force = hitDirection * knockbackForce; bool damageDealt = false; // Try vIDamageReceiver first var receiver = targetCollider.GetComponent() ?? targetCollider.GetComponentInParent(); if (receiver != null && !damageDealt) { receiver.TakeDamage(damageInfo); damageDealt = true; } // Fallback to vHealthController if (!damageDealt) { var hc = targetCollider.GetComponent() ?? targetCollider.GetComponentInParent(); if (hc != null) { hc.TakeDamage(damageInfo); damageDealt = true; } } // Fallback to vThirdPersonController if (!damageDealt) { var tpc = targetCollider.GetComponent() ?? targetCollider.GetComponentInParent(); if (tpc != null) { // Handle Beyond variant if (tpc is Beyond.bThirdPersonController beyond) { if (!beyond.GodMode && !beyond.isImmortal) { tpc.TakeDamage(damageInfo); damageDealt = true; } } else { tpc.TakeDamage(damageInfo); damageDealt = true; } } } // Apply physics force var rb = targetCollider.attachedRigidbody; if (rb != null && knockbackForce > 0f) { rb.AddForce(hitDirection * knockbackForce, ForceMode.Impulse); } if (enableDebug && damageDealt) { Debug.Log($"[MeteorProjectile] Dealt {damage} damage to {targetCollider.name}"); } } #endregion Impact & Damage #region Helpers private Vector3 SnapToGround(Vector3 point) { Vector3 rayStart = point + Vector3.up * 10f; if (Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, 50f, groundMask, QueryTriggerInteraction.Ignore)) { return hit.point; } return point; } #endregion Helpers #if UNITY_EDITOR private void OnDrawGizmosSelected() { // Draw collision sphere Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, collisionRadius); // Draw explosion radius Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, explosionRadius); // Draw target point if (currentTargetPoint != Vector3.zero) { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(currentTargetPoint, 0.5f); Gizmos.DrawLine(transform.position, currentTargetPoint); } } #endif } public sealed class ReadOnlyInInspectorAttribute : PropertyAttribute { } }