Refactor character attributes system; replace individual attributes with a dictionary-based approach for better scalability and maintainability
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user