326 lines
10 KiB
C#
326 lines
10 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using OTGIntegrated.Core;
|
||
|
|
using UnityEngine;
|
||
|
|
|
||
|
|
namespace OTGIntegrated.Stats
|
||
|
|
{
|
||
|
|
public sealed class PlayerStats : MonoBehaviour
|
||
|
|
{
|
||
|
|
[Header("Base Values")]
|
||
|
|
[SerializeField] private float baseMaxHealth = 100f;
|
||
|
|
[SerializeField] private float baseMaxMana = 100f;
|
||
|
|
[SerializeField] private float baseDamage = 10f;
|
||
|
|
[SerializeField] private float baseMoveSpeed = 5f;
|
||
|
|
[SerializeField] private float baseCriticalChance = 0f;
|
||
|
|
[SerializeField] private float baseCooldownReduction = 0f;
|
||
|
|
|
||
|
|
[Header("Runtime")]
|
||
|
|
[SerializeField] private float currentHealth = 100f;
|
||
|
|
[SerializeField] private float currentMana = 100f;
|
||
|
|
[SerializeField] private List<StatModifier> activeModifiers = new List<StatModifier>();
|
||
|
|
|
||
|
|
private readonly Dictionary<StatType, float> baseValues = new Dictionary<StatType, float>();
|
||
|
|
private readonly Dictionary<StatType, float> finalValues = new Dictionary<StatType, float>();
|
||
|
|
|
||
|
|
public event Action OnStatsChanged;
|
||
|
|
|
||
|
|
public float MaxHealth => GetStat(StatType.MaxHealth);
|
||
|
|
public float CurrentHealth => currentHealth;
|
||
|
|
public float MaxMana => GetStat(StatType.MaxMana);
|
||
|
|
public float CurrentMana => currentMana;
|
||
|
|
public float Damage => GetStat(StatType.Damage);
|
||
|
|
public float MoveSpeed => GetStat(StatType.MoveSpeed);
|
||
|
|
public float CriticalChance => GetStat(StatType.CriticalChance);
|
||
|
|
public float CooldownReduction => GetStat(StatType.CooldownReduction);
|
||
|
|
public bool IsAlive => currentHealth > 0f;
|
||
|
|
|
||
|
|
private void Awake()
|
||
|
|
{
|
||
|
|
RecalculateStats(false);
|
||
|
|
currentHealth = Mathf.Clamp(currentHealth <= 0f ? MaxHealth : currentHealth, 0f, MaxHealth);
|
||
|
|
currentMana = Mathf.Clamp(currentMana <= 0f ? MaxMana : currentMana, 0f, MaxMana);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnValidate()
|
||
|
|
{
|
||
|
|
ClampBaseValues();
|
||
|
|
currentHealth = Mathf.Max(0f, currentHealth);
|
||
|
|
currentMana = Mathf.Max(0f, currentMana);
|
||
|
|
RecalculateStats(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
public IReadOnlyList<StatModifier> GetActiveModifiers()
|
||
|
|
{
|
||
|
|
return activeModifiers;
|
||
|
|
}
|
||
|
|
|
||
|
|
public float GetBaseStat(StatType statType)
|
||
|
|
{
|
||
|
|
EnsureBaseValues();
|
||
|
|
return baseValues.TryGetValue(statType, out float value) ? value : 0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
public float GetStat(StatType statType)
|
||
|
|
{
|
||
|
|
if (finalValues.Count == 0)
|
||
|
|
{
|
||
|
|
RecalculateStats(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
return finalValues.TryGetValue(statType, out float value) ? value : GetBaseStat(statType);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void AddModifier(StatModifier modifier)
|
||
|
|
{
|
||
|
|
if (!IsValidNumber(modifier.value))
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (string.IsNullOrWhiteSpace(modifier.sourceId))
|
||
|
|
{
|
||
|
|
modifier.sourceId = "Runtime";
|
||
|
|
}
|
||
|
|
|
||
|
|
activeModifiers.Add(modifier);
|
||
|
|
RecalculateStats();
|
||
|
|
}
|
||
|
|
|
||
|
|
public int RemoveModifiersBySource(string sourceId)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(sourceId))
|
||
|
|
{
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int removed = activeModifiers.RemoveAll(modifier => modifier.sourceId == sourceId);
|
||
|
|
if (removed > 0)
|
||
|
|
{
|
||
|
|
RecalculateStats();
|
||
|
|
}
|
||
|
|
|
||
|
|
return removed;
|
||
|
|
}
|
||
|
|
|
||
|
|
public int RemoveModifiersBySourcePrefix(string sourcePrefix)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(sourcePrefix))
|
||
|
|
{
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int removed = activeModifiers.RemoveAll(modifier =>
|
||
|
|
!string.IsNullOrEmpty(modifier.sourceId) &&
|
||
|
|
modifier.sourceId.StartsWith(sourcePrefix, StringComparison.Ordinal));
|
||
|
|
|
||
|
|
if (removed > 0)
|
||
|
|
{
|
||
|
|
RecalculateStats();
|
||
|
|
}
|
||
|
|
|
||
|
|
return removed;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void ClearModifiers()
|
||
|
|
{
|
||
|
|
if (activeModifiers.Count == 0)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
activeModifiers.Clear();
|
||
|
|
RecalculateStats();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void RecalculateStats(bool notify = true)
|
||
|
|
{
|
||
|
|
float previousMaxHealth = finalValues.TryGetValue(StatType.MaxHealth, out float oldHealth) ? oldHealth : baseMaxHealth;
|
||
|
|
float previousMaxMana = finalValues.TryGetValue(StatType.MaxMana, out float oldMana) ? oldMana : baseMaxMana;
|
||
|
|
|
||
|
|
ClampBaseValues();
|
||
|
|
EnsureBaseValues();
|
||
|
|
finalValues.Clear();
|
||
|
|
|
||
|
|
foreach (StatType statType in Enum.GetValues(typeof(StatType)))
|
||
|
|
{
|
||
|
|
float flatAdd = 0f;
|
||
|
|
float percentAdd = 0f;
|
||
|
|
float percentMultiply = 1f;
|
||
|
|
|
||
|
|
for (int i = 0; i < activeModifiers.Count; i++)
|
||
|
|
{
|
||
|
|
StatModifier modifier = activeModifiers[i];
|
||
|
|
if (modifier.statType != statType || !IsValidNumber(modifier.value))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (modifier.modifierType)
|
||
|
|
{
|
||
|
|
case ModifierType.FlatAdd:
|
||
|
|
flatAdd += modifier.value;
|
||
|
|
break;
|
||
|
|
case ModifierType.PercentAdd:
|
||
|
|
percentAdd += modifier.value;
|
||
|
|
break;
|
||
|
|
case ModifierType.PercentMultiply:
|
||
|
|
percentMultiply *= Mathf.Max(0f, 1f + modifier.value);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
float value = (GetBaseStat(statType) + flatAdd) * Mathf.Max(0f, 1f + percentAdd) * percentMultiply;
|
||
|
|
finalValues[statType] = ClampFinalValue(statType, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (previousMaxHealth > 0f && MaxHealth > previousMaxHealth)
|
||
|
|
{
|
||
|
|
currentHealth += MaxHealth - previousMaxHealth;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (previousMaxMana > 0f && MaxMana > previousMaxMana)
|
||
|
|
{
|
||
|
|
currentMana += MaxMana - previousMaxMana;
|
||
|
|
}
|
||
|
|
|
||
|
|
ClampVitals();
|
||
|
|
|
||
|
|
if (notify)
|
||
|
|
{
|
||
|
|
OnStatsChanged?.Invoke();
|
||
|
|
GameEvents.RaiseStatsChanged();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void TakeDamage(float amount)
|
||
|
|
{
|
||
|
|
if (amount <= 0f)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
currentHealth = Mathf.Clamp(currentHealth - amount, 0f, MaxHealth);
|
||
|
|
RaiseChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Heal(float amount)
|
||
|
|
{
|
||
|
|
if (amount <= 0f)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
currentHealth = Mathf.Clamp(currentHealth + amount, 0f, MaxHealth);
|
||
|
|
RaiseChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool SpendMana(float amount)
|
||
|
|
{
|
||
|
|
if (amount <= 0f)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (currentMana + 0.001f < amount)
|
||
|
|
{
|
||
|
|
GameEvents.RaiseNotification("Недостаточно маны");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
currentMana = Mathf.Clamp(currentMana - amount, 0f, MaxMana);
|
||
|
|
RaiseChanged();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void RestoreMana(float amount)
|
||
|
|
{
|
||
|
|
if (amount <= 0f)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
currentMana = Mathf.Clamp(currentMana + amount, 0f, MaxMana);
|
||
|
|
RaiseChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
public float GetNormalizedHealth()
|
||
|
|
{
|
||
|
|
return MaxHealth <= 0f ? 0f : Mathf.Clamp01(currentHealth / MaxHealth);
|
||
|
|
}
|
||
|
|
|
||
|
|
public float GetNormalizedMana()
|
||
|
|
{
|
||
|
|
return MaxMana <= 0f ? 0f : Mathf.Clamp01(currentMana / MaxMana);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SetRuntimeVitals(float health, float mana)
|
||
|
|
{
|
||
|
|
currentHealth = Mathf.Clamp(health, 0f, MaxHealth);
|
||
|
|
currentMana = Mathf.Clamp(mana, 0f, MaxMana);
|
||
|
|
RaiseChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void EnsureBaseValues()
|
||
|
|
{
|
||
|
|
baseValues[StatType.MaxHealth] = Mathf.Max(1f, baseMaxHealth);
|
||
|
|
baseValues[StatType.MaxMana] = Mathf.Max(0f, baseMaxMana);
|
||
|
|
baseValues[StatType.Damage] = Mathf.Max(0f, baseDamage);
|
||
|
|
baseValues[StatType.MoveSpeed] = Mathf.Max(0.1f, baseMoveSpeed);
|
||
|
|
baseValues[StatType.CriticalChance] = Mathf.Clamp01(baseCriticalChance);
|
||
|
|
baseValues[StatType.CooldownReduction] = Mathf.Clamp(baseCooldownReduction, 0f, 0.9f);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ClampBaseValues()
|
||
|
|
{
|
||
|
|
baseMaxHealth = Mathf.Max(1f, baseMaxHealth);
|
||
|
|
baseMaxMana = Mathf.Max(0f, baseMaxMana);
|
||
|
|
baseDamage = Mathf.Max(0f, baseDamage);
|
||
|
|
baseMoveSpeed = Mathf.Max(0.1f, baseMoveSpeed);
|
||
|
|
baseCriticalChance = Mathf.Clamp01(baseCriticalChance);
|
||
|
|
baseCooldownReduction = Mathf.Clamp(baseCooldownReduction, 0f, 0.9f);
|
||
|
|
}
|
||
|
|
|
||
|
|
private float ClampFinalValue(StatType statType, float value)
|
||
|
|
{
|
||
|
|
if (!IsValidNumber(value))
|
||
|
|
{
|
||
|
|
value = GetBaseStat(statType);
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (statType)
|
||
|
|
{
|
||
|
|
case StatType.MaxHealth:
|
||
|
|
return Mathf.Max(1f, value);
|
||
|
|
case StatType.MaxMana:
|
||
|
|
case StatType.Damage:
|
||
|
|
return Mathf.Max(0f, value);
|
||
|
|
case StatType.MoveSpeed:
|
||
|
|
return Mathf.Max(0.1f, value);
|
||
|
|
case StatType.CriticalChance:
|
||
|
|
case StatType.CooldownReduction:
|
||
|
|
return Mathf.Clamp(value, 0f, 0.9f);
|
||
|
|
default:
|
||
|
|
return Mathf.Max(0f, value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ClampVitals()
|
||
|
|
{
|
||
|
|
currentHealth = Mathf.Clamp(currentHealth, 0f, MaxHealth);
|
||
|
|
currentMana = Mathf.Clamp(currentMana, 0f, MaxMana);
|
||
|
|
}
|
||
|
|
|
||
|
|
private bool IsValidNumber(float value)
|
||
|
|
{
|
||
|
|
return !float.IsNaN(value) && !float.IsInfinity(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RaiseChanged()
|
||
|
|
{
|
||
|
|
OnStatsChanged?.Invoke();
|
||
|
|
GameEvents.RaiseStatsChanged();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|