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