2263 lines
85 KiB
C#
2263 lines
85 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace BulletHellTemplate
|
||
{
|
||
/// <summary>
|
||
/// Represents the main character entity in the game, handling movement, stats, skill usage, animations, and more.
|
||
/// </summary>
|
||
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<SkillPerkData> skillsPerkData = new List<SkillPerkData>();
|
||
private Dictionary<SkillPerkData, Coroutine> activeSkillRoutines = new Dictionary<SkillPerkData, Coroutine>();
|
||
private List<StatPerkData> statsPerkData = new List<StatPerkData>();
|
||
|
||
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<UIGameplay>();
|
||
if (uiGameplay != null)
|
||
{
|
||
uiGameplay.SetCharacterEntity(this);
|
||
}
|
||
|
||
characterController = GetComponent<AdvancedCharacterController>();
|
||
if (characterController == null)
|
||
{
|
||
Debug.LogError("AdvancedCharacterController component not found on the character.");
|
||
}
|
||
|
||
// Initialize Animator if needed
|
||
animator = GetComponentInChildren<Animator>();
|
||
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}";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Displays a status message related to insufficient MP or other notifications.
|
||
/// </summary>
|
||
/// <param name="message">The message to display.</param>
|
||
public void DisplayMPInsufficientMessage(string message)
|
||
{
|
||
insufficientMPText.text = message;
|
||
StartCoroutine(HideStatusMessageAfterDelay(insufficientMPText, 2f));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Coroutine to hide the status message after a delay.
|
||
/// </summary>
|
||
/// <param name="statusText">The TextMeshProUGUI component displaying the status message.</param>
|
||
/// <param name="delay">The delay in seconds before hiding the message.</param>
|
||
private IEnumerator HideStatusMessageAfterDelay(TextMeshProUGUI _insufficientMPText, float delay)
|
||
{
|
||
yield return new WaitForSeconds(delay);
|
||
_insufficientMPText.text = string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sets the character data and updates the character model.
|
||
/// </summary>
|
||
/// <param name="_characterData">The character data to set.</param>
|
||
public void SetCharacterData(CharacterData _characterData)
|
||
{
|
||
characterData = _characterData;
|
||
UpdateCharacterModel();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns the index of a specific skill in the character's skill array.
|
||
/// </summary>
|
||
/// <param name="skill">The skill to find.</param>
|
||
/// <returns>The index of the skill, or -1 if not found.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retrieves the cooldown duration for a specific skill.
|
||
/// </summary>
|
||
/// <param name="skillIndex">The index of the skill in the character's skill array.</param>
|
||
/// <returns>The cooldown duration in seconds, or 0 if invalid.</returns>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retrieves the icon sprite for a specific skill.
|
||
/// </summary>
|
||
/// <param name="skillIndex">The index of the skill in the character's skill array.</param>
|
||
/// <returns>The icon sprite of the skill, or null if invalid.</returns>
|
||
public Sprite GetSkillIcon(int skillIndex)
|
||
{
|
||
if (characterData != null && skillIndex >= 0 && skillIndex < characterData.skills.Length)
|
||
{
|
||
return characterData.skills[skillIndex].icon;
|
||
}
|
||
else
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retrieves the SkillData for a given skill index.
|
||
/// </summary>
|
||
/// <param name="index">The skill index.</param>
|
||
/// <returns>The SkillData object, or null if invalid.</returns>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Instantiates the character model from CharacterData and sets up the animator reference.
|
||
/// </summary>
|
||
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<Animator>();
|
||
if (animator == null)
|
||
{
|
||
Debug.LogError("Animator component not found on the character model.");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Initializes character stats based on the data provided in CharacterData.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies all the stat upgrades that have been accumulated for this character.
|
||
/// </summary>
|
||
private void ApplyStatUpgrades()
|
||
{
|
||
int characterId = GetCharacterData().characterId;
|
||
Dictionary<StatType, int> 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Stops the player's movement for a specified duration.
|
||
/// </summary>
|
||
/// <param name="duration">Duration in seconds to stop movement.</param>
|
||
/// <param name="allowRotation">If true, allows rotation while movement is stopped.</param>
|
||
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
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <param name="direction">The movement direction.</param>
|
||
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
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <param name="index">The index of the skill to use.</param>
|
||
/// <param name="inputDirection">The input direction from a joystick or other source.</param>
|
||
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));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <param name="dashSettings">Advanced dash settings.</param>
|
||
/// <param name="inputDirection">The dash direction input.</param>
|
||
/// <param name="nearestTarget">Nearest target transform (if any).</param>
|
||
/// <param name="skillIndex">Index for the UseSkill animation trigger.</param>
|
||
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");
|
||
}
|
||
/// <summary>
|
||
/// Triggers an action animation for a skill without waiting. Uses PlayActionAnimationOnce on the CharacterModel.
|
||
/// </summary>
|
||
/// <param name="skillIndex">Index of the skill animation trigger.</param>
|
||
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}");
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Coroutine to reset the ability to auto-attack after a delay.
|
||
/// </summary>
|
||
/// <param name="delay">Delay before allowing auto-attack again.</param>
|
||
private IEnumerator ResetAutoAttack(float delay)
|
||
{
|
||
canAutoAttack = false;
|
||
yield return new WaitForSeconds(delay);
|
||
canAutoAttack = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Updates the directional aim's rotation based on the joystick input and updates the corresponding indicator.
|
||
/// </summary>
|
||
/// <param name="joystickDirection">The direction from the skill joystick.</param>
|
||
/// <param name="skillIndex">Index of the skill being aimed.</param>
|
||
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;
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Activates or deactivates the directional aim and manages indicator animations.
|
||
/// </summary>
|
||
/// <param name="isActive">True to activate the directional aim; false to deactivate.</param>
|
||
/// <param name="skillIndex">Index of the skill being used.</param>
|
||
/// <param name="skillUsed">True if the skill was used (triggering cast indicator animation); false if canceled.</param>
|
||
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 <20>position AoE circle<6C> 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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Converts a 2D direction vector to a 3D direction.
|
||
/// </summary>
|
||
/// <param name="direction">The 2D direction vector.</param>
|
||
/// <returns>The 3D direction vector.</returns>
|
||
private Vector3 GetDirection(Vector2 direction)
|
||
{
|
||
return new Vector3(direction.x, 0, direction.y);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Coroutine to launch a skill without waiting for the animation to complete.
|
||
/// Triggers the appropriate animation and, after delayToLaunch, spawns the skill damage.
|
||
/// </summary>
|
||
/// <param name="skill">The skill data.</param>
|
||
/// <param name="direction">The launch direction.</param>
|
||
/// <param name="isAutoAttack">True if this is an auto-attack.</param>
|
||
/// <param name="index">Index used for naming the trigger.</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Coroutine to revert to the base animation after a specified delay.
|
||
/// </summary>
|
||
/// <param name="baseAnim">The base animation key (e.g., "Idle" or "Run").</param>
|
||
/// <param name="delay">Delay in seconds.</param>
|
||
private IEnumerator RevertToBaseAnimation(string baseAnim, float delay)
|
||
{
|
||
yield return new WaitForSeconds(delay);
|
||
if (characterModel != null && characterModel.usePlayableAnimations)
|
||
{
|
||
characterModel.PlayBaseAnimation(baseAnim);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Executes the auto-attack action using the AutoAttack skill from characterData.
|
||
/// </summary>
|
||
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));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Coroutine to rotate the character model towards the attack direction,
|
||
/// ignoring the Y component to prevent vertical rotation.
|
||
/// </summary>
|
||
/// <param name="direction">The direction of the attack.</param>
|
||
/// <param name="skill">The skill data containing rotation duration.</param>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finds the nearest target with the tags "Monster" or "Box".
|
||
/// </summary>
|
||
/// <returns>The transform of the nearest target, or null if no targets are found.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Generates a random direction on the XZ plane.
|
||
/// </summary>
|
||
/// <returns>A normalized Vector3 representing a random direction.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Coroutine to handle the auto-attack based on the cooldown.
|
||
/// </summary>
|
||
private IEnumerator AutoAttackRoutine()
|
||
{
|
||
while (true)
|
||
{
|
||
Attack();
|
||
yield return new WaitForSeconds(GetCurrentAttackSpeed());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Makes the character jump using the character controller.
|
||
/// </summary>
|
||
public void Jump()
|
||
{
|
||
if (characterController != null)
|
||
{
|
||
characterController.Jump();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Alters the movement speed of the character.
|
||
/// </summary>
|
||
/// <param name="newSpeed">The new speed value.</param>
|
||
public void AlterSpeed(float newSpeed)
|
||
{
|
||
if (characterController != null)
|
||
{
|
||
characterController.AlterSpeed(newSpeed);
|
||
currentMoveSpeed = newSpeed;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies a force to the character for pushing.
|
||
/// </summary>
|
||
/// <param name="force">The force to apply.</param>
|
||
public void Push(Vector3 force)
|
||
{
|
||
if (characterController != null)
|
||
{
|
||
characterController.Push(force);
|
||
}
|
||
}
|
||
|
||
public CharacterTypeData GetCharacterType()
|
||
{
|
||
return characterData.characterType;
|
||
}
|
||
|
||
public CharacterData GetCharacterData()
|
||
{
|
||
return characterData;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Receives damage, reducing shield first if available and then HP.
|
||
/// Damage is reduced by current defense but not lower than a global minimum.
|
||
/// </summary>
|
||
/// <param name="damage">The damage to receive.</param>
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sets the character invincible for a duration.
|
||
/// </summary>
|
||
/// <param name="duration">Duration in seconds.</param>
|
||
public IEnumerator Invincible(float duration)
|
||
{
|
||
isInvincible = true;
|
||
yield return new WaitForSeconds(duration);
|
||
isInvincible = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies damage to the target monster considering elemental advantages and weaknesses.
|
||
/// </summary>
|
||
/// <param name="target">The target monster.</param>
|
||
/// <param name="damage">The base amount of damage.</param>
|
||
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}.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Handles the character's death.
|
||
/// </summary>
|
||
private void Die()
|
||
{
|
||
Debug.Log("Character has died.");
|
||
GameplayManager.Singleton.PauseGame();
|
||
UIGameplay.Singleton.DisplayEndGameScreen(false);
|
||
// GameManager.Singleton.EndGame(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies a skill perk and manages its execution based on level and cooldown.
|
||
/// </summary>
|
||
/// <param name="skillPerk">The skill perk to apply.</param>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Coroutine to execute a skill, launching damage entities periodically based on cooldown.
|
||
/// </summary>
|
||
/// <param name="skillPerk">The skill perk data.</param>
|
||
/// <param name="level">The current level of the skill.</param>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Coroutine for multi-shot spawning of damage entities with angle and delay settings.
|
||
/// </summary>
|
||
/// <param name="skillPerk">The skill perk data.</param>
|
||
/// <param name="level">The level of the skill perk.</param>
|
||
/// <param name="baseDirection">Base direction for shots.</param>
|
||
/// <param name="shots">Number of shots.</param>
|
||
/// <param name="angle">Angle between shots.</param>
|
||
/// <param name="delay">Delay between shots.</param>
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Instantiates a damage entity based on the skill level and configuration, aiming it in a specified direction.
|
||
/// </summary>
|
||
/// <param name="skillPerk">The skill perk data.</param>
|
||
/// <param name="level">The level of the skill perk.</param>
|
||
/// <param name="direction">Direction to launch the damage entity.</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies a stat perk to the character, adjusting the relevant stat.
|
||
/// </summary>
|
||
/// <param name="statPerk">The stat perk to apply.</param>
|
||
/// <param name="level">The level of the perk being applied.</param>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies the stats of a specific perk based on its level.
|
||
/// </summary>
|
||
/// <param name="statPerk">The stat perk to apply.</param>
|
||
/// <param name="level">The level of the perk.</param>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Updates the specified stat type of the character by adding the provided value.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the base value for a given stat type.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies the specified stat upgrade to the character based on the upgrade level.
|
||
/// </summary>
|
||
/// <param name="statType">The type of stat to upgrade.</param>
|
||
/// <param name="level">The level of the upgrade to apply.</param>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retrieves the corresponding StatUpgrade for the given StatType.
|
||
/// </summary>
|
||
private StatUpgrade GetStatUpgrade(StatType statType)
|
||
{
|
||
return GetCharacterData().statUpgrades.Find(upgrade => upgrade.statType == statType);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Updates the specified stat type of the character by adding the provided value from a stat upgrade.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retrieves the list of skill perks associated with the character.
|
||
/// </summary>
|
||
public List<SkillPerkData> GetSkillsPerkData()
|
||
{
|
||
return skillsPerkData;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retrieves the list of stat perks associated with the character.
|
||
/// </summary>
|
||
public List<StatPerkData> GetStatsPerkData()
|
||
{
|
||
return statsPerkData;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies the skill's buffs and debuffs to this character (heal, shield, speed buff, etc.).
|
||
/// </summary>
|
||
/// <param name="skill">The skill data containing buff/debuff options.</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Temporarily increases the character's move speed and reverts after duration.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Temporarily increases the character's attack speed and reverts after duration.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Temporarily increases the character's defense and reverts after duration.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Temporarily increases the character's damage and reverts after duration.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Temporarily applies a slow debuff to the user, reducing their move speed by a percentage, and reverts after duration.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Increases the shield of the character.
|
||
/// </summary>
|
||
/// <param name="amount">The amount of shield to increase.</param>
|
||
public void GainShield(int amount)
|
||
{
|
||
if (amount > 0 && (currentShield + amount) <= GetMaxHP())
|
||
{
|
||
currentShield += amount;
|
||
}
|
||
else
|
||
{
|
||
currentShield = GetMaxHP();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Increases the shield of the character for a specified duration.
|
||
/// </summary>
|
||
/// <param name="amount">The amount of shield to increase.</param>
|
||
/// <param name="duration">Duration in seconds for which the shield remains.</param>
|
||
public void GainShield(int amount, float duration)
|
||
{
|
||
if (amount > 0 && (currentShield + amount) <= GetMaxHP())
|
||
{
|
||
currentShield += amount;
|
||
}
|
||
else
|
||
{
|
||
currentShield = GetMaxHP();
|
||
}
|
||
|
||
StartCoroutine(RemoveShieldAfterDuration(amount, duration));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Coroutine to remove the shield increase after the specified duration.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Regenerates HP based on the base HP regeneration rate.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Regenerates MP based on the base MP regeneration rate.
|
||
/// </summary>
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Increases the maximum HP of the character.
|
||
/// </summary>
|
||
/// <param name="amount">Amount to increase max HP by.</param>
|
||
public void GainMaxHP(int amount)
|
||
{
|
||
if (amount > 0)
|
||
{
|
||
currentHP += amount;
|
||
MaxHp += amount;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Heals the character by a specified amount up to their maximum health.
|
||
/// </summary>
|
||
/// <param name="amount">The amount to heal.</param>
|
||
public void Heal(int amount)
|
||
{
|
||
if (amount > 0)
|
||
{
|
||
currentHP = Mathf.Min(currentHP + amount, GetMaxHP());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Heals the character's MP.
|
||
/// </summary>
|
||
/// <param name="amount">The amount to heal.</param>
|
||
public void HealMP(int amount)
|
||
{
|
||
if (amount > 0)
|
||
{
|
||
currentMP = Mathf.Min(currentMP + amount, GetMaxMP());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Applies health leech based on the damage dealt.
|
||
/// </summary>
|
||
/// <param name="damage">The total damage dealt.</param>
|
||
public void ApplyHpLeech(float damage)
|
||
{
|
||
float leechAmount = damage * currentHPLeech;
|
||
currentHP += leechAmount;
|
||
if (currentHP > GetMaxHP())
|
||
{
|
||
currentHP = GetMaxHP();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Increases the character's base damage.
|
||
/// </summary>
|
||
/// <param name="amount">The amount to increase base damage.</param>
|
||
public void GainDamage(float amount)
|
||
{
|
||
if (amount > 0)
|
||
{
|
||
currentDamage += amount;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Increases the attack speed of the character.
|
||
/// </summary>
|
||
/// <param name="amount">The amount to decrease the attack speed timer by.</param>
|
||
public void GainAttackSpeed(float amount)
|
||
{
|
||
if (amount > 0)
|
||
{
|
||
currentAttackSpeed = Mathf.Max(currentAttackSpeed - amount, 0);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Increases the cooldown reduction percentage.
|
||
/// </summary>
|
||
/// <param name="amount">The amount to increase cooldown reduction by.</param>
|
||
public void CooldownReduction(float amount)
|
||
{
|
||
if (amount > 0)
|
||
{
|
||
currentCooldownReduction += amount;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Increases the critical hit rate of the character.
|
||
/// </summary>
|
||
/// <param name="amount">The amount to increase the critical rate by.</param>
|
||
public void GainCriticalRate(float amount)
|
||
{
|
||
if (amount > 0)
|
||
{
|
||
currentCriticalRate += amount;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Increases the movement speed of the character.
|
||
/// </summary>
|
||
/// <param name="amount">The amount to increase movement speed by.</param>
|
||
public void GainMoveSpeed(float amount)
|
||
{
|
||
if (amount > 0)
|
||
{
|
||
currentMoveSpeed += amount;
|
||
if (characterController != null)
|
||
{
|
||
characterController.AlterSpeed(currentMoveSpeed);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Adds XP to the character and handles leveling up.
|
||
/// </summary>
|
||
/// <param name="xpAmount">The amount of XP to add.</param>
|
||
public void AddXP(int xpAmount)
|
||
{
|
||
currentXP += xpAmount;
|
||
|
||
while (currentLevel < GameplayManager.Singleton.maxLevel &&
|
||
currentXP >= GameplayManager.Singleton.xpToNextLevel[currentLevel])
|
||
{
|
||
LevelUp();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Revives the character to full HP and MP and grants temporary invincibility.
|
||
/// </summary>
|
||
public void Revive()
|
||
{
|
||
currentHP += GetMaxHP();
|
||
currentMP += GetMaxMP();
|
||
StartCoroutine(Invincible(2.5f));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Levels up the character and handles perk selection based on the upgrade mode.
|
||
/// </summary>
|
||
private void LevelUp()
|
||
{
|
||
currentLevel++;
|
||
currentXP -= GameplayManager.Singleton.xpToNextLevel[currentLevel - 1];
|
||
|
||
List<object> 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.");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the current level of the character.
|
||
/// </summary>
|
||
public int GetCurrentLevel()
|
||
{
|
||
return currentLevel;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the current XP of the character.
|
||
/// </summary>
|
||
public int GetCurrentXP()
|
||
{
|
||
return currentXP;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the XP required for the next level.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|