723 lines
27 KiB
C#
723 lines
27 KiB
C#
|
using System.Collections;
|
||
|
using System.Collections.Generic;
|
||
|
using UnityEngine;
|
||
|
using UnityEngine.AI;
|
||
|
|
||
|
namespace BulletHellTemplate
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Represents a monster in the game with its stats, behaviors, and skills.
|
||
|
/// </summary>
|
||
|
public class MonsterEntity : MonoBehaviour
|
||
|
{
|
||
|
[Header("Base Stats")]
|
||
|
public CharacterStats characterStats;
|
||
|
public CharacterTypeData characterTypeData;
|
||
|
private Transform target;
|
||
|
private float currentHP;
|
||
|
private NavMeshAgent navMeshAgent;
|
||
|
private bool isPaused = false;
|
||
|
private int monsterGoldValue;
|
||
|
private int monsterXPValue; // XP given by this monster when killed
|
||
|
private Animator animator; // Reference to the Animator
|
||
|
[Header("Resource Drop Settings")]
|
||
|
public bool dropGold;
|
||
|
public bool dropExp;
|
||
|
public DropEntity goldDropPrefab;
|
||
|
public DropEntity expDropPrefab;
|
||
|
|
||
|
[Header("Attack Settings")]
|
||
|
public float attackCooldown = 1.5f; // Time between basic attacks
|
||
|
private bool canAttack = true; // Tracks if the monster can attack
|
||
|
public bool isFinalBoss;
|
||
|
[Header("Skill Settings")]
|
||
|
public bool canUseSkill;
|
||
|
public Skill[] skills; // Array of skills the monster can use
|
||
|
|
||
|
[Header("Visual Effects Settings")]
|
||
|
public bool ShowSlowEffect = false;
|
||
|
public bool ShowDOTEffect = false;
|
||
|
public bool ShowStunEffect = false;
|
||
|
|
||
|
public Color SlowEffectColor = new Color(0.5f, 0.5f, 1f); // Light Blue
|
||
|
public Color DOTEffectColor = new Color(0f, 1f, 0f); // Green
|
||
|
public Color StunEffectColor = new Color(1f, 1f, 0f); // Yellow
|
||
|
|
||
|
public float EffectInterval = 0.2f;
|
||
|
private Dictionary<SkillData, Coroutine> activeDOTsFromSkills = new Dictionary<SkillData, Coroutine>(); // DOTs SkillData
|
||
|
private Dictionary<SkillPerkData, Coroutine> activeDOTsFromPerks = new Dictionary<SkillPerkData, Coroutine>(); // DOTs SkillPerkData
|
||
|
void Start()
|
||
|
{
|
||
|
currentHP = characterStats.baseHP;
|
||
|
navMeshAgent = GetComponent<NavMeshAgent>();
|
||
|
navMeshAgent.speed = characterStats.baseMoveSpeed;
|
||
|
// Initialize Animator if needed
|
||
|
animator = GetComponentInChildren<Animator>();
|
||
|
if (animator == null)
|
||
|
{
|
||
|
Debug.LogError("Animator component not found on the character.");
|
||
|
}
|
||
|
// Initialize skills cooldowns
|
||
|
foreach (var skill in skills)
|
||
|
{
|
||
|
skill.currentCooldown = 0f;
|
||
|
}
|
||
|
StartCoroutine(RegenerateHP());
|
||
|
}
|
||
|
|
||
|
void Update()
|
||
|
{
|
||
|
if (GameplayManager.Singleton.IsPaused())
|
||
|
{
|
||
|
if (!isPaused)
|
||
|
{
|
||
|
isPaused = true;
|
||
|
navMeshAgent.isStopped = true;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (isPaused)
|
||
|
{
|
||
|
isPaused = false;
|
||
|
navMeshAgent.isStopped = false;
|
||
|
}
|
||
|
|
||
|
FindNearestTarget();
|
||
|
if (target != null)
|
||
|
{
|
||
|
MoveTowardsTarget();
|
||
|
HandleSkillUsage(); // Check and use skills if available
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
public float GetCurrentHP()
|
||
|
{
|
||
|
return currentHP;
|
||
|
}
|
||
|
|
||
|
public float GetMaxHP()
|
||
|
{
|
||
|
return characterStats.baseHP;
|
||
|
}
|
||
|
/// <summary>
|
||
|
/// Sets the amount of gold this monster will give when killed.
|
||
|
/// </summary>
|
||
|
/// <param name="amount">The amount of gold.</param>
|
||
|
public void SetMonsterGoldValue(int amount)
|
||
|
{
|
||
|
monsterGoldValue = amount;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Sets the amount of XP this monster will give when killed.
|
||
|
/// </summary>
|
||
|
/// <param name="amount">The amount of XP.</param>
|
||
|
public void SetMonsterXPValue(int amount)
|
||
|
{
|
||
|
monsterXPValue = amount;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Finds the nearest target with the "Character" tag.
|
||
|
/// </summary>
|
||
|
private void FindNearestTarget()
|
||
|
{
|
||
|
GameObject[] characters = GameObject.FindGameObjectsWithTag("Character");
|
||
|
float closestDistance = Mathf.Infinity;
|
||
|
|
||
|
foreach (GameObject character in characters)
|
||
|
{
|
||
|
float distance = Vector3.Distance(transform.position, character.transform.position);
|
||
|
if (distance < closestDistance)
|
||
|
{
|
||
|
closestDistance = distance;
|
||
|
target = character.transform;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Moves the monster towards the target.
|
||
|
/// </summary>
|
||
|
private void MoveTowardsTarget()
|
||
|
{
|
||
|
if (target == null)
|
||
|
return;
|
||
|
|
||
|
navMeshAgent.SetDestination(target.position);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Handles the usage of skills by the monster.
|
||
|
/// </summary>
|
||
|
private void HandleSkillUsage()
|
||
|
{
|
||
|
if (canUseSkill && target != null)
|
||
|
{
|
||
|
foreach (var skill in skills)
|
||
|
{
|
||
|
if (skill.currentCooldown <= 0f && IsTargetWithinMinDistance(skill))
|
||
|
{
|
||
|
UseSkill(skill);
|
||
|
skill.currentCooldown = skill.cooldown;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
skill.currentCooldown -= Time.deltaTime;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Checks if the current target is within the minimum distance required for the skill.
|
||
|
/// </summary>
|
||
|
/// <param name="skill">The skill to check against.</param>
|
||
|
/// <returns>True if the target is within the minimum distance, otherwise false.</returns>
|
||
|
private bool IsTargetWithinMinDistance(Skill skill)
|
||
|
{
|
||
|
if (target == null)
|
||
|
return false;
|
||
|
|
||
|
float distanceToTarget = Vector3.Distance(transform.position, target.position);
|
||
|
return distanceToTarget <= skill.minDistance;
|
||
|
}
|
||
|
/// <summary>
|
||
|
/// Uses a specific skill on the current target, triggers the appropriate animation,
|
||
|
/// and delays the skill launch if needed to synchronize with the animation.
|
||
|
/// </summary>
|
||
|
/// <param name="skill">The skill to use.</param>
|
||
|
private void UseSkill(Skill skill)
|
||
|
{
|
||
|
if (target != null && skill.damagePrefab != null)
|
||
|
{
|
||
|
// Trigger the correct animation based on the skill's animationIndex
|
||
|
if (animator != null)
|
||
|
{
|
||
|
string animationTrigger = $"UseSkill{skill.animationIndex}";
|
||
|
animator.SetTrigger(animationTrigger);
|
||
|
}
|
||
|
|
||
|
// Start coroutine to delay the skill launch if delayToLaunch is greater than 0
|
||
|
if (skill.delayToLaunch > 0)
|
||
|
{
|
||
|
StartCoroutine(DelayedSkillLaunch(skill));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
LaunchSkill(skill);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Debug.LogWarning($"Skill {skill.skillName} does not have a damage prefab assigned.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Coroutine to delay the skill launch after the animation has started.
|
||
|
/// </summary>
|
||
|
/// <param name="skill">The skill to be launched after the delay.</param>
|
||
|
private IEnumerator DelayedSkillLaunch(Skill skill)
|
||
|
{
|
||
|
// Wait for the specified delay to match the animation timing
|
||
|
yield return new WaitForSeconds(skill.delayToLaunch);
|
||
|
LaunchSkill(skill);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Launches the skill, handling multiple shots with angle and delay between shots.
|
||
|
/// </summary>
|
||
|
/// <param name="skill">The skill to launch.</param>
|
||
|
private void LaunchSkill(Skill skill)
|
||
|
{
|
||
|
if (target == null)
|
||
|
return;
|
||
|
|
||
|
Vector3 spawnPosition = transform.position + skill.spawnOffset;
|
||
|
|
||
|
// Calculate the direction towards the target
|
||
|
Vector3 baseDirection = (target.position - spawnPosition).normalized;
|
||
|
|
||
|
// Handle multi-shot logic
|
||
|
int totalShots = skill.shots > 0 ? skill.shots : 1; // Default to 1 shot if not specified
|
||
|
float angleStep = (skill.angle > 0 && totalShots > 1) ? skill.angle / (totalShots - 1) : 0;
|
||
|
|
||
|
for (int i = 0; i < totalShots; i++)
|
||
|
{
|
||
|
float shotAngle = (skill.angle > 0) ? -skill.angle / 2 + i * angleStep : 0;
|
||
|
Quaternion rotation = Quaternion.AngleAxis(shotAngle, Vector3.up);
|
||
|
Vector3 shotDirection = rotation * baseDirection;
|
||
|
|
||
|
// Instantiate the damage entity
|
||
|
MonsterDamageEntity damageEntity = Instantiate(skill.damagePrefab, spawnPosition, Quaternion.identity);
|
||
|
|
||
|
// Set up the damage entity
|
||
|
damageEntity.SetDamage(skill.damageType, skill.damageAmount, shotDirection * skill.launchSpeed, skill.lifeTime,this);
|
||
|
|
||
|
// Delay between multiple shots if applicable
|
||
|
if (skill.delayBetweenShots > 0 && i < totalShots - 1)
|
||
|
{
|
||
|
StartCoroutine(LaunchNextShotWithDelay(skill, i, spawnPosition, baseDirection, shotAngle, angleStep));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Coroutine to handle the delay between multiple shots in a multi-shot skill.
|
||
|
/// </summary>
|
||
|
/// <param name="skill">The skill being used.</param>
|
||
|
/// <param name="shotIndex">The index of the current shot.</param>
|
||
|
/// <param name="spawnPosition">Position where the shot is spawned.</param>
|
||
|
/// <param name="baseDirection">The base direction towards the target.</param>
|
||
|
/// <param name="currentAngle">The angle for the current shot.</param>
|
||
|
/// <param name="angleStep">The step for the angle between shots.</param>
|
||
|
private IEnumerator LaunchNextShotWithDelay(Skill skill, int shotIndex, Vector3 spawnPosition, Vector3 baseDirection, float currentAngle, float angleStep)
|
||
|
{
|
||
|
yield return new WaitForSeconds(skill.delayBetweenShots);
|
||
|
|
||
|
float shotAngle = (skill.angle > 0) ? -skill.angle / 2 + shotIndex * angleStep : 0;
|
||
|
Quaternion rotation = Quaternion.AngleAxis(shotAngle, Vector3.up);
|
||
|
Vector3 shotDirection = rotation * baseDirection;
|
||
|
|
||
|
// Instantiate the damage entity for the next shot
|
||
|
MonsterDamageEntity damageEntity = Instantiate(skill.damagePrefab, spawnPosition, Quaternion.identity);
|
||
|
|
||
|
// Set up the damage entity
|
||
|
damageEntity.SetDamage(skill.damageType, skill.damageAmount, shotDirection * skill.launchSpeed, skill.lifeTime,this);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Receives damage, reducing shield first if available and then HP.
|
||
|
/// The damage is reduced by the current defense value, but cannot be lower than the minimum damage.
|
||
|
/// </summary>
|
||
|
/// <param name="damage">The amount of damage to receive.</param>
|
||
|
public void ReceiveDamage(float damage)
|
||
|
{
|
||
|
if (GameplayManager.Singleton.IsPaused())
|
||
|
return;
|
||
|
|
||
|
// Reduce damage by the defense value, ensuring it is not lower than the minimum damage allowed
|
||
|
damage = Mathf.Max(damage - characterStats.baseDefense, GameplayManager.Singleton.minDamage);
|
||
|
|
||
|
if (characterStats.baseShield > 0)
|
||
|
{
|
||
|
// Apply damage to the shield first
|
||
|
float shieldDamage = Mathf.Min(damage, characterStats.baseShield);
|
||
|
characterStats.baseShield -= shieldDamage;
|
||
|
damage -= shieldDamage;
|
||
|
|
||
|
// Update the UI or visual feedback for the shield
|
||
|
}
|
||
|
|
||
|
if (damage > 0)
|
||
|
{
|
||
|
// Apply remaining damage to HP
|
||
|
currentHP -= damage;
|
||
|
|
||
|
// If HP reaches zero, trigger death
|
||
|
if (currentHP <= 0)
|
||
|
{
|
||
|
Die();
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/// <summary>
|
||
|
/// Applies damage to a target, considering element advantages and weaknesses.
|
||
|
/// </summary>
|
||
|
/// <param name="target">The target to apply damage to.</param>
|
||
|
public void ApplyDamage(CharacterEntity target)
|
||
|
{
|
||
|
if (GameplayManager.Singleton.IsPaused())
|
||
|
return;
|
||
|
|
||
|
if (target == null)
|
||
|
{
|
||
|
Debug.LogWarning("Target is null. Cannot apply damage.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
CharacterTypeData targetType = target.GetCharacterType();
|
||
|
float criticalMultiplier = Random.value < characterStats.baseCriticalRate ? characterStats.baseCriticalDamageMultiplier : 1.0f;
|
||
|
float baseDamage = characterStats.baseDamage * criticalMultiplier;
|
||
|
float totalDamage = GameInstance.Singleton.TotalDamageWithElements(characterTypeData, targetType, baseDamage);
|
||
|
|
||
|
target.ReceiveDamage(totalDamage);
|
||
|
}
|
||
|
|
||
|
private void Die()
|
||
|
{
|
||
|
StopAllCoroutines();
|
||
|
if (dropExp)
|
||
|
{
|
||
|
if (expDropPrefab) Instantiate(expDropPrefab, transform.position, Quaternion.identity).SetValue(monsterXPValue);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
GameplayManager.Singleton.character.AddXP(monsterXPValue);
|
||
|
}
|
||
|
|
||
|
if (dropGold)
|
||
|
{
|
||
|
if (goldDropPrefab) Instantiate(goldDropPrefab, transform.position, Quaternion.identity).SetValue(monsterGoldValue);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
GameplayManager.Singleton.IncrementGainGold(monsterGoldValue);
|
||
|
}
|
||
|
|
||
|
GameplayManager.Singleton.IncrementMonstersKilled();
|
||
|
Destroy(gameObject);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Handles collision with other objects.
|
||
|
/// </summary>
|
||
|
/// <param name="other">The collider of the object the monster collided with.</param>
|
||
|
private void OnTriggerEnter(Collider other)
|
||
|
{
|
||
|
if (GameplayManager.Singleton.IsPaused())
|
||
|
return;
|
||
|
|
||
|
if (other.CompareTag("Character") && canAttack)
|
||
|
{
|
||
|
CharacterEntity character = other.GetComponent<CharacterEntity>();
|
||
|
if (character != null)
|
||
|
{
|
||
|
ApplyDamage(character);
|
||
|
StartCoroutine(AttackCooldown());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Coroutine to handle the attack cooldown.
|
||
|
/// </summary>
|
||
|
/// <returns></returns>
|
||
|
private IEnumerator AttackCooldown()
|
||
|
{
|
||
|
canAttack = false;
|
||
|
yield return new WaitForSeconds(attackCooldown);
|
||
|
canAttack = true;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Applies a slow effect to the monster.
|
||
|
/// </summary>
|
||
|
public IEnumerator ApplySlow(float percent, float duration)
|
||
|
{
|
||
|
if (navMeshAgent == null || !navMeshAgent.isActiveAndEnabled) yield break;
|
||
|
|
||
|
float originalSpeed = navMeshAgent.speed;
|
||
|
navMeshAgent.speed *= (1f - percent);
|
||
|
|
||
|
if (ShowSlowEffect)
|
||
|
StartCoroutine(FlashEffect(SlowEffectColor, duration));
|
||
|
|
||
|
yield return new WaitForSeconds(duration);
|
||
|
|
||
|
if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled)
|
||
|
navMeshAgent.speed = originalSpeed;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Applies a knockback effect to the monster, pushing it back a certain distance over a duration.
|
||
|
/// If a new knockback occurs before the previous one ends, it overrides the current knockback.
|
||
|
/// </summary>
|
||
|
/// <param name="distance">The distance to knock the monster back.</param>
|
||
|
/// <param name="duration">The duration over which the knockback occurs.</param>
|
||
|
public IEnumerator Knockback(float distance, float duration)
|
||
|
{
|
||
|
// Exit if the monster is already dead or the target is null
|
||
|
if (currentHP <= 0 || target == null)
|
||
|
{
|
||
|
yield break;
|
||
|
}
|
||
|
|
||
|
// Stop the NavMeshAgent if it exists to prevent interference with movement
|
||
|
if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled)
|
||
|
{
|
||
|
navMeshAgent.isStopped = true;
|
||
|
}
|
||
|
|
||
|
Vector3 knockbackDirection = (transform.position - target.position).normalized;
|
||
|
Vector3 knockbackStartPosition = transform.position;
|
||
|
Vector3 knockbackEndPosition = knockbackStartPosition + knockbackDirection * distance;
|
||
|
|
||
|
float elapsedTime = 0f;
|
||
|
|
||
|
// Apply the knockback over the specified duration
|
||
|
while (elapsedTime < duration)
|
||
|
{
|
||
|
// Exit the knockback if the monster dies or the object is destroyed during the effect
|
||
|
if (currentHP <= 0 || this == null || target == null)
|
||
|
{
|
||
|
yield break;
|
||
|
}
|
||
|
|
||
|
// Move the monster over time using linear interpolation
|
||
|
transform.position = Vector3.Lerp(knockbackStartPosition, knockbackEndPosition, elapsedTime / duration);
|
||
|
|
||
|
// Update the elapsed time
|
||
|
elapsedTime += Time.deltaTime;
|
||
|
|
||
|
yield return null;
|
||
|
}
|
||
|
|
||
|
// Ensure the monster ends up at the final knockback position
|
||
|
transform.position = knockbackEndPosition;
|
||
|
|
||
|
// Resume the NavMeshAgent if it exists
|
||
|
if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled)
|
||
|
{
|
||
|
navMeshAgent.isStopped = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Stuns the monster, preventing it from moving or acting for the specified duration.
|
||
|
/// If a new stun occurs while the monster is already stunned, it resets the stun duration.
|
||
|
/// </summary>
|
||
|
/// <param name="stunDuration">The duration of the stun in seconds.</param>
|
||
|
public IEnumerator Stun(float stunDuration)
|
||
|
{
|
||
|
if (currentHP <= 0 || navMeshAgent == null)
|
||
|
{
|
||
|
yield break;
|
||
|
}
|
||
|
|
||
|
// Check if navMeshAgent is valid before accessing it
|
||
|
if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled)
|
||
|
{
|
||
|
navMeshAgent.isStopped = true; // Stop movement
|
||
|
}
|
||
|
|
||
|
if (ShowStunEffect)
|
||
|
StartCoroutine(FlashEffect(StunEffectColor, stunDuration));
|
||
|
|
||
|
canAttack = false; // Prevent attacks
|
||
|
|
||
|
yield return new WaitForSeconds(stunDuration);
|
||
|
|
||
|
// Ensure navMeshAgent still exists and is not destroyed before resuming movement
|
||
|
if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled)
|
||
|
{
|
||
|
navMeshAgent.isStopped = false; // Resume movement
|
||
|
}
|
||
|
|
||
|
canAttack = true; // Allow attacks
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Applies a DOT effect to the monster.
|
||
|
/// </summary>
|
||
|
public void ApplyDOT(SkillData skillData, int totalDamage, float duration)
|
||
|
{
|
||
|
if (activeDOTsFromSkills.ContainsKey(skillData))
|
||
|
{
|
||
|
StopCoroutine(activeDOTsFromSkills[skillData]);
|
||
|
activeDOTsFromSkills.Remove(skillData);
|
||
|
}
|
||
|
|
||
|
Coroutine newDOT = StartCoroutine(ApplyDOTCoroutine(skillData, totalDamage, duration));
|
||
|
activeDOTsFromSkills.Add(skillData, newDOT);
|
||
|
|
||
|
if (ShowDOTEffect)
|
||
|
StartCoroutine(FlashEffect(DOTEffectColor, duration));
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Applies a damage-over-time (DOT) effect to the monster, dealing total damage evenly over a specified duration.
|
||
|
/// If a DOT from the same SkillPerkData is already applied, it is removed and the new one is applied.
|
||
|
/// </summary>
|
||
|
/// <param name="skillPerkData">The SkillPerkData that applies the DOT effect.</param>
|
||
|
/// <param name="totalDamage">The total amount of damage to apply over time.</param>
|
||
|
/// <param name="duration">The duration over which to apply the damage.</param>
|
||
|
public void ApplyDOT(SkillPerkData skillPerkData, int totalDamage, float duration)
|
||
|
{
|
||
|
// Remove the current DOT if it's from the same SkillPerkData
|
||
|
if (activeDOTsFromPerks.ContainsKey(skillPerkData))
|
||
|
{
|
||
|
StopCoroutine(activeDOTsFromPerks[skillPerkData]); // Stop the existing DOT coroutine for this perk
|
||
|
activeDOTsFromPerks.Remove(skillPerkData); // Remove it from the active DOTs dictionary
|
||
|
}
|
||
|
|
||
|
// Start the new DOT effect and add it to the active DOTs dictionary
|
||
|
Coroutine newDOT = StartCoroutine(ApplyDOTFromPerkCoroutine(skillPerkData, totalDamage, duration));
|
||
|
activeDOTsFromPerks.Add(skillPerkData, newDOT);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Applies a damage-over-time effect consistently over the specified duration.
|
||
|
/// </summary>
|
||
|
private IEnumerator ApplyDOTCoroutine(SkillData skillData, int totalDamage, float duration)
|
||
|
{
|
||
|
if (currentHP <= 0) yield break;
|
||
|
|
||
|
float tickInterval = 0.2f;
|
||
|
int ticks = Mathf.CeilToInt(duration / tickInterval);
|
||
|
int damagePerTick = totalDamage / ticks;
|
||
|
int remainder = totalDamage - (damagePerTick * (ticks - 1));
|
||
|
|
||
|
float elapsedTime = 0f;
|
||
|
|
||
|
for (int i = 0; i < ticks; i++)
|
||
|
{
|
||
|
if (currentHP <= 0) yield break;
|
||
|
|
||
|
int damageToApply = i == ticks - 1 ? damagePerTick + remainder : damagePerTick;
|
||
|
damageToApply = Mathf.Min(damageToApply, (int)currentHP - 1);
|
||
|
|
||
|
ReceiveDamage(damageToApply);
|
||
|
|
||
|
elapsedTime += tickInterval;
|
||
|
yield return new WaitForSeconds(tickInterval);
|
||
|
}
|
||
|
|
||
|
activeDOTsFromSkills.Remove(skillData);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Coroutine that applies a damage-over-time (DOT) effect to the monster, dealing total damage evenly over a specified duration.
|
||
|
/// This is for DOT effects from SkillPerkData.
|
||
|
/// </summary>
|
||
|
/// <param name="skillPerkData">The SkillPerkData that applies the DOT effect.</param>
|
||
|
/// <param name="totalDamage">The total amount of damage to apply over time.</param>
|
||
|
/// <param name="duration">The duration over which to apply the damage.</param>
|
||
|
private IEnumerator ApplyDOTFromPerkCoroutine(SkillPerkData skillPerkData, int totalDamage, float duration)
|
||
|
{
|
||
|
if (currentHP <= 0)
|
||
|
{
|
||
|
yield break; // Exit if the monster is already dead
|
||
|
}
|
||
|
|
||
|
int ticks = Mathf.CeilToInt(duration); // Total number of ticks based on duration
|
||
|
int damagePerTick = Mathf.Max(1, totalDamage / ticks); // Damage applied per tick, minimum 1 to ensure damage
|
||
|
|
||
|
float tickInterval = 1f; // Apply damage every second
|
||
|
float elapsedTime = 0f;
|
||
|
|
||
|
while (elapsedTime < duration && currentHP > 1)
|
||
|
{
|
||
|
if (currentHP <= 0)
|
||
|
{
|
||
|
yield break; // Exit if the monster dies during the DOT effect
|
||
|
}
|
||
|
|
||
|
int damageToApply = Mathf.Min(damagePerTick, (int)currentHP - 1); // Apply only the damage that leaves at least 1 HP
|
||
|
ReceiveDamage(damageToApply); // Apply the calculated damage
|
||
|
|
||
|
elapsedTime += tickInterval;
|
||
|
yield return new WaitForSeconds(tickInterval); // Wait for the next tick
|
||
|
}
|
||
|
|
||
|
// Remove the DOT from the active list once it completes
|
||
|
activeDOTsFromPerks.Remove(skillPerkData);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Flashes the object in a specific color while an effect is active.
|
||
|
/// </summary>
|
||
|
private IEnumerator FlashEffect(Color effectColor, float duration)
|
||
|
{
|
||
|
float elapsedTime = 0f;
|
||
|
List<Renderer> renderers = new List<Renderer>(GetComponentsInChildren<Renderer>());
|
||
|
|
||
|
while (elapsedTime < duration)
|
||
|
{
|
||
|
foreach (var renderer in renderers)
|
||
|
{
|
||
|
if (renderer != null)
|
||
|
renderer.material.color = effectColor;
|
||
|
}
|
||
|
|
||
|
yield return new WaitForSeconds(EffectInterval);
|
||
|
|
||
|
foreach (var renderer in renderers)
|
||
|
{
|
||
|
if (renderer != null)
|
||
|
renderer.material.color = Color.white;
|
||
|
}
|
||
|
|
||
|
yield return new WaitForSeconds(EffectInterval);
|
||
|
|
||
|
elapsedTime += 2 * EffectInterval;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Regenerates HP based on the base HP regeneration rate.
|
||
|
/// </summary>
|
||
|
private IEnumerator RegenerateHP()
|
||
|
{
|
||
|
while (true)
|
||
|
{
|
||
|
yield return new WaitForSeconds(1f); // Regenerate every second
|
||
|
|
||
|
// Check if the game is paused
|
||
|
if (GameplayManager.Singleton.IsPaused())
|
||
|
{
|
||
|
// If the game is paused, wait until it resumes
|
||
|
while (GameplayManager.Singleton.IsPaused())
|
||
|
{
|
||
|
yield return null; // Wait for the next frame
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Regenerate HP if not paused
|
||
|
if (currentHP < GetMaxHP())
|
||
|
{
|
||
|
currentHP += characterStats.baseHPRegen;
|
||
|
if (currentHP > GetMaxHP())
|
||
|
{
|
||
|
currentHP = GetMaxHP();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/// <summary>
|
||
|
/// Applies health leech based on the damage dealt.
|
||
|
/// </summary>
|
||
|
/// <param name="damage">The total damage dealt.</param>
|
||
|
public void ApplyHpLeech(float damage)
|
||
|
{
|
||
|
float leechAmount = damage * (characterStats.baseHPLeech / 100f);
|
||
|
currentHP += leechAmount;
|
||
|
if (currentHP > GetMaxHP())
|
||
|
{
|
||
|
currentHP = GetMaxHP();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Represents a skill that a monster can use.
|
||
|
/// </summary>
|
||
|
[System.Serializable]
|
||
|
public class Skill
|
||
|
{
|
||
|
public string skillName; // The name of the skill
|
||
|
public CharacterTypeData damageType; // The type of damage the skill deals
|
||
|
public float damageAmount; // The amount of damage the skill deals
|
||
|
public float cooldown; // The cooldown time before the skill can be used again
|
||
|
public int shots;
|
||
|
public float angle;
|
||
|
public float delayBetweenShots;
|
||
|
[HideInInspector] public float currentCooldown; // The current cooldown remaining
|
||
|
public MonsterDamageEntity damagePrefab; // Prefab to use for the skill's damage entity
|
||
|
public Vector3 spawnOffset; // Offset from the monster's position to spawn the damage entity
|
||
|
public float launchSpeed; // Speed at which the damage entity will be launched
|
||
|
public float lifeTime = 3;
|
||
|
public float minDistance; // Minimum distance required to use this skill
|
||
|
public float delayToLaunch;
|
||
|
public int animationIndex;
|
||
|
}
|
||
|
}
|
||
|
|