[#14] rozbudowa stats + dodanie modyfikatorów

This commit is contained in:
2026-06-30 20:28:51 +02:00
parent 86b4a8a93f
commit 8326c4b279
33 changed files with 444 additions and 129 deletions
@@ -12,57 +12,109 @@ using UnityEngine;
namespace RPGCore.Stats.ObjectModules.StatsObjectModule
{
[Serializable]
[ObjectModule(
name: "[Stats] Statistics",
description: "Attached to any implementation of <b>BaseObject</b>. " +
"Create scriptable object <b>"+nameof(StatValueDefinitionSO)+"</b> to define new stats. " +
"Stats will be automatically created in any matching implementation of <b>BaseObject</b>. " +
"Remember to add all definitions to generated <b>"+nameof(StatsSettings)+"</b>."
)]
public class StatsModule : ObjectModule<BaseObject>
{
[SerializeField]
[SerializableDictionary("Stat Definition", "Base Value", isKeyEditable: false)]
private SerializableDictionary<StatDefinitionSO, int> _serializedStats;
[SerializableDictionary("Value Definition", "Base Value", isKeyEditable: false)]
private SerializableDictionary<StatValueDefinitionSO, int> _serializedValues;
[SerializeField]
[SerializableDictionary("Resource Definition", "Default Value", isKeyEditable: false)]
private SerializableDictionary<StatResourceDefinitionSO, int> _serializedResources;
private Dictionary<StatDefinitionSO, StatValue> _stats = new();
private List<StatDefinitionSO> _queue = new();
private Dictionary<StatValueDefinitionSO, StatValue> _values = new();
private Dictionary<StatResourceDefinitionSO, StatResource> _resources = new();
private List<StatValueDefinitionSO> _queue = new();
#if UNITY_EDITOR
private void OnValidate()
{
FixSerializedStats();
FixSerializedStatValues();
FixSerializedStatResources();
}
private void FixSerializedStats()
private void FixSerializedStatValues()
{
// fixing only when variable is serialized (sometimes OnValidate can be called before deserializing on project open)
if (_serializedStats == null) return;
if (_serializedValues == null) return;
// All stat definitions that should be added to this object...
var correctStats = SettingsManager.Get<StatsSettings>().allStats
.Where(stat => stat)
.Where(stat => stat.attachedTo.type.IsAssignableFrom(parent.GetType()))
// All value definitions that should be added to this object...
var correctValues = SettingsManager.Get<StatsSettings>().allValues
.Where(value => value)
.Where(value => value.attachedTo.type.IsAssignableFrom(parent.GetType()))
.ToList();
//... but also remember that some of them can override other's, overriden ones should not be here
correctStats.Select(stat => stat.overrides)
.Where(overridenStat => overridenStat)
.ForEach(overridenStat => correctStats.Remove(overridenStat));
correctValues.Select(value => value.overrides)
.Where(overridenValue => overridenValue)
.ForEach(overridenValue => correctValues.Remove(overridenValue));
// ADD MISSING
var missingStats = correctStats.Except(_serializedStats.Keys).ToList();
if (missingStats.Any())
var missingValues = correctValues.Except(_serializedValues.Keys).ToList();
if (missingValues.Any())
{
missingStats.ForEach(missingStat => _serializedStats.Add(missingStat, 0));
var missingStatsString = string.Join(", ", missingStats.Select(s => s.name));
Debug.LogWarning($"[StatsModule] automatically adding missing stats: {missingStatsString}", parent);
missingValues.ForEach(missingValue => _serializedValues.Add(missingValue, 0));
var missingValuesString = string.Join(", ", missingValues.Select(s => s.name));
Debug.LogWarning($"[StatsModule] automatically adding missing stats: {missingValuesString}", parent);
UnityEditor.EditorUtility.SetDirty(parent.gameObject);
}
// REMOVE INVALID
var invalidStats = _serializedStats.Keys.Except(correctStats).ToList();
if (invalidStats.Any())
var invalidValues = _serializedValues.Keys.Except(correctValues).ToList();
if (invalidValues.Any())
{
invalidStats.ForEach(invalidStat => _serializedStats.Remove(invalidStat));
var invalidStatsString = string.Join(", ", invalidStats.Select(missingStat => missingStat.name));
Debug.LogWarning($"[StatsModule] automatically removing invalid stats: {invalidStatsString}", parent);
invalidValues.ForEach(invalidValue => _serializedValues.Remove(invalidValue));
var invalidValuesString = string.Join(", ", invalidValues.Select(missingValue => missingValue.name));
Debug.LogWarning($"[StatsModule] automatically removing invalid values: {invalidValuesString}", parent);
UnityEditor.EditorUtility.SetDirty(parent.gameObject);
}
}
private void FixSerializedStatResources()
{
// fixing only when variable is serialized (sometimes OnValidate can be called before deserializing on project open)
if (_serializedResources == null) return;
// All stat definitions that should be added to this object...
var correctResources = SettingsManager.Get<StatsSettings>().allResources
.Where(resource => resource)
.Where(resource => resource.valueDefinitionSO.attachedTo.type.IsAssignableFrom(parent.GetType()))
.ToList();
//... but also remember that some of them can override other's, overriden ones should not be here
var baseValues = SettingsManager.Get<StatsSettings>().allValues
.Where(value => value)
.Where(value => !value.overrides)
.ToList();
correctResources
.Where(resource => !baseValues.Contains(resource.valueDefinitionSO))
.ForEach(resource => correctResources.Remove(resource));
// ADD MISSING
var missingResources = correctResources.Except(_serializedResources.Keys).ToList();
if (missingResources.Any())
{
missingResources.ForEach(missingResource => _serializedResources.Add(missingResource, 0));
var missingResourcesString = string.Join(", ", missingResources.Select(s => s.name));
Debug.LogWarning($"[StatsModule] automatically adding missing resources: {missingResourcesString}", parent);
UnityEditor.EditorUtility.SetDirty(parent.gameObject);
}
// REMOVE INVALID
var invalidResources = _serializedResources.Keys.Except(correctResources).ToList();
if (invalidResources.Any())
{
invalidResources.ForEach(invalidResource => _serializedResources.Remove(invalidResource));
var invalidResourcesString = string.Join(", ", invalidResources.Select(missingStat => missingStat.name));
Debug.LogWarning($"[StatsModule] automatically removing invalid resources: {invalidResourcesString}", parent);
UnityEditor.EditorUtility.SetDirty(parent.gameObject);
}
}
@@ -71,19 +123,20 @@ namespace RPGCore.Stats.ObjectModules.StatsObjectModule
[DynamicValueProvider]
internal StatValue StatValueProvider(string name)
{
return _stats.GetValueOrDefault(_stats.Keys.FirstOrDefault(def => def.name == name));
return _values.GetValueOrDefault(_values.Keys.FirstOrDefault(def => def.name == name));
}
private void Awake()
{
// Create instances of all stats that will be used runtime
CreateStats();
CreateResources();
}
private void Start()
{
// First stats refresh
_stats.Keys.ForEach(AddToQueueRefresh);
_values.Keys.ForEach(AddToQueueRefresh);
QueueRefresh();
}
@@ -93,32 +146,55 @@ namespace RPGCore.Stats.ObjectModules.StatsObjectModule
}
/// <summary>
/// Really important part - we create stats for runtime usage.
/// <see cref="StatDefinitionSO"/> can also override another one, in that case <see cref="StatValue"/>
/// uses main definition, but it is visible as that overriden one.
/// Really important part - we create values for runtime usage.
/// </summary>
private void CreateStats()
{
_serializedStats.DictForEach((statDefinition, baseValue) =>
_serializedValues.DictForEach((definition, baseValue) =>
{
var baseDefinition = statDefinition.GetBaseDefinition();
var statValue = new StatValue(statDefinition, baseValue, parent);
statValue.sourceContext.SetSource("object", parent);
_stats.Add(baseDefinition, statValue);
var baseDefinition = definition.GetBaseDefinition();
var value = new StatValue(definition, baseValue, parent);
value.sources.SetSource("object", parent);
value.sources.SetSource("stats", this);
_values.Add(baseDefinition, value);
});
}
/// <summary>
/// Really important part - we create resources for runtime usage.
/// </summary>
private void CreateResources()
{
_serializedResources.DictForEach((definition, startingResource) =>
{
_resources.Add(
definition,
new StatResource(definition, startingResource, Get(definition.valueDefinitionSO))
);
});
}
/// <summary>
/// Returns runtime values for given stat definition.
/// </summary>
/// <param name="statDefinitionSO">Runtime stats will be found by this definition (or overriden definition if given)</param>
/// <returns>Stat values (baseValue, value, resource)</returns>
public StatValue Get(StatDefinitionSO statDefinitionSO)
/// <param name="valueDefinitionSO">Runtime stats will be found by this definition (or overriden definition if given)</param>
/// <returns>Stat value (baseValue, value)</returns>
public StatValue Get(StatValueDefinitionSO valueDefinitionSO)
{
#if UNITY_EDITOR
if (!UnityEditor.EditorApplication.isPlaying) CreateStats();
#endif
return _stats.GetValueOrDefault(statDefinitionSO.GetBaseDefinition());
return _values.GetValueOrDefault(valueDefinitionSO.GetBaseDefinition());
}
/// <inheritdoc cref="Get(StatValueDefinitionSO)"/>
/// <returns>Stat resource (baseValue, value, resource)</returns>
public StatResource Get(StatResourceDefinitionSO resourceDefinitionSO)
{
#if UNITY_EDITOR
if (!UnityEditor.EditorApplication.isPlaying) CreateResources();
#endif
return _resources.GetValueOrDefault(resourceDefinitionSO);
}
/// <summary>
@@ -136,9 +212,9 @@ namespace RPGCore.Stats.ObjectModules.StatsObjectModule
}
/// <summary>
/// Select <see cref="StatDefinitionSO"/> along with its dependencies to refresh next frame.
/// Select <see cref="StatValueDefinitionSO"/> along with its dependencies to refresh next frame.
/// </summary>
public void AddToQueueRefresh(StatDefinitionSO statDefinitionSO)
public void AddToQueueRefresh(StatValueDefinitionSO statDefinitionSO)
{
statDefinitionSO = statDefinitionSO.GetBaseDefinition();
@@ -146,7 +222,7 @@ namespace RPGCore.Stats.ObjectModules.StatsObjectModule
if (!_queue.Contains(statDefinitionSO)) _queue.Add(statDefinitionSO);
// All definitions that uses this one as dependency should be refreshed too
_stats.Keys
_values.Keys
.Where(otherStatDefinition => otherStatDefinition.dependencies.Contains(statDefinitionSO))
.ForEach(AddToQueueRefresh);
}