167 lines
5.1 KiB
C#
167 lines
5.1 KiB
C#
using UnityEngine;
|
|
|
|
public class Projectile : MonoBehaviour
|
|
{
|
|
[Header("Flight Settings")]
|
|
[SerializeField] private float speed = 50f;
|
|
[SerializeField] private float lifeTime = 5f;
|
|
[SerializeField] private bool usePhysics = false;
|
|
|
|
[Header("Damage Settings")]
|
|
[SerializeField] private int damage = 50;
|
|
[SerializeField] private bool isExplosive = false;
|
|
[SerializeField] private float explosionRadius = 4f;
|
|
[SerializeField] private LayerMask damageableLayers = ~0;
|
|
|
|
[Header("Visual & Audio Effects")]
|
|
[SerializeField] private GameObject impactEffectPrefab;
|
|
[SerializeField] private AudioClip impactSound;
|
|
|
|
private Rigidbody rb;
|
|
private string ownerTag = "Player";
|
|
private bool hasImpacted = false;
|
|
|
|
private void Awake()
|
|
{
|
|
rb = GetComponent<Rigidbody>();
|
|
if (rb != null)
|
|
{
|
|
rb.useGravity = usePhysics;
|
|
if (usePhysics)
|
|
{
|
|
rb.linearVelocity = transform.forward * speed;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
Destroy(gameObject, lifeTime);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!usePhysics || rb == null)
|
|
{
|
|
// Manual kinematic translation with raycast sweep to prevent tunneling
|
|
float step = speed * Time.deltaTime;
|
|
Ray ray = new Ray(transform.position, transform.forward);
|
|
|
|
if (Physics.Raycast(ray, out RaycastHit hit, step, damageableLayers, QueryTriggerInteraction.Ignore))
|
|
{
|
|
if (hit.collider.CompareTag(ownerTag)) return;
|
|
|
|
transform.position = hit.point;
|
|
OnHit(hit.collider, hit.point, hit.normal);
|
|
return;
|
|
}
|
|
|
|
transform.position += transform.forward * step;
|
|
}
|
|
}
|
|
|
|
public void Initialize(int customDamage, float customSpeed, string shooterTag)
|
|
{
|
|
damage = customDamage;
|
|
speed = customSpeed;
|
|
ownerTag = shooterTag;
|
|
|
|
if (rb != null && usePhysics)
|
|
{
|
|
rb.linearVelocity = transform.forward * speed;
|
|
}
|
|
}
|
|
|
|
private void OnTriggerEnter(Collider other)
|
|
{
|
|
if (hasImpacted || other.CompareTag(ownerTag)) return;
|
|
HandleCollision(other, transform.position, -transform.forward);
|
|
}
|
|
|
|
private void OnCollisionEnter(Collision collision)
|
|
{
|
|
if (hasImpacted || collision.collider.CompareTag(ownerTag)) return;
|
|
|
|
ContactPoint contact = collision.GetContact(0);
|
|
HandleCollision(collision.collider, contact.point, contact.normal);
|
|
}
|
|
|
|
private void OnHit(Collider hitCollider, Vector3 hitPoint, Vector3 hitNormal)
|
|
{
|
|
if (hasImpacted) return;
|
|
HandleCollision(hitCollider, hitPoint, hitNormal);
|
|
}
|
|
|
|
private void HandleCollision(Collider hitCollider, Vector3 hitPoint, Vector3 hitNormal)
|
|
{
|
|
hasImpacted = true;
|
|
|
|
if (isExplosive)
|
|
{
|
|
Explode(hitPoint);
|
|
}
|
|
else
|
|
{
|
|
// Direct hit damage
|
|
if (hitCollider.TryGetComponent<EnemyController>(out var enemy))
|
|
{
|
|
enemy.TakeDamage(damage);
|
|
}
|
|
else if (hitCollider.TryGetComponent<PlayerHealthController>(out var playerHealth))
|
|
{
|
|
playerHealth.TakeDamage(damage);
|
|
}
|
|
}
|
|
|
|
// Spawn visual impact effect
|
|
if (impactEffectPrefab != null)
|
|
{
|
|
Quaternion rot = hitNormal != Vector3.zero ? Quaternion.LookRotation(hitNormal) : Quaternion.identity;
|
|
GameObject impact = Instantiate(impactEffectPrefab, hitPoint, rot);
|
|
Destroy(impact, 2.5f);
|
|
}
|
|
|
|
// Play impact sound
|
|
if (impactSound != null)
|
|
{
|
|
AudioSource.PlayClipAtPoint(impactSound, hitPoint);
|
|
}
|
|
|
|
Destroy(gameObject);
|
|
}
|
|
|
|
private void Explode(Vector3 center)
|
|
{
|
|
Collider[] hits = Physics.OverlapSphere(center, explosionRadius, damageableLayers);
|
|
foreach (Collider col in hits)
|
|
{
|
|
if (col.CompareTag(ownerTag)) continue;
|
|
|
|
if (col.TryGetComponent<EnemyController>(out var enemy))
|
|
{
|
|
float distance = Vector3.Distance(center, col.transform.position);
|
|
float damageFalloff = Mathf.Clamp01(1f - (distance / explosionRadius));
|
|
int finalDamage = Mathf.RoundToInt(damage * damageFalloff);
|
|
enemy.TakeDamage(Mathf.Max(1, finalDamage));
|
|
}
|
|
else if (col.TryGetComponent<PlayerHealthController>(out var playerHealth))
|
|
{
|
|
float distance = Vector3.Distance(center, col.transform.position);
|
|
float damageFalloff = Mathf.Clamp01(1f - (distance / explosionRadius));
|
|
int finalDamage = Mathf.RoundToInt(damage * damageFalloff);
|
|
playerHealth.TakeDamage(Mathf.Max(1, finalDamage));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
if (isExplosive)
|
|
{
|
|
Gizmos.color = Color.red;
|
|
Gizmos.DrawWireSphere(transform.position, explosionRadius);
|
|
}
|
|
}
|
|
}
|
|
|