Refactor character attributes system; replace individual attributes with a dictionary-based approach for better scalability and maintainability

This commit is contained in:
2025-08-02 06:06:51 +02:00
parent 93cbc4a3e5
commit 3871cba753
21 changed files with 505 additions and 417 deletions

View File

@@ -1,348 +1,113 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Sirenix.OdinInspector;
using Sirenix.Serialization;
using UnityEngine;
namespace Data
{
public enum Stat
{
Health,
MaxHealth,
MoveSpeed,
Luck,
Armor,
Damage,
RangedDamage,
MeleeDamage,
AttackRange,
AttackSpeed
}
[Serializable]
public class CharacterAttributes
{
[OdinSerialize] public float health = 100f;
[OdinSerialize] public float maxHealth = 100f;
[OdinSerialize] public float moveSpeed = 5f;
[OdinSerialize] public float luck = 0f;
[OdinSerialize] public float armor = 0f;
[OdinSerialize] public int level = 1;
[OdinSerialize] public int experience = 0;
[OdinSerialize] public int baseExperienceToLevelUp = 100;
private float lastLevel = 1f;
[OdinSerialize, PropertyTooltip("This is damage multiplier")]
public float damage = 1f;
[OdinSerialize, PropertyTooltip("This is damage multiplier for ranged attacks")]
public float rangedDamage = 1f;
[OdinSerialize, PropertyTooltip("This is damage multiplier for melee attacks")]
public float meleeDamage = 1f;
[OdinSerialize] public float attackRange = 16f;
[OdinSerialize] public float attackSpeed = 1f;
public event Action<float> OnHealthChanged;
public event Action<float> OnMaxHealthChanged;
public event Action<float> OnMoveSpeedChanged;
public event Action<float> OnLuckChanged;
public event Action<float> OnArmorChanged;
public event Action<float> OnDamageChanged;
public event Action<float> OnRangedDamageChanged;
public event Action<float> OnMeleeDamageChanged;
public event Action<float> OnAttackRangeChanged;
public event Action<float> OnAttackSpeedChanged;
[OdinSerialize, DictionaryDrawerSettings(KeyLabel = "Stat", ValueLabel = "Value", DisplayMode = DictionaryDisplayOptions.OneLine)]
private Dictionary<Attribute, AttributeData<float>> attributes = new();
public event Action<int> OnExperienceChanged;
public event Action<int> OnLevelChanged;
public event Action OnLevelUp;
public float Health
public CharacterAttributes()
{
get => health;
private set
foreach (Attribute attr in Enum.GetValues(typeof(Attribute)))
{
if (Math.Abs(health - value) < float.Epsilon) return;
health = value;
OnHealthChanged?.Invoke(health);
}
}
public float MaxHealth
{
get => maxHealth;
private set
{
if (Math.Abs(maxHealth - value) < float.Epsilon) return;
maxHealth = value;
OnMaxHealthChanged?.Invoke(maxHealth);
}
}
public float MoveSpeed
{
get => moveSpeed;
private set
{
if (Math.Abs(moveSpeed - value) < float.Epsilon) return;
moveSpeed = value;
OnMoveSpeedChanged?.Invoke(moveSpeed);
}
}
public float Luck
{
get => luck;
private set
{
if (Math.Abs(luck - value) < float.Epsilon) return;
luck = value;
OnLuckChanged?.Invoke(luck);
}
}
public float Armor
{
get => armor;
private set
{
if (Math.Abs(armor - value) < float.Epsilon) return;
armor = value;
OnArmorChanged?.Invoke(armor);
}
}
public float Damage
{
get => damage;
private set
{
if (Math.Abs(damage - value) < float.Epsilon) return;
damage = value;
OnDamageChanged?.Invoke(damage);
}
}
public float RangedDamage
{
get => rangedDamage;
private set
{
if (Math.Abs(rangedDamage - value) < float.Epsilon) return;
rangedDamage = value;
OnRangedDamageChanged?.Invoke(rangedDamage);
}
}
public float MeleeDamage
{
get => meleeDamage;
private set
{
if (Math.Abs(meleeDamage - value) < float.Epsilon) return;
meleeDamage = value;
OnMeleeDamageChanged?.Invoke(meleeDamage);
}
}
public float AttackRange
{
get => attackRange;
private set
{
if (Math.Abs(attackRange - value) < float.Epsilon) return;
attackRange = value;
OnAttackRangeChanged?.Invoke(attackRange);
}
}
public float AttackSpeed
{
get => attackSpeed;
private set
{
if (Math.Abs(attackSpeed - value) < float.Epsilon) return;
attackSpeed = value;
OnAttackSpeedChanged?.Invoke(attackSpeed);
if (!attributes.ContainsKey(attr))
attributes[attr] = new AttributeData<float>();
}
}
public int Experience
{
get => experience;
private set
{
if (experience == value) return;
experience = value;
OnExperienceChanged?.Invoke(experience);
if (experience >= ExperienceToNextLevel())
{
Level++;
experience -= ExperienceToNextLevel();
experience = Math.Min(0, experience);
}
else if (experience < 0)
{
experience = 0;
}
}
}
public float Get(Attribute attr) => attributes[attr].Value;
public int Level
{
get => level;
private set
{
if (level == value) return;
level = value;
OnLevelChanged?.Invoke(level);
OnLevelUp?.Invoke();
}
}
public void SetHealth(float value)
{
Health = Math.Clamp(value, 0, MaxHealth);
}
public void SetMaxHealth(float value)
{
MaxHealth = Math.Max(value, 0);
if (Health > MaxHealth)
{
Health = MaxHealth;
}
}
public void SetMoveSpeed(float value)
{
MoveSpeed = Math.Max(value, 0);
}
public void SetLuck(float value)
{
Luck = Math.Max(value, 0);
}
public void SetArmor(float value)
{
Armor = Math.Max(value, 0);
}
public void SetDamage(float value)
{
Damage = Math.Max(value, 0);
}
public void SetRangedDamage(float value)
{
RangedDamage = Math.Max(value, 0);
}
public void SetMeleeDamage(float value)
{
MeleeDamage = Math.Max(value, 0);
}
public void SetAttackRange(float value)
{
AttackRange = Math.Max(value, 0);
}
public void SetAttackSpeed(float value)
{
AttackSpeed = Math.Max(value, 0);
}
public void SetExperience(int value)
{
Experience = Math.Max(value, 0);
}
public void Set(Attribute attr, float value) => attributes[attr].Set(value);
public void SetLevel(int value)
{
Level = Math.Max(value, 1);
}
public void Modify(Attribute attr, float delta) => attributes[attr].Set(attributes[attr].Value + delta);
public void ModifyHealth(float delta)
{
SetHealth(Health + delta);
}
public void ModifyMaxHealth(float delta)
{
SetMaxHealth(MaxHealth + delta);
}
public void ModifyMoveSpeed(float delta)
{
SetMoveSpeed(MoveSpeed + delta);
}
public void ModifyLuck(float delta)
{
SetLuck(Luck + delta);
}
public void ModifyArmor(float delta)
{
SetArmor(Armor + delta);
}
public void ModifyDamage(float delta)
{
SetDamage(Damage + delta);
}
public void ModifyRangedDamage(float delta)
{
SetRangedDamage(RangedDamage + delta);
}
public void ModifyMeleeDamage(float delta)
{
SetMeleeDamage(MeleeDamage + delta);
}
public void ModifyAttackRange(float delta)
{
SetAttackRange(AttackRange + delta);
}
public void ModifyAttackSpeed(float delta)
{
SetAttackSpeed(AttackSpeed + delta);
}
public void Subscribe(Attribute attr, Action<float> listener) => attributes[attr].OnChanged += listener;
public void ModifyExperience(int delta)
{
SetExperience(Experience + delta);
}
public void ModifyLevel(int delta)
{
SetLevel(Level + delta);
}
public void Unsubscribe(Attribute attr, Action<float> listener) => attributes[attr].OnChanged -= listener;
public void Reset()
{
Health = MaxHealth = 100f;
MoveSpeed = 5f;
Luck = 0f;
Armor = 0f;
Damage = 1f;
RangedDamage = 1f;
MeleeDamage = 1f;
AttackRange = 16f;
AttackSpeed = 1f;
Level = 1;
Experience = 0;
Set(Attribute.Health, Get(Attribute.MaxHealth));
Set(Attribute.MoveSpeed, 5f);
Set(Attribute.Luck, 0f);
Set(Attribute.Armor, 0f);
Set(Attribute.Damage, 1f);
Set(Attribute.RangedDamage, 1f);
Set(Attribute.MeleeDamage, 1f);
Set(Attribute.AttackRange, 1f);
Set(Attribute.AttackSpeed, 1f);
Set(Attribute.Level, 1f);
Set(Attribute.Experience, 0f);
Set(Attribute.BaseExperienceToNextLevel, 100f);
}
/*
* health: 10
maxHealth: 10
moveSpeed: 2
luck: 0
armor: 0
level: 1
experience: 0
baseExperienceToLevelUp: 100
damage: 1
rangedDamage: 1
meleeDamage: 1
attackRange: 1
attackSpeed: 1
basic enemy btw
*/
public void Init()
{
foreach (Attribute attr in Enum.GetValues(typeof(Attribute)))
{
if (!attributes.ContainsKey(attr))
attributes[attr] = new AttributeData<float>();
}
lastLevel = Get(Attribute.Level);
Subscribe(Attribute.Experience, OnExperienceChanged);
}
private int ExperienceToNextLevel()
{
return (int)(baseExperienceToLevelUp * Math.Pow(Level, 2));
return (int)(Get(Attribute.BaseExperienceToNextLevel) * Math.Pow(Get(Attribute.Level), 2));
}
private void OnExperienceChanged(float newExp)
{
var xpToNext = ExperienceToNextLevel();
if (newExp >= xpToNext)
{
Modify(Attribute.Level, 1f);
Set(Attribute.Experience, newExp - xpToNext);
}
else if (newExp < 0)
{
Set(Attribute.Experience, 0f);
}
var currentLevel = Get(Attribute.Level);
if (currentLevel > lastLevel)
{
lastLevel = currentLevel;
OnLevelUp?.Invoke();
}
}
}
}