first commit

This commit is contained in:
2026-06-04 23:22:13 +03:00
commit d329f501be
310 changed files with 36395 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
using System;
using OTGIntegrated.Core;
using OTGIntegrated.Talents;
using UnityEngine;
namespace OTGIntegrated.Stats
{
public sealed class LevelSystem : MonoBehaviour
{
[Header("Progression")]
public int level = 1;
public int currentExp = 0;
public int expToNextLevel = 100;
private TalentManager talentManager;
public event Action OnExperienceChanged;
public event Action OnLevelChanged;
public void Initialize(TalentManager talentManager)
{
this.talentManager = talentManager;
level = Mathf.Max(1, level);
currentExp = Mathf.Max(0, currentExp);
expToNextLevel = Mathf.Max(1, expToNextLevel);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.X))
{
AddExperience(50);
}
}
public void AddExperience(int amount)
{
if (amount <= 0)
{
return;
}
currentExp += amount;
GameEvents.RaiseExperienceGained(amount);
GameEvents.RaiseNotification($"Получено опыта: {amount}");
while (currentExp >= expToNextLevel)
{
currentExp -= expToNextLevel;
LevelUp();
}
OnExperienceChanged?.Invoke();
}
public void LevelUp()
{
level = Mathf.Max(1, level + 1);
expToNextLevel = Mathf.Max(1, Mathf.RoundToInt(expToNextLevel * 1.25f + 25f));
if (talentManager != null)
{
talentManager.AddTalentPoints(1);
}
GameEvents.RaiseLevelUp(level);
GameEvents.RaiseNotification($"Уровень повышен: {level}");
OnLevelChanged?.Invoke();
}
public float GetNormalizedExp()
{
return expToNextLevel <= 0 ? 0f : Mathf.Clamp01((float)currentExp / expToNextLevel);
}
public void SetProgress(int savedLevel, int savedExp, int savedExpToNext)
{
level = Mathf.Max(1, savedLevel);
currentExp = Mathf.Max(0, savedExp);
expToNextLevel = Mathf.Max(1, savedExpToNext);
OnExperienceChanged?.Invoke();
OnLevelChanged?.Invoke();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ebb4a0bf744265069f337938f0ae255
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -0,0 +1,9 @@
namespace OTGIntegrated.Stats
{
public enum ModifierType
{
FlatAdd,
PercentAdd,
PercentMultiply
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2240c0c0f7a697db9db9b7747af0b75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+325
View File
@@ -0,0 +1,325 @@
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();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3bb33a590d7a867f38d838d4dddb03d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+21
View File
@@ -0,0 +1,21 @@
using System;
namespace OTGIntegrated.Stats
{
[Serializable]
public struct StatModifier
{
public StatType statType;
public ModifierType modifierType;
public float value;
public string sourceId;
public StatModifier(StatType statType, ModifierType modifierType, float value, string sourceId = "")
{
this.statType = statType;
this.modifierType = modifierType;
this.value = value;
this.sourceId = sourceId;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6372c114a00623a6986e92fdcdf85ae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+12
View File
@@ -0,0 +1,12 @@
namespace OTGIntegrated.Stats
{
public enum StatType
{
MaxHealth,
MaxMana,
Damage,
MoveSpeed,
CriticalChance,
CooldownReduction
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c29308e24c83bb7ab58c6a7fe9a7f75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: