using Invector; using Invector.vCharacterController; using Lean.Pool; using UnityEngine; namespace ArcherEnemy { /// /// Arrow projectile shot by archer enemies /// Flies in straight line with gravity and deals damage on hit /// public class ArcherProjectile : MonoBehaviour { #region Configuration [Header("Movement")] [Tooltip("Initial velocity of the arrow (m/s)")] public float initialSpeed = 30f; [Tooltip("Gravity multiplier (higher = more arc)")] public float gravityMultiplier = 1f; [Tooltip("Max lifetime before auto-despawn (seconds)")] public float maxLifetime = 10f; [Header("Damage")] [Tooltip("Damage dealt on hit")] public int damage = 15; [Tooltip("Knockback force")] public float knockbackForce = 5f; [Tooltip("Layers that can be hit")] public LayerMask hitLayers = -1; [Header("Effects")] [Tooltip("Impact VFX prefab")] public GameObject impactVFXPrefab; [Tooltip("Trail renderer (optional)")] public TrailRenderer trail; [Header("Audio")] [Tooltip("Impact sound")] public AudioClip impactSound; [Header("Debug")] [Tooltip("Enable debug logging")] public bool enableDebug = false; [Tooltip("Show trajectory gizmos")] public bool showGizmos = true; #endregion Configuration #region Runtime State private Vector3 velocity; private float lifetime = 0f; private bool hasHit = false; private AudioSource audioSource; #endregion Runtime State #region Unity Lifecycle private void Awake() { if (impactSound != null) { audioSource = GetComponent(); if (audioSource == null) { audioSource = gameObject.AddComponent(); audioSource.playOnAwake = false; audioSource.spatialBlend = 1f; // 3D sound } } } private void OnEnable() { ResetState(); // Set initial velocity in forward direction velocity = transform.forward * initialSpeed; if (enableDebug) Debug.Log($"[ArcherProjectile] Spawned at {transform.position}, velocity: {velocity}"); } private void Update() { if (hasHit) return; lifetime += Time.deltaTime; // Check lifetime if (lifetime >= maxLifetime) { if (enableDebug) Debug.Log("[ArcherProjectile] Lifetime expired"); Despawn(); return; } // Apply gravity velocity += Physics.gravity * gravityMultiplier * Time.deltaTime; // Calculate movement step Vector3 moveStep = velocity * Time.deltaTime; Vector3 newPosition = transform.position + moveStep; // Raycast for collision detection if (Physics.Raycast(transform.position, moveStep.normalized, out RaycastHit hit, moveStep.magnitude, hitLayers, QueryTriggerInteraction.Ignore)) { OnHit(hit); return; } // Move arrow transform.position = newPosition; // Rotate arrow to face movement direction if (velocity.sqrMagnitude > 0.001f) { transform.rotation = Quaternion.LookRotation(velocity.normalized); } } #endregion Unity Lifecycle #region Collision & Damage private void OnHit(RaycastHit hit) { if (hasHit) return; hasHit = true; if (enableDebug) Debug.Log($"[ArcherProjectile] Hit: {hit.collider.name} at {hit.point}"); // Position arrow at impact point transform.position = hit.point; transform.rotation = Quaternion.LookRotation(hit.normal); // Try to deal damage DealDamage(hit.collider, hit.point, hit.normal); // Spawn impact VFX SpawnImpactVFX(hit.point, hit.normal); // Play impact sound PlayImpactSound(); // Stick arrow to surface or despawn StickToSurface(hit); } private void DealDamage(Collider targetCollider, Vector3 hitPoint, Vector3 hitNormal) { // Calculate hit direction (opposite of normal for knockback) Vector3 hitDirection = -hitNormal; if (velocity.sqrMagnitude > 0.001f) { hitDirection = velocity.normalized; } // Create damage info vDamage damageInfo = new vDamage(damage) { sender = transform, hitPosition = hitPoint }; if (knockbackForce > 0f) { damageInfo.force = hitDirection * knockbackForce; } bool damageDealt = false; // Try vIDamageReceiver var damageReceiver = targetCollider.GetComponent() ?? targetCollider.GetComponentInParent(); if (damageReceiver != null) { damageReceiver.TakeDamage(damageInfo); damageDealt = true; if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vIDamageReceiver"); } // Fallback to vHealthController if (!damageDealt) { var healthController = targetCollider.GetComponent() ?? targetCollider.GetComponentInParent(); if (healthController != null) { healthController.TakeDamage(damageInfo); damageDealt = true; if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vHealthController"); } } // 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; if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via bThirdPersonController"); } else { if (enableDebug) Debug.Log("[ArcherProjectile] Target is immortal - no damage"); } } else { tpc.TakeDamage(damageInfo); damageDealt = true; if (enableDebug) Debug.Log("[ArcherProjectile] Damage dealt via vThirdPersonController"); } } } if (!damageDealt && enableDebug) { Debug.Log("[ArcherProjectile] No damage dealt - no valid receiver found"); } } #endregion Collision & Damage #region Effects private void SpawnImpactVFX(Vector3 position, Vector3 normal) { if (impactVFXPrefab == null) return; Quaternion rotation = Quaternion.LookRotation(normal); GameObject vfx = LeanPool.Spawn(impactVFXPrefab, position, rotation); LeanPool.Despawn(vfx, 3f); if (enableDebug) Debug.Log("[ArcherProjectile] Impact VFX spawned"); } private void PlayImpactSound() { if (audioSource != null && impactSound != null) { audioSource.PlayOneShot(impactSound); } } #endregion Effects #region Arrow Sticking private void StickToSurface(RaycastHit hit) { // Option 1: Parent arrow to hit object (if it has rigidbody, it will move with it) // Option 2: Just despawn after short delay // For now, despawn after brief delay to show impact LeanPool.Despawn(gameObject, 0.1f); } #endregion Arrow Sticking #region Pooling private void ResetState() { hasHit = false; lifetime = 0f; velocity = Vector3.zero; // Reset trail if present if (trail != null) { trail.Clear(); } } private void Despawn() { if (enableDebug) Debug.Log("[ArcherProjectile] Despawning"); LeanPool.Despawn(gameObject); } #endregion Pooling #region Gizmos #if UNITY_EDITOR private void OnDrawGizmos() { if (!showGizmos || !Application.isPlaying) return; // Draw velocity vector Gizmos.color = Color.yellow; Gizmos.DrawRay(transform.position, velocity.normalized * 2f); // Draw forward direction Gizmos.color = Color.blue; Gizmos.DrawRay(transform.position, transform.forward * 1f); } #endif #endregion Gizmos } }