473 lines
18 KiB
C#
473 lines
18 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace BulletHellTemplate
|
|
{
|
|
/// <summary>
|
|
/// Handles the damage entity that applies damage to targets upon collision.
|
|
/// </summary>
|
|
[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<Collider> validTargets = new List<Collider>();
|
|
private Dictionary<Collider, float> targetCooldowns = new Dictionary<Collider, float>();
|
|
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<Rigidbody>();
|
|
rb.useGravity = false;
|
|
// Ensure there's a Collider, add SphereCollider if none exists
|
|
if (GetComponent<Collider>() == null)
|
|
{
|
|
gameObject.AddComponent<SphereCollider>().isTrigger = true;
|
|
}
|
|
else
|
|
{
|
|
GetComponent<Collider>().isTrigger = true;
|
|
}
|
|
|
|
|
|
initialPosition = transform.position;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the skill data and damage properties for this damage entity.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the skill perk data and configures damage properties for this damage entity.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates cooldowns and the remaining lifetime of the object.
|
|
/// </summary>
|
|
private void UpdateCooldownsAndLifetime()
|
|
{
|
|
List<Collider> cooldownKeys = new List<Collider>(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the boomerang mechanic, returning the projectile to the attacker when it reaches max distance.
|
|
/// </summary>
|
|
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<BoxEntity>();
|
|
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<MonsterEntity>());
|
|
}
|
|
else if (other.CompareTag("Box"))
|
|
{
|
|
BoxEntity box = other.GetComponent<BoxEntity>();
|
|
if (box != null)
|
|
{
|
|
box.ReceiveDamage((int)baseDamageSkill);
|
|
targetCooldowns[other] = 0.3f;
|
|
}
|
|
}
|
|
|
|
validTargets.Remove(other);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies calculated damage to the specified monster target.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Starts the size change coroutine based on the given DamageEntitySizeChange configuration.
|
|
/// </summary>
|
|
public void StartSizeChange(DamageEntitySizeChange sizeChangeConfig)
|
|
{
|
|
if (sizeChangeConfig != null)
|
|
{
|
|
StartCoroutine(ChangeSizeOverTime(sizeChangeConfig));
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Coroutine to gradually change the size of the DamageEntity based on DamageEntitySizeChange parameters,
|
|
/// and pauses when the game is paused.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|