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; } } } }