using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace BulletHellTemplate
{
///
/// Represents a monster in the game with its stats, behaviors, and skills.
///
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 activeDOTsFromSkills = new Dictionary(); // DOTs SkillData
private Dictionary activeDOTsFromPerks = new Dictionary(); // DOTs SkillPerkData
void Start()
{
currentHP = characterStats.baseHP;
navMeshAgent = GetComponent();
navMeshAgent.speed = characterStats.baseMoveSpeed;
// Initialize Animator if needed
animator = GetComponentInChildren();
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;
}
///
/// Sets the amount of gold this monster will give when killed.
///
/// The amount of gold.
public void SetMonsterGoldValue(int amount)
{
monsterGoldValue = amount;
}
///
/// Sets the amount of XP this monster will give when killed.
///
/// The amount of XP.
public void SetMonsterXPValue(int amount)
{
monsterXPValue = amount;
}
///
/// Finds the nearest target with the "Character" tag.
///
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;
}
}
}
///
/// Moves the monster towards the target.
///
private void MoveTowardsTarget()
{
if (target == null)
return;
navMeshAgent.SetDestination(target.position);
}
///
/// Handles the usage of skills by the monster.
///
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;
}
}
}
}
///
/// Checks if the current target is within the minimum distance required for the skill.
///
/// The skill to check against.
/// True if the target is within the minimum distance, otherwise false.
private bool IsTargetWithinMinDistance(Skill skill)
{
if (target == null)
return false;
float distanceToTarget = Vector3.Distance(transform.position, target.position);
return distanceToTarget <= skill.minDistance;
}
///
/// Uses a specific skill on the current target, triggers the appropriate animation,
/// and delays the skill launch if needed to synchronize with the animation.
///
/// The skill to use.
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.");
}
}
///
/// Coroutine to delay the skill launch after the animation has started.
///
/// The skill to be launched after the delay.
private IEnumerator DelayedSkillLaunch(Skill skill)
{
// Wait for the specified delay to match the animation timing
yield return new WaitForSeconds(skill.delayToLaunch);
LaunchSkill(skill);
}
///
/// Launches the skill, handling multiple shots with angle and delay between shots.
///
/// The skill to launch.
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));
}
}
}
///
/// Coroutine to handle the delay between multiple shots in a multi-shot skill.
///
/// The skill being used.
/// The index of the current shot.
/// Position where the shot is spawned.
/// The base direction towards the target.
/// The angle for the current shot.
/// The step for the angle between shots.
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);
}
///
/// 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.
///
/// The amount of damage to receive.
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();
}
}
}
///
/// Applies damage to a target, considering element advantages and weaknesses.
///
/// The target to apply damage to.
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);
}
///
/// Handles collision with other objects.
///
/// The collider of the object the monster collided with.
private void OnTriggerEnter(Collider other)
{
if (GameplayManager.Singleton.IsPaused())
return;
if (other.CompareTag("Character") && canAttack)
{
CharacterEntity character = other.GetComponent();
if (character != null)
{
ApplyDamage(character);
StartCoroutine(AttackCooldown());
}
}
}
///
/// Coroutine to handle the attack cooldown.
///
///
private IEnumerator AttackCooldown()
{
canAttack = false;
yield return new WaitForSeconds(attackCooldown);
canAttack = true;
}
///
/// Applies a slow effect to the monster.
///
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;
}
///
/// 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.
///
/// The distance to knock the monster back.
/// The duration over which the knockback occurs.
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;
}
}
///
/// 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.
///
/// The duration of the stun in seconds.
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
}
///
/// Applies a DOT effect to the monster.
///
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));
}
///
/// 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.
///
/// The SkillPerkData that applies the DOT effect.
/// The total amount of damage to apply over time.
/// The duration over which to apply the damage.
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);
}
///
/// Applies a damage-over-time effect consistently over the specified duration.
///
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);
}
///
/// 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.
///
/// The SkillPerkData that applies the DOT effect.
/// The total amount of damage to apply over time.
/// The duration over which to apply the damage.
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);
}
///
/// Flashes the object in a specific color while an effect is active.
///
private IEnumerator FlashEffect(Color effectColor, float duration)
{
float elapsedTime = 0f;
List renderers = new List(GetComponentsInChildren());
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;
}
}
///
/// Regenerates HP based on the base HP regeneration rate.
///
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();
}
}
}
}
///
/// Applies health leech based on the damage dealt.
///
/// The total damage dealt.
public void ApplyHpLeech(float damage)
{
float leechAmount = damage * (characterStats.baseHPLeech / 100f);
currentHP += leechAmount;
if (currentHP > GetMaxHP())
{
currentHP = GetMaxHP();
}
}
}
///
/// Represents a skill that a monster can use.
///
[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;
}
}