Files
TheVVaS-Assets/RPGCore.Stats/Runtime/ObjectModules/StatsObjectModule/StatsModule.cs
T

230 lines
9.9 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]
[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("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<StatValueDefinitionSO, StatValue> _values = new();
private Dictionary<StatResourceDefinitionSO, StatResource> _resources = new();
private List<StatValueDefinitionSO> _queue = new();
#if UNITY_EDITOR
private void OnValidate()
{
FixSerializedStatValues();
FixSerializedStatResources();
}
private void FixSerializedStatValues()
{
// fixing only when variable is serialized (sometimes OnValidate can be called before deserializing on project open)
if (_serializedValues == null) return;
// 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
correctValues.Select(value => value.overrides)
.Where(overridenValue => overridenValue)
.ForEach(overridenValue => correctValues.Remove(overridenValue));
// ADD MISSING
var missingValues = correctValues.Except(_serializedValues.Keys).ToList();
if (missingValues.Any())
{
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 invalidValues = _serializedValues.Keys.Except(correctValues).ToList();
if (invalidValues.Any())
{
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);
}
}
#endif
[DynamicValueProvider]
internal StatValue StatValueProvider(string 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
_values.Keys.ForEach(AddToQueueRefresh);
QueueRefresh();
}
private void FixedUpdate()
{
if (_queue.Count > 0) QueueRefresh();
}
/// <summary>
/// Really important part - we create values for runtime usage.
/// </summary>
private void CreateStats()
{
_serializedValues.DictForEach((definition, baseValue) =>
{
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="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 _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>
/// 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="StatValueDefinitionSO"/> along with its dependencies to refresh next frame.
/// </summary>
public void AddToQueueRefresh(StatValueDefinitionSO 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
_values.Keys
.Where(otherStatDefinition => otherStatDefinition.dependencies.Contains(statDefinitionSO))
.ForEach(AddToQueueRefresh);
}
}
}