230 lines
9.9 KiB
C#
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);
|
|
}
|
|
}
|
|
} |