Files
beyond/Assets/AI/_Archer/ArcherProjectile.cs
2025-11-20 14:34:59 +01:00

327 lines
7.4 KiB
C#

using Invector;
using Invector.vCharacterController;
using Lean.Pool;
using UnityEngine;
namespace ArcherEnemy
{
/// <summary>
/// Arrow projectile shot by archer enemies
/// Flies in straight line with gravity and deals damage on hit
/// </summary>
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<AudioSource>();
if (audioSource == null)
{
audioSource = gameObject.AddComponent<AudioSource>();
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<vIDamageReceiver>() ??
targetCollider.GetComponentInParent<vIDamageReceiver>();
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<vHealthController>() ??
targetCollider.GetComponentInParent<vHealthController>();
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<vThirdPersonController>() ??
targetCollider.GetComponentInParent<vThirdPersonController>();
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
}
}