2263 lines
85 KiB
C#
Raw Normal View History

2025-09-19 14:56:58 +05:00
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;
}
}