using System.Collections; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; namespace BulletHellTemplate { /// /// Represents the main character entity in the game, handling movement, stats, skill usage, animations, and more. /// public class CharacterEntity : MonoBehaviour { private UIGameplay uiGameplay; private AdvancedCharacterController characterController; private CharacterData characterData; private Animator animator; private CharacterModel characterModel; // Character stats private float MaxHp; private float currentHP; private float currentHPRegen; private float currentHPLeech; private float MaxMp; private float currentMP; private float currentMPRegen; private float currentDamage; private float currentAttackSpeed; private float currentCooldownReduction; private float currentCriticalRate; private float currentCriticalDamageMultiplier; private float currentDefense; private float currentShield; private float currentMoveSpeed; private float currentCollectRange; private float currentMaxStats; private float currentMaxSkills; private int currentXP = 0; private int currentLevel = 1; [Header("Transforms")] public Transform characterModelTransform; public Transform launchTransform; public Transform effectsTransform; public GameObject directionalAim; [Header("Managers")] [Tooltip("Reference to the indicator manager.")] public IndicatorManager indicatorManager; [Header("UI Elements")] public Image hpBar; public Image mpBar; public Image shieldBar; public Image shieldIcon; public Image invencibleIcon; public Image buffIcon; public Image debuffIcon; public TextMeshProUGUI level; public TextMeshProUGUI insufficientMPText; private List skillsPerkData = new List(); private Dictionary activeSkillRoutines = new Dictionary(); private List statsPerkData = new List(); private bool canAutoAttack = true; private bool isInvincible = false; private int activeBuffCount = 0; private bool isMovementStopped = false; private bool isBuffActive => activeBuffCount > 0; private bool isDebuffActive = false; void Start() { // Find references to UIGameplay and AdvancedCharacterController uiGameplay = FindObjectOfType(); if (uiGameplay != null) { uiGameplay.SetCharacterEntity(this); } characterController = GetComponent(); if (characterController == null) { Debug.LogError("AdvancedCharacterController component not found on the character."); } // Initialize Animator if needed animator = GetComponentInChildren(); if (animator == null) { Debug.LogError("Animator component not found on the character."); } InitializeStats(); ApplyStatUpgrades(); // Start the auto-attack coroutine StartCoroutine(AutoAttackRoutine()); StartCoroutine(RegenerateHP()); StartCoroutine(RegenerateMP()); // Ensure the directional aim is initially inactive if (directionalAim != null) { directionalAim.SetActive(false); } } private void Update() { // Update HP bar if (hpBar != null) { hpBar.fillAmount = currentHP / MaxHp; } // Update MP bar if (mpBar != null) { mpBar.fillAmount = currentMP / MaxMp; } // Update Shield bar if (shieldBar != null) { shieldBar.fillAmount = currentShield / MaxHp; } if (shieldIcon != null) { shieldIcon.gameObject.SetActive(currentShield > 0); } if (invencibleIcon != null) { invencibleIcon.gameObject.SetActive(isInvincible); } if (buffIcon != null) { buffIcon.gameObject.SetActive(isBuffActive); } if (debuffIcon != null) { debuffIcon.gameObject.SetActive(isDebuffActive); } // Update Level text if (level != null) { level.text = $"Lv: {currentLevel}"; } } /// /// Displays a status message related to insufficient MP or other notifications. /// /// The message to display. public void DisplayMPInsufficientMessage(string message) { insufficientMPText.text = message; StartCoroutine(HideStatusMessageAfterDelay(insufficientMPText, 2f)); } /// /// Coroutine to hide the status message after a delay. /// /// The TextMeshProUGUI component displaying the status message. /// The delay in seconds before hiding the message. private IEnumerator HideStatusMessageAfterDelay(TextMeshProUGUI _insufficientMPText, float delay) { yield return new WaitForSeconds(delay); _insufficientMPText.text = string.Empty; } /// /// Sets the character data and updates the character model. /// /// The character data to set. public void SetCharacterData(CharacterData _characterData) { characterData = _characterData; UpdateCharacterModel(); } /// /// Returns the index of a specific skill in the character's skill array. /// /// The skill to find. /// The index of the skill, or -1 if not found. public int GetSkillIndex(SkillData skill) { if (skill == null) { Debug.LogWarning("Skill is null. Cannot find index."); return -1; } for (int index = 0; index < characterData.skills.Length; index++) { if (characterData.skills[index] == skill) { return index; } } Debug.LogWarning($"Skill {skill.skillName} not found in the character's skills."); return -1; } /// /// Retrieves the cooldown duration for a specific skill. /// /// The index of the skill in the character's skill array. /// The cooldown duration in seconds, or 0 if invalid. public float GetSkillCooldown(int skillIndex) { if (characterData != null && skillIndex >= 0 && skillIndex < characterData.skills.Length) { return characterData.skills[skillIndex].cooldown; } else { Debug.LogWarning($"Invalid skill index: {skillIndex}. Character data might be null or index out of range."); return 0f; } } /// /// Retrieves the icon sprite for a specific skill. /// /// The index of the skill in the character's skill array. /// The icon sprite of the skill, or null if invalid. public Sprite GetSkillIcon(int skillIndex) { if (characterData != null && skillIndex >= 0 && skillIndex < characterData.skills.Length) { return characterData.skills[skillIndex].icon; } else { return null; } } /// /// Retrieves the SkillData for a given skill index. /// /// The skill index. /// The SkillData object, or null if invalid. public SkillData GetSkillData(int index) { if (index >= 0 && index < characterData.skills.Length) { return characterData.skills[index]; } else { Debug.LogWarning($"Invalid skill index: {index}. Array length: {characterData.skills.Length}"); return null; } } /// /// Instantiates the character model from CharacterData and sets up the animator reference. /// public void UpdateCharacterModel() { if (characterData != null && characterData.characterModel != null || characterData.characterSkins != null) { // Destroy any existing character model to avoid duplicates foreach (Transform child in characterModelTransform) { Destroy(child.gameObject); } int skinIndex = PlayerSave.GetCharacterSkin(characterData.characterId); if (characterData.characterSkins != null && characterData.characterSkins.Length > 0 && skinIndex >= 0 && skinIndex < characterData.characterSkins.Length) { CharacterSkin skin = characterData.characterSkins[skinIndex]; if (skin.skinCharacterModel != null) { characterModel = Instantiate(skin.skinCharacterModel, characterModelTransform.position, characterModelTransform.rotation, characterModelTransform); } } else if (characterData.characterModel != null) { characterModel = Instantiate(characterData.characterModel, characterModelTransform.position, characterModelTransform.rotation, characterModelTransform); } animator = characterModel.GetComponent(); if (animator == null) { Debug.LogError("Animator component not found on the character model."); } } } /// /// Initializes character stats based on the data provided in CharacterData. /// private void InitializeStats() { int _characterLevel = PlayerSave.GetCharacterLevel(characterData.characterId); float levelMultiplier = 1f + (characterData.statsPercentageIncreaseByLevel * (_characterLevel - 1)); if (characterData != null && characterData.baseStats != null) { MaxMp = characterData.baseStats.baseMP * levelMultiplier; currentMP = MaxMp; MaxHp = characterData.baseStats.baseHP * levelMultiplier; currentHP = MaxHp; currentHPRegen = characterData.baseStats.baseHPRegen * levelMultiplier; currentHPLeech = characterData.baseStats.baseHPLeech * levelMultiplier; currentMPRegen = characterData.baseStats.baseMPRegen * levelMultiplier; currentDamage = characterData.baseStats.baseDamage * levelMultiplier; currentDefense = characterData.baseStats.baseDefense * levelMultiplier; currentShield = characterData.baseStats.baseShield * levelMultiplier; currentAttackSpeed = characterData.baseStats.baseAttackSpeed * levelMultiplier; currentMoveSpeed = characterData.baseStats.baseMoveSpeed * levelMultiplier; currentCollectRange = characterData.baseStats.baseCollectRange * levelMultiplier; currentMPRegen = characterData.baseStats.baseMPRegen * levelMultiplier; currentDamage = characterData.baseStats.baseDamage * levelMultiplier; currentDefense = characterData.baseStats.baseDefense * levelMultiplier; currentShield = characterData.baseStats.baseShield * levelMultiplier; currentMaxStats = characterData.baseStats.baseMaxStats; currentMaxSkills = characterData.baseStats.baseMaxSkills; if (characterController != null) { characterController.AlterSpeed(currentMoveSpeed); } } else { Debug.LogError("CharacterData or CharacterStats is null."); } ApplyEquippedItemsStats(); } /// /// Applies all the stat upgrades that have been accumulated for this character. /// private void ApplyStatUpgrades() { int characterId = GetCharacterData().characterId; Dictionary upgradeLevels = PlayerSave.LoadAllCharacterUpgradeLevels(characterId); foreach (var upgrade in upgradeLevels) { if (upgrade.Value > 0) { float totalUpgradeAmount = 0f; for (int i = 1; i <= upgrade.Value; i++) { StatUpgrade statUpgrade = GetStatUpgrade(upgrade.Key); if (statUpgrade != null) { totalUpgradeAmount += statUpgrade.upgradeAmounts[i - 1]; } else { Debug.LogError($"StatUpgrade not found for {upgrade.Key}"); } } UpdateStatFromUpgrade(upgrade.Key, totalUpgradeAmount); } } } /// /// Applies stats from all equipped items (base item stats and upgrade bonuses) directly to the current stats. /// This method updates currentHP, currentMP, currentDamage, etc., based on the equipped items. /// private void ApplyEquippedItemsStats() { // For each slot in the character's item slots, find the equipped item GUID. foreach (string slotName in characterData.itemSlots) { string uniqueItemGuid = InventorySave.GetEquippedItemForSlot(characterData.characterId, slotName); if (string.IsNullOrEmpty(uniqueItemGuid)) continue; // Retrieve the purchased item data based on the unique GUID. var purchasedItem = MonetizationManager.Singleton.GetPurchasedInventoryItems() .Find(pi => pi.uniqueItemGuid == uniqueItemGuid); if (purchasedItem == null) continue; // Find the corresponding scriptable inventory item. var soItem = FindItemById(purchasedItem.itemId); if (soItem == null) continue; // Apply base item stats to the current stats. if (soItem.itemStats != null) { MaxHp += soItem.itemStats.baseHP; currentHP = MaxHp; currentHPRegen += soItem.itemStats.baseHPRegen; currentHPLeech += soItem.itemStats.baseHPLeech; currentMP += soItem.itemStats.baseMP; currentMPRegen += soItem.itemStats.baseMPRegen; currentDamage += soItem.itemStats.baseDamage; currentAttackSpeed += soItem.itemStats.baseAttackSpeed; currentCooldownReduction += soItem.itemStats.baseCooldownReduction; currentCriticalRate += soItem.itemStats.baseCriticalRate; currentCriticalDamageMultiplier += soItem.itemStats.baseCriticalDamageMultiplier; currentDefense += soItem.itemStats.baseDefense; currentShield += soItem.itemStats.baseShield; currentMoveSpeed += soItem.itemStats.baseMoveSpeed; currentCollectRange += soItem.itemStats.baseCollectRange; } // Apply upgrade bonuses from the item. int itemLevel = InventorySave.GetItemUpgradeLevel(uniqueItemGuid); float totalUpgrade = 0f; for (int i = 0; i < itemLevel && i < soItem.itemUpgrades.Count; i++) { totalUpgrade += soItem.itemUpgrades[i].statIncreasePercentagePerLevel; } if (totalUpgrade > 0f) { float factor = totalUpgrade; // For example, 0.2 means +20%. currentHP += soItem.itemStats.baseHP * factor; currentHPRegen += soItem.itemStats.baseHPRegen * factor; currentHPLeech += soItem.itemStats.baseHPLeech * factor; currentMP += soItem.itemStats.baseMP * factor; currentMPRegen += soItem.itemStats.baseMPRegen * factor; currentDamage += soItem.itemStats.baseDamage * factor; currentAttackSpeed += soItem.itemStats.baseAttackSpeed * factor; currentCooldownReduction += soItem.itemStats.baseCooldownReduction * factor; currentCriticalRate += soItem.itemStats.baseCriticalRate * factor; currentCriticalDamageMultiplier += soItem.itemStats.baseCriticalDamageMultiplier * factor; currentDefense += soItem.itemStats.baseDefense * factor; currentShield += soItem.itemStats.baseShield * factor; currentMoveSpeed += soItem.itemStats.baseMoveSpeed * factor; currentCollectRange += soItem.itemStats.baseCollectRange * factor; } } } private InventoryItem FindItemById(string itemId) { if (GameInstance.Singleton == null || GameInstance.Singleton.inventoryItems == null) return null; foreach (InventoryItem item in GameInstance.Singleton.inventoryItems) { if (item.itemId == itemId) return item; } return null; } /// /// Stops the player's movement for a specified duration. /// /// Duration in seconds to stop movement. /// If true, allows rotation while movement is stopped. public IEnumerator StopMovement(float duration, bool allowRotation = false) { if (characterController == null) { Debug.LogWarning("CharacterController is not assigned."); yield break; } characterController.StopMovement(allowRotation); isMovementStopped = true; yield return new WaitForSeconds(duration); characterController.ResumeMovement(); isMovementStopped = false; } #region Movement and Animation Control /// /// Moves the character using the given direction. /// If the game is paused or movement is stopped (and rotation is allowed), forces Idle. /// Otherwise, uses the CharacterModel (if playable animations are enabled) to play the run or idle animations. /// Compares the movement direction with the CharacterModel's forward to decide whether to play RunForward, RunBackward, RunLeft, or RunRight. /// /// The movement direction. public void Move(Vector3 direction) { // If the game is paused, play Idle and return. if (GameplayManager.Singleton.IsPaused()) { if (characterModel != null && characterModel.usePlayableAnimations) { characterModel.PlayBaseAnimation("Idle"); } else if (animator != null) { animator.SetBool("isRunning", false); animator.SetFloat("speed", 0); animator.SetBool("isIdle", true); } return; } // If the character is stopped and can rotate while stopped, rotate if there's input, else stay Idle. if (isMovementStopped && characterController != null && characterController.CanRotateWhileStopped) { if (direction.magnitude > 0f) { float targetAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg; float angle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref characterController.rotationSpeed, 0.1f); transform.rotation = Quaternion.Euler(0f, angle, 0f); } if (characterModel != null && characterModel.usePlayableAnimations) { characterModel.PlayBaseAnimation("Idle"); } else if (animator != null) { animator.SetBool("isRunning", false); animator.SetFloat("speed", 0); animator.SetBool("isIdle", true); } return; } // If the character can move, apply movement via the controller. if (characterController != null) { characterController.Move(direction); // Playable Animations if (characterModel != null && characterModel.usePlayableAnimations) { // If an action is currently playing, do not override with run/idle. if (characterModel.IsActionPlaying()) { return; } // If the direction has magnitude, play a run animation based on the CharacterModel's forward. if (direction.magnitude > 0.1f) { Vector3 modelLookDirection = (characterModelTransform != null) ? characterModelTransform.forward : transform.forward; characterModel.PlayRunAnimation(direction, modelLookDirection); } else { characterModel.PlayBaseAnimation("Idle"); } } // Fallback to Legacy Animator else if (animator != null) { if (direction.magnitude > 0f) { animator.SetBool("isRunning", true); animator.SetFloat("speed", characterController.GetCurrentSpeed()); animator.SetBool("isIdle", false); } else { animator.SetBool("isRunning", false); animator.SetFloat("speed", 0f); animator.SetBool("isIdle", true); } } } } #endregion /// /// Uses the skill at the specified index and launches it in the specified direction. /// The skill is launched from the launchTransform in the direction specified. /// /// The index of the skill to use. /// The input direction from a joystick or other source. public void UseSkill(int index, Vector2 inputDirection) { if (GameplayManager.Singleton.IsPaused()) return; if (characterData == null || characterData.skills == null || index < 0 || index >= characterData.skills.Length) { Debug.LogWarning("Invalid skill index."); return; } SkillData skill = characterData.skills[index]; if (skill == null) { Debug.LogWarning("Skill data is null."); return; } if (skill.manaCost > currentMP) { DisplayMPInsufficientMessage("Not enough MP to use skill"); return; } currentMP -= skill.manaCost; // Spawn effect if (skill.spawnEffect != null) { GameObject effectInstance = Instantiate(skill.spawnEffect, effectsTransform.position, effectsTransform.rotation, effectsTransform); if (skill.effectDuration > 0) { Destroy(effectInstance, skill.effectDuration); } } // Play audio if (skill.spawnAudio != null && !skill.playSpawnAudioEaShot) { AudioManager.Singleton.PlayAudio(skill.spawnAudio, "vfx"); } if (skill.skillAudio != null && !skill.playSkillAudioEaShot) { StartCoroutine(PlaySkillAudio(skill.skillAudio, skill.playSkillAudioAfter)); } if (launchTransform == null) { launchTransform = transform; } ApplySkillBuffs(skill); Vector3 skillDirection = Vector3.zero; switch (skill.AimMode) { case AimType.InputDirection: skillDirection = GetDirection(inputDirection).normalized; break; case AimType.TargetNearestEnemy: Transform nearestTarget = FindNearestMonster(); skillDirection = (nearestTarget != null) ? (nearestTarget.position - launchTransform.position).normalized : transform.forward; break; case AimType.FowardDirection: skillDirection = transform.forward; break; case AimType.ReverseDirection: skillDirection = -transform.forward; break; case AimType.RandomDirection: skillDirection = GetRandomDirection(); break; } if (skill.isRotateToEnemy) { StartCoroutine(RotateCharacterModel(skillDirection, skill)); } if (skill.delayToMove > 0) { bool canRotate = skill.canRotateWhileStopped; StartCoroutine(StopMovement(skill.delayToMove, canRotate)); } // Advanced dash logic if (skill.advancedDashSettings != null && skill.advancedDashSettings.enableAdvancedDash) { Transform nearest = FindNearestMonster(); StartCoroutine(HandleAdvancedDash(skill.advancedDashSettings, skillDirection, nearest, index)); } else if (skill.isDash) { Vector3 dashDirection = skillDirection; if (skill.isReverseDash) { dashDirection = -skillDirection; } StartCoroutine(characterController.Dash(dashDirection, skill.dashSpeed, skill.dashDuration)); } // Shield if (skill.receiveShield) { GainShield(skill.shieldAmount, skill.shieldDuration); } // Invincibility if (skill.isInvincible) { StartCoroutine(Invincible(skill.invincibleDuration)); } // Reset auto-attack StartCoroutine(ResetAutoAttack(skill.autoAttackDelay)); // Launch skill projectiles StartCoroutine(LaunchSkill(skill, skillDirection, false, index)); } /// /// Coroutine to handle advanced dash without waiting for the animation to complete. /// Each dash wave may trigger an action animation (UseSkillX) if AnimationTriggerEachDash is true. /// /// Advanced dash settings. /// The dash direction input. /// Nearest target transform (if any). /// Index for the UseSkill animation trigger. private IEnumerator HandleAdvancedDash(AdvancedDashSettings dashSettings, Vector3 inputDirection, Transform nearestTarget, int skillIndex) { for (int i = 0; i < dashSettings.dashWaves; i++) { Vector3 dashDir = Vector3.zero; switch (dashSettings.dashMode) { case DashMode.InputDirection: dashDir = inputDirection.normalized; break; case DashMode.ForwardOnly: dashDir = transform.forward; break; case DashMode.ReverseOnly: dashDir = -inputDirection.normalized; break; case DashMode.NearestTarget: dashDir = nearestTarget != null ? (nearestTarget.position - transform.position).normalized : transform.forward; break; case DashMode.RandomDirection: dashDir = GetRandomDirection(); break; } if (dashSettings.AnimationTriggerEachDash) { if (characterModel != null && characterModel.usePlayableAnimations) { float speed = (characterController != null) ? characterController.GetCurrentSpeed() : 0f; string returnAnim = (speed > 0f) ? "RunForward" : "Idle"; StartCoroutine(characterModel.PlayActionAnimationOnce( $"UseSkill{skillIndex}", 0f, returnAnim )); } else if (animator != null) { animator.SetTrigger($"UseSkill{skillIndex}"); animator.SetBool("isIdle", false); } } yield return StartCoroutine(characterController.Dash(dashDir, dashSettings.dashSpeed, dashSettings.dashDuration)); if (i < dashSettings.dashWaves - 1 && dashSettings.delayBetweenWaves > 0f) { yield return new WaitForSeconds(dashSettings.delayBetweenWaves); } } } private IEnumerator PlaySkillAudio(AudioClip skillAudio, float delay) { yield return new WaitForSeconds(delay); AudioManager.Singleton.PlayAudio(skillAudio, "vfx"); } /// /// Triggers an action animation for a skill without waiting. Uses PlayActionAnimationOnce on the CharacterModel. /// /// Index of the skill animation trigger. public void PlayTriggerAnimation(int skillIndex) { if (characterModel != null && characterModel.usePlayableAnimations) { float speed = (characterController != null) ? characterController.GetCurrentSpeed() : 0f; string returnAnim = (speed > 0f) ? "RunForward" : "Idle"; StartCoroutine(characterModel.PlayActionAnimationOnce( $"UseSkill{skillIndex}", 0f, returnAnim )); } else if (animator != null) { animator.SetTrigger($"UseSkill{skillIndex}"); } } /// /// Coroutine to reset the ability to auto-attack after a delay. /// /// Delay before allowing auto-attack again. private IEnumerator ResetAutoAttack(float delay) { canAutoAttack = false; yield return new WaitForSeconds(delay); canAutoAttack = true; } /// /// Updates the directional aim's rotation based on the joystick input and updates the corresponding indicator. /// /// The direction from the skill joystick. /// Index of the skill being aimed. public void UpdateDirectionalAim(Vector2 joystickDirection, int skillIndex) { if (directionalAim == null) { Debug.LogWarning("Directional aim object is not set."); return; } if (joystickDirection.magnitude > 0) { Vector3 direction3D = new Vector3(joystickDirection.x, 0, joystickDirection.y).normalized; Quaternion newRotation = Quaternion.LookRotation(direction3D, Vector3.up); directionalAim.transform.rotation = newRotation; } if (!directionalAim.activeSelf || indicatorManager == null) return; SkillData skillData = GetSkillData(skillIndex); if (skillData == null || skillData.rangeIndicatorType == RangeIndicatorType.None) return; // 1) Calculate maxRange from skillData float maxRange = 1f; if (skillData.rangeIndicatorType == RangeIndicatorType.RadialAoE) { maxRange = skillData.radialAoEIndicatorSettings.radiusMaxRange; } else if (skillData.rangeIndicatorType == RangeIndicatorType.Radial) { maxRange = skillData.radialIndicatorSettings.radiusArea; } // For arrow or cone, you might define a maxRange somewhere // 2) We get the magnitude of joystickDirection to define how far the user is aiming // magnitude is 0..1 from the joystick => we can scale to maxRange float joystickMag = Mathf.Clamp01(joystickDirection.magnitude); float actualDistance = joystickMag * maxRange; // The "aim world position" in front of the character => transform.position + transform.forward * actualDistance Vector3 aimWorldPos = transform.position + transform.forward * actualDistance; switch (skillData.rangeIndicatorType) { case RangeIndicatorType.Radial: // Show 1) a big circle with radiusArea (the maxRange) around the player indicatorManager.ShowCircleRangeIndicator(maxRange); break; case RangeIndicatorType.RadialAoE: // Show the big circle for max range indicatorManager.ShowCircleRangeIndicator(skillData.radialAoEIndicatorSettings.radiusMaxRange); // Show a smaller circle at the aim position with radius = skillData.radialAoEIndicatorSettings.radiusArea indicatorManager.ShowPositionAoECircle(skillData.radialAoEIndicatorSettings.radiusArea, aimWorldPos); break; case RangeIndicatorType.Arrow: indicatorManager.ShowArrowIndicator(); // We rotate around Y only, or we can use the directionalAim's rotation: Quaternion arrowRot = directionalAim.transform.rotation; // pass the distance, maxRange, etc. indicatorManager.UpdateArrowIndicator( transform.position, // base position actualDistance, maxRange, arrowRot ); break; case RangeIndicatorType.Cone: indicatorManager.ShowConeIndicator(); Quaternion coneRot = directionalAim.transform.rotation; indicatorManager.UpdateConeIndicator( transform.position, actualDistance, maxRange, coneRot ); break; } } /// /// Activates or deactivates the directional aim and manages indicator animations. /// /// True to activate the directional aim; false to deactivate. /// Index of the skill being used. /// True if the skill was used (triggering cast indicator animation); false if canceled. public void SetDirectionalAimActive(bool isActive, int skillIndex, bool skillUsed) { if (directionalAim != null) directionalAim.SetActive(isActive); if (!isActive && indicatorManager != null) { SkillData skillData = GetSkillData(skillIndex); if (skillData != null && skillData.rangeIndicatorType != RangeIndicatorType.None) { // Hide the “position AoE circle” if RadialAoE: indicatorManager.HidePositionAoECircle(); if (skillUsed) { float castDuration = skillData.delayToLaunch; switch (skillData.rangeIndicatorType) { case RangeIndicatorType.Radial: if (skillData.radialIndicatorSettings.useCastIndicator) indicatorManager.StartCastCircleIndicator(skillData.radialIndicatorSettings.radiusArea, castDuration); else indicatorManager.CloseIndicators(); break; case RangeIndicatorType.RadialAoE: if (skillData.radialAoEIndicatorSettings.useCastIndicator) { // Animate the small circle => castDamageIndicator indicatorManager.StartCastDamageIndicator(skillData.radialAoEIndicatorSettings.radiusArea, castDuration); // Optionally show curve line Vector3 startPos = transform.position; Vector3 endPos = transform.position + transform.forward * skillData.radialAoEIndicatorSettings.radiusMaxRange; indicatorManager.ShowAoECurveLine(startPos, endPos); StartCoroutine(indicatorManager.HideAoECurveLineAfterDelay(castDuration + 0.1f)); } else { indicatorManager.CloseIndicators(); } break; case RangeIndicatorType.Arrow: if (skillData.arrowIndicatorSettings.useCastIndicator) { indicatorManager.StartCastArrowIndicator(skillData.arrowIndicatorSettings.arrowSize, castDuration); } else { indicatorManager.CloseIndicators(); } break; case RangeIndicatorType.Cone: if (skillData.coneIndicatorSettings.useCastIndicator) { indicatorManager.StartCastConeIndicator(skillData.coneIndicatorSettings.ConeSize, castDuration); } else { indicatorManager.CloseIndicators(); } break; } } else { // If canceled, close immediately indicatorManager.CloseIndicators(); } } } } /// /// Converts a 2D direction vector to a 3D direction. /// /// The 2D direction vector. /// The 3D direction vector. private Vector3 GetDirection(Vector2 direction) { return new Vector3(direction.x, 0, direction.y); } /// /// Coroutine to launch a skill without waiting for the animation to complete. /// Triggers the appropriate animation and, after delayToLaunch, spawns the skill damage. /// /// The skill data. /// The launch direction. /// True if this is an auto-attack. /// Index used for naming the trigger. private IEnumerator LaunchSkill(SkillData skill, Vector3 direction, bool isAutoAttack, int index) { string trigger = isAutoAttack ? "Attack" : $"UseSkill{index}"; if (characterModel != null && characterModel.usePlayableAnimations) { float speed = (characterController != null) ? characterController.GetCurrentSpeed() : 0f; string returnAnim = (speed > 0f) ? "RunForward" : "Idle"; StartCoroutine(characterModel.PlayActionAnimationOnce( trigger, skill.delayToLaunch, returnAnim )); } else if (animator != null) { animator.SetTrigger(trigger); animator.SetBool("isIdle", false); } yield return new WaitForSeconds(skill.delayToLaunch); skill.LaunchDamage(launchTransform, direction, this); } /// /// Coroutine to revert to the base animation after a specified delay. /// /// The base animation key (e.g., "Idle" or "Run"). /// Delay in seconds. private IEnumerator RevertToBaseAnimation(string baseAnim, float delay) { yield return new WaitForSeconds(delay); if (characterModel != null && characterModel.usePlayableAnimations) { characterModel.PlayBaseAnimation(baseAnim); } } /// /// Executes the auto-attack action using the AutoAttack skill from characterData. /// public void Attack() { if (GameplayManager.Singleton.IsPaused()) return; if (!canAutoAttack) return; if (characterData == null || characterData.autoAttack == null) { Debug.LogWarning("No auto-attack skill available to use."); return; } SkillData skill = characterData.autoAttack; if (launchTransform == null) { launchTransform = transform; } // Find the nearest target with the tag "Monster" or "Box" Transform nearestTarget = FindNearestMonster(); if (nearestTarget == null) { return; } Vector3 directionToTarget = nearestTarget.position - launchTransform.position; directionToTarget.y = 0f; Vector3 attackDirection = directionToTarget.normalized; // Launch the skill in the direction of the nearest target StartCoroutine(LaunchSkill(skill, attackDirection, true, 0)); // Spawn the skill's effect if any if (skill.spawnEffect != null) { GameObject effectInstance = Instantiate(skill.spawnEffect, effectsTransform.position, effectsTransform.rotation, effectsTransform); if (skill.effectDuration > 0) { Destroy(effectInstance, skill.effectDuration); } } // Play skill audio if (skill.spawnAudio != null) { AudioManager.Singleton.PlayAudio(skill.spawnAudio, "vfx"); } if (skill.skillAudio != null) { StartCoroutine(PlaySkillAudio(skill.skillAudio, skill.playSkillAudioAfter)); } if (skill.isRotateToEnemy) { StartCoroutine(RotateCharacterModel(attackDirection, skill)); } } /// /// Coroutine to rotate the character model towards the attack direction, /// ignoring the Y component to prevent vertical rotation. /// /// The direction of the attack. /// The skill data containing rotation duration. private IEnumerator RotateCharacterModel(Vector3 direction, SkillData skill) { if (characterModelTransform == null) { Debug.LogWarning("Character model transform is not set."); yield break; } Vector3 flatDirection = new Vector3(direction.x, 0, direction.z).normalized; if (flatDirection == Vector3.zero) { Debug.LogWarning("Direction is zero after flattening. Cannot rotate character model."); yield break; } Quaternion targetRotation = Quaternion.LookRotation(flatDirection); float timer = 0f; while (timer < skill.rotateDuration) { Quaternion desiredLocalRotation = Quaternion.Inverse(transform.rotation) * targetRotation; characterModelTransform.localRotation = desiredLocalRotation; timer += Time.deltaTime; yield return null; } characterModelTransform.localRotation = Quaternion.identity; } /// /// Finds the nearest target with the tags "Monster" or "Box". /// /// The transform of the nearest target, or null if no targets are found. private Transform FindNearestMonster() { GameObject[] monsters = GameObject.FindGameObjectsWithTag("Monster"); GameObject[] boxes = GameObject.FindGameObjectsWithTag("Box"); GameObject[] allTargets = new GameObject[monsters.Length + boxes.Length]; System.Array.Copy(monsters, 0, allTargets, 0, monsters.Length); System.Array.Copy(boxes, 0, allTargets, monsters.Length, boxes.Length); if (allTargets.Length == 0) { return null; } Transform nearestTarget = null; float nearestDistance = Mathf.Infinity; foreach (GameObject target in allTargets) { float distance = Vector3.Distance(transform.position, target.transform.position); if (distance < nearestDistance) { nearestDistance = distance; nearestTarget = target.transform; } } return nearestTarget; } /// /// Generates a random direction on the XZ plane. /// /// A normalized Vector3 representing a random direction. private Vector3 GetRandomDirection() { float randomAngle = UnityEngine.Random.Range(0f, 360f); float radian = randomAngle * Mathf.Deg2Rad; return new Vector3(Mathf.Cos(radian), 0, Mathf.Sin(radian)).normalized; } /// /// Coroutine to handle the auto-attack based on the cooldown. /// private IEnumerator AutoAttackRoutine() { while (true) { Attack(); yield return new WaitForSeconds(GetCurrentAttackSpeed()); } } /// /// Makes the character jump using the character controller. /// public void Jump() { if (characterController != null) { characterController.Jump(); } } /// /// Alters the movement speed of the character. /// /// The new speed value. public void AlterSpeed(float newSpeed) { if (characterController != null) { characterController.AlterSpeed(newSpeed); currentMoveSpeed = newSpeed; } } /// /// Applies a force to the character for pushing. /// /// The force to apply. public void Push(Vector3 force) { if (characterController != null) { characterController.Push(force); } } public CharacterTypeData GetCharacterType() { return characterData.characterType; } public CharacterData GetCharacterData() { return characterData; } /// /// Receives damage, reducing shield first if available and then HP. /// Damage is reduced by current defense but not lower than a global minimum. /// /// The damage to receive. public void ReceiveDamage(float damage) { if (GameplayManager.Singleton.IsPaused()) return; if (isInvincible) return; damage = Mathf.Max(damage - currentDefense, GameplayManager.Singleton.minDamage); if (currentShield > 0) { float shieldDamage = Mathf.Min(damage, currentShield); currentShield -= shieldDamage; damage -= shieldDamage; } if (damage > 0) { currentHP -= damage; if (currentHP <= 0) { Die(); } } } /// /// Sets the character invincible for a duration. /// /// Duration in seconds. public IEnumerator Invincible(float duration) { isInvincible = true; yield return new WaitForSeconds(duration); isInvincible = false; } /// /// Applies damage to the target monster considering elemental advantages and weaknesses. /// /// The target monster. /// The base amount of damage. public void ApplyDamage(MonsterEntity target, float damage) { if (GameplayManager.Singleton.IsPaused()) return; if (target == null) { Debug.LogWarning("Target is null. Cannot apply damage."); return; } if (damage <= 0) { Debug.LogWarning("Damage must be greater than zero."); return; } CharacterTypeData targetType = target.characterTypeData; float totalDamage = GameInstance.Singleton.TotalDamageWithElements(characterData.characterType, targetType, damage); target.ReceiveDamage(totalDamage); Debug.Log($"Applied {totalDamage} damage to {target.name}."); } /// /// Handles the character's death. /// private void Die() { Debug.Log("Character has died."); GameplayManager.Singleton.PauseGame(); UIGameplay.Singleton.DisplayEndGameScreen(false); // GameManager.Singleton.EndGame(false); } /// /// Applies a skill perk and manages its execution based on level and cooldown. /// /// The skill perk to apply. public void ApplySkillPerk(SkillPerkData skillPerk) { if (skillPerk == null) { Debug.LogWarning("SkillPerkData is null."); return; } int _currentLevel = GameplayManager.Singleton.GetSkillLevel(skillPerk); if (skillsPerkData.Contains(skillPerk)) { if (activeSkillRoutines.ContainsKey(skillPerk)) { StopCoroutine(activeSkillRoutines[skillPerk]); activeSkillRoutines[skillPerk] = StartCoroutine(ExecuteSkill(skillPerk, _currentLevel)); } } else { skillsPerkData.Add(skillPerk); Coroutine skillRoutine = StartCoroutine(ExecuteSkill(skillPerk, _currentLevel)); activeSkillRoutines.Add(skillPerk, skillRoutine); } uiGameplay.UpdateSkillPerkUI(); } /// /// Coroutine to execute a skill, launching damage entities periodically based on cooldown. /// /// The skill perk data. /// The current level of the skill. private IEnumerator ExecuteSkill(SkillPerkData skillPerk, int level) { float cooldown = skillPerk.withoutCooldown ? 0f : skillPerk.cooldown; float cooldownReduction = GetCurrentCooldownReduction() / 100; float finalCooldown = cooldown * (1 - cooldownReduction); while (true) { if (GameplayManager.Singleton.IsPaused()) { yield return null; continue; } Transform nearestTarget = FindNearestMonster(); if (nearestTarget == null) { yield return new WaitForSeconds(finalCooldown); continue; } Vector3 direction = (nearestTarget.position - transform.position).normalized; if (skillPerk.isMultiShot) { bool isMaxLevel = level == skillPerk.maxLevel; int shots = isMaxLevel && skillPerk.evolveChanges ? skillPerk.shotsEvolved : skillPerk.shots; float angle = isMaxLevel && skillPerk.evolveChanges ? skillPerk.angleEvolved : skillPerk.angle; float delay = isMaxLevel && skillPerk.evolveChanges ? skillPerk.delayEvolved : skillPerk.delay; yield return StartCoroutine(MultiShotDamageEntity(skillPerk, level, direction, shots, angle, delay)); } else { SpawnDamageEntity(skillPerk, level, direction); } if (skillPerk.isDash) { StartCoroutine(characterController.Dash(transform.forward, skillPerk.dashSpeed, skillPerk.dashDuration)); } if (skillPerk.isShield) { GainShield(skillPerk.shieldAmount, skillPerk.shieldDuration); } yield return new WaitForSeconds(finalCooldown); } } /// /// Coroutine for multi-shot spawning of damage entities with angle and delay settings. /// /// The skill perk data. /// The level of the skill perk. /// Base direction for shots. /// Number of shots. /// Angle between shots. /// Delay between shots. private IEnumerator MultiShotDamageEntity(SkillPerkData skillPerk, int level, Vector3 baseDirection, int shots, float angle, float delay) { float halfAngle = (shots - 1) * angle / 2; for (int i = 0; i < shots; i++) { if (GameplayManager.Singleton.IsPaused()) { yield return null; continue; } float shotAngle = -halfAngle + i * angle; Quaternion rotation = Quaternion.Euler(0, shotAngle, 0); Vector3 shotDirection = rotation * baseDirection; SpawnDamageEntity(skillPerk, level, shotDirection); if (delay > 0 && i < shots - 1) { yield return new WaitForSeconds(delay); } } } /// /// Instantiates a damage entity based on the skill level and configuration, aiming it in a specified direction. /// /// The skill perk data. /// The level of the skill perk. /// Direction to launch the damage entity. private void SpawnDamageEntity(SkillPerkData skillPerk, int level, Vector3 direction) { if (GameplayManager.Singleton.IsPaused()) { return; } DamageEntity prefab = level >= skillPerk.maxLevel ? skillPerk.maxLvDamagePrefab : skillPerk.initialDamagePrefab; float damage = skillPerk.GetDamageForLevel(level); float attackerRate = skillPerk.GetAttackerDamageRateForLevel(level); DamageEntity instance = Instantiate(prefab, transform.position, Quaternion.LookRotation(direction)); instance.SetSkillPerkData(skillPerk, direction * skillPerk.speed, this); } public bool HasStatPerk(StatPerkData perk) { return GetStatsPerkData().Contains(perk); } /// /// Applies a stat perk to the character, adjusting the relevant stat. /// /// The stat perk to apply. /// The level of the perk being applied. public void ApplyStatPerk(StatPerkData statPerk, int level) { if (statPerk == null) { Debug.LogWarning("StatPerkData is null."); return; } if (statsPerkData.Contains(statPerk)) { ApplyPerkStats(statPerk, level); } else { statsPerkData.Add(statPerk); ApplyPerkStats(statPerk, level); } uiGameplay.UpdateStatPerkUI(); } /// /// Applies the stats of a specific perk based on its level. /// /// The stat perk to apply. /// The level of the perk. private void ApplyPerkStats(StatPerkData statPerk, int level) { float valueToAdd = 0f; if (statPerk.fixedStat != null && level < statPerk.fixedStat.values.Count) { valueToAdd = statPerk.fixedStat.values[level]; UpdateStat(statPerk.statType, valueToAdd); } if (statPerk.rateStat != null && level < statPerk.rateStat.rates.Count) { valueToAdd = (statPerk.rateStat.rates[level] * GetStatBaseValue(statPerk.statType)); UpdateStat(statPerk.statType, valueToAdd); } } /// /// Updates the specified stat type of the character by adding the provided value. /// private void UpdateStat(StatPerkData.StatType statType, float valueToAdd) { switch (statType) { case StatPerkData.StatType.HP: MaxHp += valueToAdd; currentHP += valueToAdd; break; case StatPerkData.StatType.HP_Regen: currentHPRegen += valueToAdd; break; case StatPerkData.StatType.HP_Leech: currentHPLeech += valueToAdd; break; case StatPerkData.StatType.MP: MaxMp += valueToAdd; currentMP += valueToAdd; break; case StatPerkData.StatType.MP_Regen: currentMPRegen += valueToAdd; break; case StatPerkData.StatType.Damage: currentDamage += valueToAdd; break; case StatPerkData.StatType.AttackSpeed: currentAttackSpeed -= valueToAdd; break; case StatPerkData.StatType.CooldownReduction: currentCooldownReduction += valueToAdd; break; case StatPerkData.StatType.CriticalRate: currentCriticalRate += valueToAdd; break; case StatPerkData.StatType.Defense: currentDefense += valueToAdd; break; case StatPerkData.StatType.MoveSpeed: currentMoveSpeed += valueToAdd; if (characterController != null) { characterController.AlterSpeed(currentMoveSpeed); } break; case StatPerkData.StatType.CollectRange: currentCollectRange += valueToAdd; break; } } /// /// Gets the base value for a given stat type. /// private float GetStatBaseValue(StatPerkData.StatType statType) { switch (statType) { case StatPerkData.StatType.HP: return MaxHp; case StatPerkData.StatType.HP_Regen: return currentHPRegen; case StatPerkData.StatType.HP_Leech: return currentHPLeech; case StatPerkData.StatType.MP: return MaxMp; case StatPerkData.StatType.MP_Regen: return currentMPRegen; case StatPerkData.StatType.Damage: return currentDamage; case StatPerkData.StatType.AttackSpeed: return currentAttackSpeed; case StatPerkData.StatType.CooldownReduction: return currentCooldownReduction; case StatPerkData.StatType.CriticalRate: return currentCriticalRate; case StatPerkData.StatType.Defense: return currentDefense; case StatPerkData.StatType.MoveSpeed: return currentMoveSpeed; case StatPerkData.StatType.CollectRange: return currentCollectRange; default: return 0f; } } /// /// Applies the specified stat upgrade to the character based on the upgrade level. /// /// The type of stat to upgrade. /// The level of the upgrade to apply. public void ApplyStatUpgrade(StatType statType, int level) { if (level <= 0) return; StatUpgrade upgrade = GetStatUpgrade(statType); if (upgrade != null && level > 0 && level <= upgrade.upgradeAmounts.Length) { float totalUpgradeAmount = 0f; for (int i = 1; i <= level; i++) { totalUpgradeAmount += upgrade.upgradeAmounts[i - 1]; } UpdateStatFromUpgrade(statType, totalUpgradeAmount); } } /// /// Retrieves the corresponding StatUpgrade for the given StatType. /// private StatUpgrade GetStatUpgrade(StatType statType) { return GetCharacterData().statUpgrades.Find(upgrade => upgrade.statType == statType); } /// /// Updates the specified stat type of the character by adding the provided value from a stat upgrade. /// private void UpdateStatFromUpgrade(StatType statType, float valueToAdd) { switch (statType) { case StatType.HP: MaxHp += valueToAdd; currentHP += valueToAdd; break; case StatType.HPRegen: currentHPRegen += valueToAdd; break; case StatType.HPLeech: currentHPLeech += valueToAdd; break; case StatType.MP: MaxMp += valueToAdd; currentMP += valueToAdd; break; case StatType.MPRegen: currentMPRegen += valueToAdd; break; case StatType.Damage: currentDamage += valueToAdd; break; case StatType.AttackSpeed: currentAttackSpeed -= valueToAdd; break; case StatType.CooldownReduction: currentCooldownReduction += valueToAdd; break; case StatType.CriticalRate: currentCriticalRate += valueToAdd; break; case StatType.Defense: currentDefense += valueToAdd; break; case StatType.MoveSpeed: currentMoveSpeed += valueToAdd; if (characterController != null) { characterController.AlterSpeed(currentMoveSpeed); } break; case StatType.CollectRange: currentCollectRange += valueToAdd; break; case StatType.MaxStats: currentMaxStats += valueToAdd; break; case StatType.MaxSkills: currentMaxSkills += valueToAdd; break; } } /// /// Retrieves the list of skill perks associated with the character. /// public List GetSkillsPerkData() { return skillsPerkData; } /// /// Retrieves the list of stat perks associated with the character. /// public List GetStatsPerkData() { return statsPerkData; } /// /// Applies the skill's buffs and debuffs to this character (heal, shield, speed buff, etc.). /// /// The skill data containing buff/debuff options. public void ApplySkillBuffs(SkillData skill) { if (skill.receiveHeal && skill.healAmount > 0f) { Heal((int)skill.healAmount); } if (skill.receiveShield && skill.shieldAmount > 0) { GainShield(skill.shieldAmount, skill.shieldDuration); } if (skill.receiveMoveSpeed && skill.moveSpeedAmount > 0f) { StartCoroutine(ApplyMoveSpeedBuff(skill.moveSpeedAmount, skill.moveSpeedDuration)); } if (skill.receiveAttackSpeed && skill.attackSpeedAmount > 0f) { StartCoroutine(ApplyAttackSpeedBuff(skill.attackSpeedAmount, skill.attackSpeedDuration)); } if (skill.receiveDefense && skill.defenseAmount > 0f) { StartCoroutine(ApplyDefenseBuff(skill.defenseAmount, skill.defenseDuration)); } if (skill.receiveDamage && skill.damageAmount > 0f) { StartCoroutine(ApplyDamageBuff(skill.damageAmount, skill.damageDuration)); } if (skill.isInvincible && skill.invincibleDuration > 0f) { StartCoroutine(Invincible(skill.invincibleDuration)); } // Self-slow (debuff) if (skill.receiveSlow && skill.receiveSlowAmount > 0f) { StartCoroutine(ApplySelfSlow(skill.receiveSlowAmount, skill.receiveSlowDuration)); } } private void IncrementBuffCount() { activeBuffCount++; } private void DecrementBuffCount() { activeBuffCount--; activeBuffCount = Mathf.Max(activeBuffCount, 0); } /// /// Temporarily increases the character's move speed and reverts after duration. /// private IEnumerator ApplyMoveSpeedBuff(float amount, float duration) { IncrementBuffCount(); currentMoveSpeed += amount; if (characterController != null) { characterController.AlterSpeed(currentMoveSpeed); } float elapsed = 0f; while (elapsed < duration) { if (GameplayManager.Singleton.IsPaused()) { while (GameplayManager.Singleton.IsPaused()) { yield return null; } } elapsed += Time.deltaTime; yield return null; } currentMoveSpeed -= amount; if (currentMoveSpeed < 0f) currentMoveSpeed = 0f; if (characterController != null) { characterController.AlterSpeed(currentMoveSpeed); } DecrementBuffCount(); } /// /// Temporarily increases the character's attack speed and reverts after duration. /// private IEnumerator ApplyAttackSpeedBuff(float amount, float duration) { IncrementBuffCount(); float originalAttackSpeed = currentAttackSpeed; float buffedAttackSpeed = Mathf.Max(originalAttackSpeed - amount, 0f); currentAttackSpeed = buffedAttackSpeed; float elapsed = 0f; while (elapsed < duration) { if (GameplayManager.Singleton.IsPaused()) { while (GameplayManager.Singleton.IsPaused()) { yield return null; } } elapsed += Time.deltaTime; yield return null; } currentAttackSpeed = originalAttackSpeed; DecrementBuffCount(); } /// /// Temporarily increases the character's defense and reverts after duration. /// private IEnumerator ApplyDefenseBuff(float amount, float duration) { IncrementBuffCount(); currentDefense += amount; float elapsed = 0f; while (elapsed < duration) { if (GameplayManager.Singleton.IsPaused()) { while (GameplayManager.Singleton.IsPaused()) { yield return null; } } elapsed += Time.deltaTime; yield return null; } currentDefense -= amount; if (currentDefense < 0f) currentDefense = 0f; DecrementBuffCount(); } /// /// Temporarily increases the character's damage and reverts after duration. /// private IEnumerator ApplyDamageBuff(float extraDamage, float duration) { IncrementBuffCount(); float originalMultiplier = currentDamage; currentDamage += extraDamage; float elapsed = 0f; while (elapsed < duration) { if (GameplayManager.Singleton.IsPaused()) { while (GameplayManager.Singleton.IsPaused()) { yield return null; } } elapsed += Time.deltaTime; yield return null; } currentDamage = originalMultiplier; DecrementBuffCount(); } /// /// Temporarily applies a slow debuff to the user, reducing their move speed by a percentage, and reverts after duration. /// private IEnumerator ApplySelfSlow(float slowPercentage, float duration) { float originalSpeed = currentMoveSpeed; currentMoveSpeed = Mathf.Max(originalSpeed * (1f - slowPercentage), 0f); if (characterController != null) { characterController.AlterSpeed(currentMoveSpeed); } float elapsed = 0f; while (elapsed < duration) { if (GameplayManager.Singleton.IsPaused()) { while (GameplayManager.Singleton.IsPaused()) { yield return null; } } elapsed += Time.deltaTime; yield return null; } currentMoveSpeed = originalSpeed; if (characterController != null) { characterController.AlterSpeed(currentMoveSpeed); } } /// /// Increases the shield of the character. /// /// The amount of shield to increase. public void GainShield(int amount) { if (amount > 0 && (currentShield + amount) <= GetMaxHP()) { currentShield += amount; } else { currentShield = GetMaxHP(); } } /// /// Increases the shield of the character for a specified duration. /// /// The amount of shield to increase. /// Duration in seconds for which the shield remains. public void GainShield(int amount, float duration) { if (amount > 0 && (currentShield + amount) <= GetMaxHP()) { currentShield += amount; } else { currentShield = GetMaxHP(); } StartCoroutine(RemoveShieldAfterDuration(amount, duration)); } /// /// Coroutine to remove the shield increase after the specified duration. /// private IEnumerator RemoveShieldAfterDuration(int amount, float duration) { float elapsedTime = 0f; while (elapsedTime < duration) { if (GameplayManager.Singleton.IsPaused()) { while (GameplayManager.Singleton.IsPaused()) { yield return null; } } elapsedTime += Time.deltaTime; yield return null; } currentShield = Mathf.Max(currentShield - amount, 0); } /// /// Regenerates HP based on the base HP regeneration rate. /// private IEnumerator RegenerateHP() { while (true) { yield return new WaitForSeconds(1f); if (GameplayManager.Singleton.IsPaused()) { while (GameplayManager.Singleton.IsPaused()) { yield return null; } } if (currentHP < GetMaxHP()) { currentHP += currentHPRegen; if (currentHP > GetMaxHP()) { currentHP = GetMaxHP(); } } } } /// /// Regenerates MP based on the base MP regeneration rate. /// private IEnumerator RegenerateMP() { while (true) { yield return new WaitForSeconds(1f); if (GameplayManager.Singleton.IsPaused()) { while (GameplayManager.Singleton.IsPaused()) { yield return null; } } if (currentMP < GetMaxMP()) { currentMP += currentMPRegen; if (currentMP > GetMaxMP()) { currentMP = GetMaxMP(); } } } } /// /// Increases the maximum HP of the character. /// /// Amount to increase max HP by. public void GainMaxHP(int amount) { if (amount > 0) { currentHP += amount; MaxHp += amount; } } /// /// Heals the character by a specified amount up to their maximum health. /// /// The amount to heal. public void Heal(int amount) { if (amount > 0) { currentHP = Mathf.Min(currentHP + amount, GetMaxHP()); } } /// /// Heals the character's MP. /// /// The amount to heal. public void HealMP(int amount) { if (amount > 0) { currentMP = Mathf.Min(currentMP + amount, GetMaxMP()); } } /// /// Applies health leech based on the damage dealt. /// /// The total damage dealt. public void ApplyHpLeech(float damage) { float leechAmount = damage * currentHPLeech; currentHP += leechAmount; if (currentHP > GetMaxHP()) { currentHP = GetMaxHP(); } } /// /// Increases the character's base damage. /// /// The amount to increase base damage. public void GainDamage(float amount) { if (amount > 0) { currentDamage += amount; } } /// /// Increases the attack speed of the character. /// /// The amount to decrease the attack speed timer by. public void GainAttackSpeed(float amount) { if (amount > 0) { currentAttackSpeed = Mathf.Max(currentAttackSpeed - amount, 0); } } /// /// Increases the cooldown reduction percentage. /// /// The amount to increase cooldown reduction by. public void CooldownReduction(float amount) { if (amount > 0) { currentCooldownReduction += amount; } } /// /// Increases the critical hit rate of the character. /// /// The amount to increase the critical rate by. public void GainCriticalRate(float amount) { if (amount > 0) { currentCriticalRate += amount; } } /// /// Increases the movement speed of the character. /// /// The amount to increase movement speed by. public void GainMoveSpeed(float amount) { if (amount > 0) { currentMoveSpeed += amount; if (characterController != null) { characterController.AlterSpeed(currentMoveSpeed); } } } /// /// Adds XP to the character and handles leveling up. /// /// The amount of XP to add. public void AddXP(int xpAmount) { currentXP += xpAmount; while (currentLevel < GameplayManager.Singleton.maxLevel && currentXP >= GameplayManager.Singleton.xpToNextLevel[currentLevel]) { LevelUp(); } } /// /// Revives the character to full HP and MP and grants temporary invincibility. /// public void Revive() { currentHP += GetMaxHP(); currentMP += GetMaxMP(); StartCoroutine(Invincible(2.5f)); } /// /// Levels up the character and handles perk selection based on the upgrade mode. /// private void LevelUp() { currentLevel++; currentXP -= GameplayManager.Singleton.xpToNextLevel[currentLevel - 1]; List perksToChoose = GameplayManager.Singleton.GetRandomPerks(); if (perksToChoose.Count > 0) { switch (GameplayManager.Singleton.upgradeMode) { case GameplayManager.UpgradeMode.UpgradeOnLevelUp: GameplayManager.Singleton.PauseGame(); uiGameplay.OnLevelUpChoicePowerUp(); break; case GameplayManager.UpgradeMode.UpgradeOnButtonClick: Debug.Log("Upgrade will be triggered on button click. No pause applied."); uiGameplay.AddUpgradePoints(); break; case GameplayManager.UpgradeMode.UpgradeRandom: Debug.Log("Random upgrade mode. Selecting random perk."); uiGameplay.OnRandomChoicePowerUp(); break; default: Debug.LogError("Unhandled upgrade mode!"); break; } } else { Debug.Log("No more perks available to choose or upgrade."); } } /// /// Gets the current level of the character. /// public int GetCurrentLevel() { return currentLevel; } /// /// Gets the current XP of the character. /// public int GetCurrentXP() { return currentXP; } /// /// Gets the XP required for the next level. /// public int GetXPToNextLevel() { if (currentLevel < GameplayManager.Singleton.maxLevel) { return GameplayManager.Singleton.xpToNextLevel[currentLevel]; } return -1; } // Getters public float GetCurrentHP() => currentHP; public float GetMaxHP() => MaxHp; public float GetCurrentHPRegen() => currentHPRegen; public float GetCurrentHPLeech() => currentHPLeech; public float GetCurrentMP() => currentMP; public float GetMaxMP() => MaxMp; public float GetCurrentMPRegen() => currentMPRegen; public float GetCurrentDamage() => currentDamage; public float GetCurrentAttackSpeed() => currentAttackSpeed; public float GetCurrentCooldownReduction() => currentCooldownReduction; public float GetCurrentCriticalRate() => currentCriticalRate; public float GetCurrentCriticalDamageMultiplier() => currentCriticalDamageMultiplier; public float GetCurrentDefense() => currentDefense; public float GetCurrentShield() => currentShield; public float GetCurrentMoveSpeed() => currentMoveSpeed; public float GetCurrentCollectRange() => currentCollectRange; public float GetCurrentMaxStats() => currentMaxStats; public float GetCurrentMaxSkills() => currentMaxSkills; } }