using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace BulletHellTemplate
{
///
/// Handles the damage entity that applies damage to targets upon collision.
///
[RequireComponent(typeof(Rigidbody))]
public class DamageEntity : MonoBehaviour
{
private SkillData skillData;
private SkillPerkData skillPerkData;
private float baseDamageSkill;
private float attackerDamageRate;
private bool canCauseCriticalDamage;
private CharacterEntity attackerCharacter;
private List validTargets = new List();
private Dictionary targetCooldowns = new Dictionary();
private Rigidbody rb;
private float lifetime = 0f;
private float remainingLifetime;
private bool isPaused = false;
private Vector3 storedVelocity;
private Vector3 initialPosition;
private bool isReturning = false;
private bool canExplodeOnDestroy = true;
private bool destroyOnFirstHit;
private void Awake()
{
rb = GetComponent();
rb.useGravity = false;
// Ensure there's a Collider, add SphereCollider if none exists
if (GetComponent() == null)
{
gameObject.AddComponent().isTrigger = true;
}
else
{
GetComponent().isTrigger = true;
}
initialPosition = transform.position;
}
///
/// Sets the skill data and damage properties for this damage entity.
///
public void SetSkill(SkillData _skillData, float _baseDamage, float _attackerDamageRate, bool _canCauseCriticalDamage, CharacterEntity _attackerCharacter, float _lifeTime, Vector3 velocity)
{
skillData = _skillData;
baseDamageSkill = _baseDamage;
attackerDamageRate = _attackerDamageRate;
canCauseCriticalDamage = _canCauseCriticalDamage;
attackerCharacter = _attackerCharacter;
lifetime = _lifeTime;
remainingLifetime = lifetime;
// Set velocity only if the skill is not orbital and Rigidbody is not kinematic
if (!skillData.isOrbital && !rb.isKinematic)
{
rb.linearVelocity = velocity;
}
}
///
/// Sets the skill perk data and configures damage properties for this damage entity.
///
public void SetSkillPerkData(SkillPerkData skillPerk, Vector3 velocity, CharacterEntity _attacker)
{
if (skillPerk == null)
{
Debug.LogError("SkillPerkData is null.");
return;
}
if (_attacker == null)
{
Debug.LogError("Attacker CharacterEntity is null.");
return;
}
skillPerkData = skillPerk;
attackerCharacter = _attacker;
baseDamageSkill = skillPerk.GetDamageForLevel(GameplayManager.Singleton.GetSkillLevel(skillPerk));
attackerDamageRate = skillPerk.GetAttackerDamageRateForLevel(GameplayManager.Singleton.GetSkillLevel(skillPerk));
canCauseCriticalDamage = !skillPerk.withoutCooldown;
lifetime = skillPerkData.lifeTime;
remainingLifetime = lifetime;
if (!skillPerk.isOrbital && !rb.isKinematic)
{
rb.linearVelocity = velocity;
}
}
private void Update()
{
if (GameplayManager.Singleton.IsPaused())
{
if (!isPaused && (skillData == null || !skillData.isOrbital))
{
isPaused = true;
storedVelocity = rb.linearVelocity;
rb.isKinematic = true;
}
}
else
{
if (isPaused && (skillData == null || !skillData.isOrbital))
{
isPaused = false;
rb.isKinematic = false;
rb.linearVelocity = storedVelocity;
}
UpdateCooldownsAndLifetime();
HandleBoomerang();
}
}
///
/// Updates cooldowns and the remaining lifetime of the object.
///
private void UpdateCooldownsAndLifetime()
{
List cooldownKeys = new List(targetCooldowns.Keys);
foreach (var key in cooldownKeys)
{
targetCooldowns[key] -= Time.deltaTime;
if (targetCooldowns[key] <= 0)
{
targetCooldowns.Remove(key);
}
}
if (!isPaused)
{
remainingLifetime -= Time.deltaTime;
if (remainingLifetime <= 0)
{
HandleExplosionOnDestroy();
Destroy(gameObject);
}
}
}
///
/// Handles the boomerang mechanic, returning the projectile to the attacker when it reaches max distance.
///
private void HandleBoomerang()
{
bool boomerangActive = (skillData != null && skillData.isBoomerangSkill) || (skillPerkData != null && skillPerkData.isBoomerangSkill);
float maxDistance = 0f;
if (skillData != null && skillData.isBoomerangSkill)
{
maxDistance = skillData.maxDistance;
}
else if (skillPerkData != null && skillPerkData.isBoomerangSkill)
{
maxDistance = skillPerkData.maxDistance;
}
if (boomerangActive)
{
if (!isReturning)
{
float distanceTraveled = Vector3.Distance(initialPosition, transform.position);
if (distanceTraveled >= maxDistance)
{
isReturning = true;
}
}
else
{
Vector3 directionToAttacker = (attackerCharacter.transform.position - transform.position).normalized;
rb.linearVelocity = directionToAttacker * rb.linearVelocity.magnitude;
// Rotate the object to face the attacker
if (directionToAttacker != Vector3.zero)
{
transform.rotation = Quaternion.LookRotation(directionToAttacker);
}
float distanceToAttacker = Vector3.Distance(transform.position, attackerCharacter.transform.position);
if (distanceToAttacker <= 0.3f)
{
Destroy(gameObject);
}
}
}
}
private void OnTriggerEnter(Collider other)
{
if (GameplayManager.Singleton.IsPaused())
return;
if (other.gameObject == attackerCharacter.gameObject)
{
if (isReturning)
{
Destroy(gameObject);
}
return;
}
if (other.CompareTag("Monster"))
{
if (!validTargets.Contains(other))
{
validTargets.Add(other);
}
}
else if (other.CompareTag("Box") && !targetCooldowns.ContainsKey(other))
{
BoxEntity box = other.GetComponent();
if (box != null)
{
box.ReceiveDamage((int)baseDamageSkill);
targetCooldowns[other] = 0.3f;
}
}
else if (other.CompareTag("Wall"))
{
HandleRicochet(other);
}
}
private void HandleRicochet(Collider other)
{
bool ricochetActive = (skillData != null && skillData.isRicochet) || (skillPerkData != null && skillPerkData.isRicochet);
if (ricochetActive)
{
Vector3 incomingVelocity = rb.linearVelocity;
RaycastHit hit;
Vector3 origin = transform.position - incomingVelocity.normalized * 0.1f; // A small offset backward
Vector3 direction = incomingVelocity.normalized;
if (Physics.Raycast(origin, direction, out hit, 1f))
{
Vector3 normal = hit.normal;
Vector3 reflectedVelocity = Vector3.Reflect(incomingVelocity, normal);
rb.linearVelocity = reflectedVelocity;
// Rotate the object to face the new direction
if (reflectedVelocity != Vector3.zero)
{
transform.rotation = Quaternion.LookRotation(reflectedVelocity);
}
}
else
{
// If the Raycast fails, reverse the direction
rb.linearVelocity = -incomingVelocity;
// Rotate the object to face the new direction
if (-incomingVelocity != Vector3.zero)
{
transform.rotation = Quaternion.LookRotation(-incomingVelocity);
}
}
}
}
private void HandleExplosionOnDestroy()
{
bool shouldExplode = canExplodeOnDestroy && (skillData != null && skillData.explodeOnDestroy);
if (shouldExplode)
{
DamageEntity explosionPrefab = skillData.explodeDamageEntity;
SkillLevel explosionSettings = skillData.explodeEntitySettings;
if (explosionPrefab != null && explosionSettings != null)
{
DamageEntity explosionInstance = Instantiate(explosionPrefab, transform.position, Quaternion.identity);
explosionInstance.SetSkill(skillData, explosionSettings.baseDamage, explosionSettings.attackerDamageRate, explosionSettings.canCauseCriticalDamage, attackerCharacter, explosionSettings.lifeTime, Vector3.zero);
explosionInstance.canExplodeOnDestroy = false;
explosionInstance.destroyOnFirstHit = false;
Debug.Log($"Explosion DamageEntity instantiated with lifetime: {explosionSettings.lifeTime}");
}
else
{
Debug.LogWarning("Explosion DamageEntity or settings not assigned.");
}
}
}
private void OnTriggerStay(Collider other)
{
if (GameplayManager.Singleton.IsPaused() || other.CompareTag("Character"))
return;
if (validTargets.Contains(other) && !targetCooldowns.ContainsKey(other))
{
if (other.CompareTag("Monster"))
{
ApplyDamage(other.GetComponent());
}
else if (other.CompareTag("Box"))
{
BoxEntity box = other.GetComponent();
if (box != null)
{
box.ReceiveDamage((int)baseDamageSkill);
targetCooldowns[other] = 0.3f;
}
}
validTargets.Remove(other);
}
}
///
/// Applies calculated damage to the specified monster target.
///
private void ApplyDamage(MonsterEntity target)
{
// First, check if the target is already dead
if (target.GetCurrentHP() <= 0)
{
return;
}
// Skill effects
if (skillData != null)
{
if (skillData.applySlow)
{
StartCoroutine(target.ApplySlow(skillData.slowPercent, skillData.slowDuration));
}
if (skillData.applyKnockback)
{
StartCoroutine(target.Knockback(skillData.knockbackDistance, skillData.knockbackDuration));
}
if (skillData.applyStun)
{
StartCoroutine(target.Stun(skillData.stunDuration));
}
if (skillData.applyDOT)
{
float DotTotalDamage = GameInstance.Singleton.TotalDamageWithElements(skillData.damageType, target.characterTypeData, skillData.dotAmount);
target.ApplyDOT(skillData, (int)DotTotalDamage, skillData.dotDuration);
}
}
// SkillPerk effects
if (skillPerkData != null)
{
if (skillPerkData.applySlow)
{
StartCoroutine(target.ApplySlow(skillPerkData.slowPercent, skillPerkData.slowDuration));
}
if (skillPerkData.applyKnockback)
{
StartCoroutine(target.Knockback(skillPerkData.knockbackDistance, skillPerkData.knockbackDuration));
}
if (skillPerkData.applyStun)
{
StartCoroutine(target.Stun(skillPerkData.stunDuration));
}
if (skillPerkData.applyDOT)
{
float DotTotalDamage = GameInstance.Singleton.TotalDamageWithElements(skillPerkData.damageType, target.characterTypeData, skillPerkData.dotAmount);
target.ApplyDOT(skillPerkData, (int)DotTotalDamage, skillPerkData.dotDuration);
}
}
// Calculate and apply damage
float criticalMultiplier = 1.0f;
if (canCauseCriticalDamage && Random.value < attackerCharacter.GetCurrentCriticalRate())
{
criticalMultiplier = attackerCharacter.GetCurrentCriticalDamageMultiplier();
}
float totalDamage = baseDamageSkill + (attackerCharacter.GetCurrentDamage() * attackerDamageRate);
totalDamage *= criticalMultiplier;
CharacterTypeData usedDamageType = skillData != null ? skillData.damageType : (skillPerkData != null ? skillPerkData.damageType : null);
if (usedDamageType != null)
{
totalDamage = GameInstance.Singleton.TotalDamageWithElements(usedDamageType, target.characterTypeData, totalDamage);
}
// Apply the calculated damage
target.ReceiveDamage(totalDamage);
// Apply HP leech if enabled
bool shouldApplyLeech = (skillData != null && skillData.isHpLeech) || (skillPerkData != null && skillPerkData.isHpLeech);
if (shouldApplyLeech)
{
attackerCharacter.ApplyHpLeech(totalDamage);
}
// Check for destroyOnFirstHit
destroyOnFirstHit = (skillData != null && skillData.destroyOnFirstHit);
if (destroyOnFirstHit)
{
HandleExplosionOnDestroy();
Destroy(gameObject);
return;
}
}
public void SetSizeChange(DamageEntitySizeChange sizeChangeConfig)
{
StartSizeChange(sizeChangeConfig);
}
///
/// Starts the size change coroutine based on the given DamageEntitySizeChange configuration.
///
public void StartSizeChange(DamageEntitySizeChange sizeChangeConfig)
{
if (sizeChangeConfig != null)
{
StartCoroutine(ChangeSizeOverTime(sizeChangeConfig));
}
}
///
/// Coroutine to gradually change the size of the DamageEntity based on DamageEntitySizeChange parameters,
/// and pauses when the game is paused.
///
private IEnumerator ChangeSizeOverTime(DamageEntitySizeChange sizeChangeConfig)
{
if (!sizeChangeConfig.enableSizeChange)
yield break;
Vector3 initialSize = new Vector3(sizeChangeConfig.initialSizeX, sizeChangeConfig.initialSizeY, sizeChangeConfig.initialSizeZ);
Vector3 finalSize = new Vector3(sizeChangeConfig.finalSizeX, sizeChangeConfig.finalSizeY, sizeChangeConfig.finalSizeZ);
Vector3 sizeChangeTime = new Vector3(sizeChangeConfig.sizeChangeTimeX, sizeChangeConfig.sizeChangeTimeY, sizeChangeConfig.sizeChangeTimeZ);
Vector3 elapsedTime = Vector3.zero;
while (elapsedTime.x < sizeChangeTime.x || elapsedTime.y < sizeChangeTime.y || elapsedTime.z < sizeChangeTime.z)
{
while (GameplayManager.Singleton.IsPaused())
{
yield return null;
}
float deltaTime = Time.deltaTime;
if (elapsedTime.x < sizeChangeTime.x)
elapsedTime.x = Mathf.Min(elapsedTime.x + deltaTime, sizeChangeTime.x);
if (elapsedTime.y < sizeChangeTime.y)
elapsedTime.y = Mathf.Min(elapsedTime.y + deltaTime, sizeChangeTime.y);
if (elapsedTime.z < sizeChangeTime.z)
elapsedTime.z = Mathf.Min(elapsedTime.z + deltaTime, sizeChangeTime.z);
float tX = sizeChangeTime.x > 0 ? elapsedTime.x / sizeChangeTime.x : 1;
float tY = sizeChangeTime.y > 0 ? elapsedTime.y / sizeChangeTime.y : 1;
float tZ = sizeChangeTime.z > 0 ? elapsedTime.z / sizeChangeTime.z : 1;
transform.localScale = new Vector3(
Mathf.Lerp(initialSize.x, finalSize.x, tX),
Mathf.Lerp(initialSize.y, finalSize.y, tY),
Mathf.Lerp(initialSize.z, finalSize.z, tZ)
);
yield return null;
}
}
}
}