386 lines
11 KiB
C#
386 lines
11 KiB
C#
using Invector;
|
|
using Invector.vCharacterController;
|
|
using Lean.Pool;
|
|
using UnityEngine;
|
|
|
|
namespace DemonBoss.Magic
|
|
{
|
|
/// <summary>
|
|
/// Component handling damage and collisions for fireball fired by crystals
|
|
/// Deals damage on collision with player and automatically despawns
|
|
/// </summary>
|
|
[RequireComponent(typeof(Collider))]
|
|
public class FireballDamage : MonoBehaviour
|
|
{
|
|
[Header("Damage Configuration")]
|
|
[Tooltip("Damage dealt by fireball")]
|
|
public float damage = 25f;
|
|
|
|
[Tooltip("Whether fireball can deal damage only once")]
|
|
public bool damageOnce = true;
|
|
|
|
[Tooltip("Layer mask for targets that can be hit")]
|
|
public LayerMask targetLayerMask = -1;
|
|
|
|
[Header("Impact Effects")]
|
|
[Tooltip("Explosion effect prefab on hit")]
|
|
public GameObject impactEffectPrefab;
|
|
|
|
[Tooltip("Impact sound")]
|
|
public AudioClip impactSound;
|
|
|
|
[Tooltip("Knockback force on hit")]
|
|
public float knockbackForce = 5f;
|
|
|
|
[Header("Lifetime")]
|
|
[Tooltip("Maximum fireball lifetime in seconds (failsafe)")]
|
|
public float maxLifetime = 5f;
|
|
|
|
[Tooltip("Whether to destroy fireball on terrain collision")]
|
|
public bool destroyOnTerrain = true;
|
|
|
|
[Tooltip("Layer mask for terrain/obstacles")]
|
|
public LayerMask terrainLayerMask = 1;
|
|
|
|
[Header("Debug")]
|
|
[Tooltip("Enable debug logging")]
|
|
public bool enableDebug = false;
|
|
|
|
// Private variables
|
|
private bool hasDealtDamage = false;
|
|
|
|
private float spawnTime;
|
|
private AudioSource audioSource;
|
|
private Collider fireballCollider;
|
|
private Rigidbody fireballRigidbody;
|
|
|
|
/// <summary>
|
|
/// Component initialization
|
|
/// </summary>
|
|
private void Awake()
|
|
{
|
|
fireballCollider = GetComponent<Collider>();
|
|
fireballRigidbody = GetComponent<Rigidbody>();
|
|
|
|
if (fireballCollider != null && !fireballCollider.isTrigger)
|
|
{
|
|
fireballCollider.isTrigger = true;
|
|
if (enableDebug) Debug.Log("[FireballDamage] Set collider as trigger");
|
|
}
|
|
|
|
audioSource = GetComponent<AudioSource>();
|
|
if (audioSource == null && impactSound != null)
|
|
{
|
|
audioSource = gameObject.AddComponent<AudioSource>();
|
|
audioSource.playOnAwake = false;
|
|
audioSource.spatialBlend = 1f;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start - note spawn time and start failsafe timer
|
|
/// </summary>
|
|
private void Start()
|
|
{
|
|
spawnTime = Time.time;
|
|
hasDealtDamage = false;
|
|
|
|
Invoke(nameof(FailsafeDespawn), maxLifetime);
|
|
|
|
if (enableDebug) Debug.Log("[FireballDamage] Fireball initialized");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trigger collision handling - dealing damage
|
|
/// </summary>
|
|
/// <param name="other">Collider that fireball collides with</param>
|
|
private void OnTriggerEnter(Collider other)
|
|
{
|
|
if (enableDebug) Debug.Log($"[FireballDamage] Collision with: {other.name}");
|
|
|
|
if (IsValidTarget(other))
|
|
{
|
|
DealDamageToTarget(other);
|
|
CreateImpactEffect(other.transform.position);
|
|
DespawnFireball();
|
|
return;
|
|
}
|
|
|
|
if (destroyOnTerrain && IsTerrainCollision(other))
|
|
{
|
|
if (enableDebug) Debug.Log("[FireballDamage] Terrain collision");
|
|
CreateImpactEffect(transform.position);
|
|
DespawnFireball();
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if collider is valid target
|
|
/// </summary>
|
|
/// <param name="other">Collider to check</param>
|
|
/// <returns>True if it's valid target</returns>
|
|
private bool IsValidTarget(Collider other)
|
|
{
|
|
if ((targetLayerMask.value & (1 << other.gameObject.layer)) == 0)
|
|
return false;
|
|
|
|
if (damageOnce && hasDealtDamage)
|
|
return false;
|
|
|
|
var damageReceiver = other.GetComponent<vIDamageReceiver>();
|
|
var healthController = other.GetComponent<vHealthController>();
|
|
var character = other.GetComponent<vCharacter>();
|
|
var thirdPersonController = other.GetComponent<vThirdPersonController>();
|
|
|
|
if (damageReceiver == null) damageReceiver = other.GetComponentInParent<vIDamageReceiver>();
|
|
if (healthController == null) healthController = other.GetComponentInParent<vHealthController>();
|
|
if (character == null) character = other.GetComponentInParent<vCharacter>();
|
|
if (thirdPersonController == null) thirdPersonController = other.GetComponentInParent<vThirdPersonController>();
|
|
|
|
return damageReceiver != null || healthController != null || character != null || thirdPersonController != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if it's terrain collision
|
|
/// </summary>
|
|
/// <param name="other">Collider to check</param>
|
|
/// <returns>True if it's terrain</returns>
|
|
private bool IsTerrainCollision(Collider other)
|
|
{
|
|
return (terrainLayerMask.value & (1 << other.gameObject.layer)) != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deals damage to target - ulepszona wersja
|
|
/// </summary>
|
|
/// <param name="targetCollider">Target collider</param>
|
|
private void DealDamageToTarget(Collider targetCollider)
|
|
{
|
|
if (enableDebug) Debug.Log($"[FireballDamage] Dealing {damage} damage to: {targetCollider.name}");
|
|
|
|
Vector3 hitPoint = GetClosestPointOnCollider(targetCollider);
|
|
Vector3 hitDirection = GetHitDirection(targetCollider);
|
|
|
|
vDamage damageInfo = new vDamage(Mathf.RoundToInt(damage));
|
|
damageInfo.sender = transform;
|
|
damageInfo.hitPosition = hitPoint;
|
|
|
|
if (knockbackForce > 0f)
|
|
{
|
|
damageInfo.force = hitDirection * knockbackForce;
|
|
}
|
|
|
|
bool damageDealt = false;
|
|
|
|
var damageReceiver = targetCollider.GetComponent<vIDamageReceiver>();
|
|
if (damageReceiver == null) damageReceiver = targetCollider.GetComponentInParent<vIDamageReceiver>();
|
|
|
|
if (damageReceiver != null && !damageDealt)
|
|
{
|
|
damageReceiver.TakeDamage(damageInfo);
|
|
damageDealt = true;
|
|
hasDealtDamage = true;
|
|
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vIDamageReceiver");
|
|
}
|
|
|
|
if (!damageDealt)
|
|
{
|
|
var healthController = targetCollider.GetComponent<vHealthController>();
|
|
if (healthController == null) healthController = targetCollider.GetComponentInParent<vHealthController>();
|
|
|
|
if (healthController != null)
|
|
{
|
|
healthController.TakeDamage(damageInfo);
|
|
damageDealt = true;
|
|
hasDealtDamage = true;
|
|
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vHealthController");
|
|
}
|
|
}
|
|
|
|
if (!damageDealt)
|
|
{
|
|
var thirdPersonController = targetCollider.GetComponent<vThirdPersonController>();
|
|
if (thirdPersonController == null) thirdPersonController = targetCollider.GetComponentInParent<vThirdPersonController>();
|
|
|
|
if (thirdPersonController != null)
|
|
{
|
|
if (thirdPersonController is Beyond.bThirdPersonController beyondController)
|
|
{
|
|
if (!beyondController.GodMode && !beyondController.isImmortal)
|
|
{
|
|
thirdPersonController.TakeDamage(damageInfo);
|
|
damageDealt = true;
|
|
hasDealtDamage = true;
|
|
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through bThirdPersonController");
|
|
}
|
|
else
|
|
{
|
|
if (enableDebug) Debug.Log("[FireballDamage] Player is immortal or in God Mode - no damage dealt");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
thirdPersonController.TakeDamage(damageInfo);
|
|
damageDealt = true;
|
|
hasDealtDamage = true;
|
|
if (enableDebug) Debug.Log("[FireballDamage] Damage dealt through vThirdPersonController");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!damageDealt)
|
|
{
|
|
if (enableDebug) Debug.LogWarning("[FireballDamage] Could not deal damage - no valid damage receiver found!");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes the closest point on the collider as the point of impact
|
|
/// </summary>
|
|
private Vector3 GetClosestPointOnCollider(Collider targetCollider)
|
|
{
|
|
return targetCollider.ClosestPoint(transform.position);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the direction of impact for knockback
|
|
/// </summary>
|
|
private Vector3 GetHitDirection(Collider targetCollider)
|
|
{
|
|
Vector3 direction;
|
|
|
|
if (fireballRigidbody != null && fireballRigidbody.linearVelocity.magnitude > 0.1f)
|
|
{
|
|
direction = fireballRigidbody.linearVelocity.normalized;
|
|
}
|
|
else
|
|
{
|
|
direction = (targetCollider.transform.position - transform.position).normalized;
|
|
}
|
|
|
|
direction.y = Mathf.Max(0.2f, direction.y);
|
|
return direction.normalized;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates impact effect
|
|
/// </summary>
|
|
/// <param name="impactPosition">Impact position</param>
|
|
private void CreateImpactEffect(Vector3 impactPosition)
|
|
{
|
|
if (impactEffectPrefab != null)
|
|
{
|
|
GameObject impact = LeanPool.Spawn(impactEffectPrefab, impactPosition, Quaternion.identity);
|
|
|
|
LeanPool.Despawn(impact, 3f);
|
|
|
|
if (enableDebug) Debug.Log("[FireballDamage] Impact effect created");
|
|
}
|
|
|
|
if (audioSource != null && impactSound != null)
|
|
{
|
|
audioSource.PlayOneShot(impactSound);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Despawns fireball from map
|
|
/// </summary>
|
|
private void DespawnFireball()
|
|
{
|
|
if (enableDebug) Debug.Log("[FireballDamage] Despawning fireball");
|
|
|
|
CancelInvoke(nameof(FailsafeDespawn));
|
|
|
|
LeanPool.Despawn(gameObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Failsafe despawn - removes fireball after maximum lifetime
|
|
/// </summary>
|
|
private void FailsafeDespawn()
|
|
{
|
|
if (enableDebug) Debug.Log("[FireballDamage] Failsafe despawn - lifetime exceeded");
|
|
DespawnFireball();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Forces immediate despawn
|
|
/// </summary>
|
|
public void ForceDespawn()
|
|
{
|
|
if (enableDebug) Debug.Log("[FireballDamage] Forced despawn");
|
|
DespawnFireball();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if fireball has already dealt damage
|
|
/// </summary>
|
|
/// <returns>True if damage was already dealt</returns>
|
|
public bool HasDealtDamage()
|
|
{
|
|
return hasDealtDamage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns fireball lifetime
|
|
/// </summary>
|
|
/// <returns>Time in seconds since spawn</returns>
|
|
public float GetLifetime()
|
|
{
|
|
return Time.time - spawnTime;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets new damage (e.g. for different difficulty levels)
|
|
/// </summary>
|
|
/// <param name="newDamage">New damage</param>
|
|
public void SetDamage(float newDamage)
|
|
{
|
|
damage = newDamage;
|
|
if (enableDebug) Debug.Log($"[FireballDamage] Set new damage: {damage}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets new knockback
|
|
/// </summary>
|
|
/// <param name="newKnockback">New knockback force</param>
|
|
public void SetKnockback(float newKnockback)
|
|
{
|
|
knockbackForce = newKnockback;
|
|
if (enableDebug) Debug.Log($"[FireballDamage] Set new knockback: {knockbackForce}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets fireball state (useful for pooling)
|
|
/// </summary>
|
|
public void ResetFireball()
|
|
{
|
|
hasDealtDamage = false;
|
|
spawnTime = Time.time;
|
|
|
|
CancelInvoke();
|
|
|
|
Invoke(nameof(FailsafeDespawn), maxLifetime);
|
|
|
|
if (enableDebug) Debug.Log("[FireballDamage] Fireball reset");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called on spawn by Lean Pool
|
|
/// </summary>
|
|
private void OnSpawn()
|
|
{
|
|
ResetFireball();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called on despawn by Lean Pool
|
|
/// </summary>
|
|
private void OnDespawn()
|
|
{
|
|
CancelInvoke();
|
|
}
|
|
}
|
|
} |