Files
2026-04-25 23:37:10 +02:00

154 lines
6.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using RPGCore.Core;
using RPGCore.Core.Objects;
using RPGCoreCommon.DynamicValues;
using RPGCoreCommon.Helpers;
using RPGCoreCommon.Helpers.CustomTypes;
using RPGCoreCommon.Settings;
using UnityEngine;
namespace RPGCore.Stats.ObjectModules.StatsObjectModule
{
[Serializable]
public class StatsModule : ObjectModule<BaseObject>
{
[SerializeField]
[SerializableDictionary("Stat Definition", "Base Value", isKeyEditable: false)]
private SerializableDictionary<StatDefinitionSO, int> _serializedStats;
private Dictionary<StatDefinitionSO, StatValue> _stats = new();
private List<StatDefinitionSO> _queue = new();
#if UNITY_EDITOR
private void OnValidate()
{
FixSerializedStats();
}
private void FixSerializedStats()
{
// fixing only when variable is serialized (sometimes OnValidate can be called before deserializing on project open)
if (_serializedStats == 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()))
.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));
// ADD MISSING
var missingStats = correctStats.Except(_serializedStats.Keys).ToList();
if (missingStats.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);
UnityEditor.EditorUtility.SetDirty(parent.gameObject);
}
// REMOVE INVALID
var invalidStats = _serializedStats.Keys.Except(correctStats).ToList();
if (invalidStats.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);
UnityEditor.EditorUtility.SetDirty(parent.gameObject);
}
}
#endif
[DynamicValueProvider]
internal StatValue StatValueProvider(string name)
{
return _stats.GetValueOrDefault(_stats.Keys.FirstOrDefault(def => def.name == name));
}
private void Awake()
{
// Create instances of all stats that will be used runtime
CreateStats();
}
private void Start()
{
// First stats refresh
_stats.Keys.ForEach(AddToQueueRefresh);
QueueRefresh();
}
private void FixedUpdate()
{
if (_queue.Count > 0) QueueRefresh();
}
/// <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.
/// </summary>
private void CreateStats()
{
_serializedStats.DictForEach((statDefinition, baseValue) =>
{
var baseDefinition = statDefinition.GetBaseDefinition();
var statValue = new StatValue(statDefinition, baseValue, parent);
statValue.sourceContext.SetSource("object", parent);
_stats.Add(baseDefinition, statValue);
});
}
/// <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)
{
#if UNITY_EDITOR
if (!UnityEditor.EditorApplication.isPlaying) CreateStats();
#endif
return _stats.GetValueOrDefault(statDefinitionSO.GetBaseDefinition());
}
/// <summary>
/// Refreshing stats, every stat only once even if queued multiple times.
/// To ensure that dependencies should be refreshed before definition that is using it.
/// </summary>
private void QueueRefresh()
{
_queue
.Select(def => Get(def).definition)
.Sort((d1, d2) => d2.dependencies.Contains(d1) ? - 1 : d1.dependencies.Contains(d2) ? 1 : 0)
.Select(Get)
.ForEach(stat => stat.Refresh());
_queue.Clear();
}
/// <summary>
/// Select <see cref="StatDefinitionSO"/> along with its dependencies to refresh next frame.
/// </summary>
public void AddToQueueRefresh(StatDefinitionSO statDefinitionSO)
{
statDefinitionSO = statDefinitionSO.GetBaseDefinition();
// Add definition to refresh queue
if (!_queue.Contains(statDefinitionSO)) _queue.Add(statDefinitionSO);
// All definitions that uses this one as dependency should be refreshed too
_stats.Keys
.Where(otherStatDefinition => otherStatDefinition.dependencies.Contains(statDefinitionSO))
.ForEach(AddToQueueRefresh);
}
}
}