327 lines
7.4 KiB
C#
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
|
|
}
|
|
} |