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 activeModifiers = new List(); private readonly Dictionary baseValues = new Dictionary(); private readonly Dictionary finalValues = new Dictionary(); 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 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(); } } }