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 BaseObject. " + "Create scriptable object "+nameof(StatValueDefinitionSO)+" to define new stats. " + "Stats will be automatically created in any matching implementation of BaseObject. " + "Remember to add all definitions to generated "+nameof(StatsSettings)+"." )] public class StatsModule : ObjectModule { [SerializeField] [SerializableDictionary("Value Definition", "Base Value", isKeyEditable: false)] private SerializableDictionary _serializedValues; [SerializeField] [SerializableDictionary("Resource Definition", "Default Value", isKeyEditable: false)] private SerializableDictionary _serializedResources; private Dictionary _values = new(); private Dictionary _resources = new(); private List _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().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().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().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(); } /// /// Really important part - we create values for runtime usage. /// 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); }); } /// /// Really important part - we create resources for runtime usage. /// private void CreateResources() { _serializedResources.DictForEach((definition, startingResource) => { _resources.Add( definition, new StatResource(definition, startingResource, Get(definition.valueDefinitionSO)) ); }); } /// /// Returns runtime values for given stat definition. /// /// Runtime stats will be found by this definition (or overriden definition if given) /// Stat value (baseValue, value) public StatValue Get(StatValueDefinitionSO valueDefinitionSO) { #if UNITY_EDITOR if (!UnityEditor.EditorApplication.isPlaying) CreateStats(); #endif return _values.GetValueOrDefault(valueDefinitionSO.GetBaseDefinition()); } /// /// Stat resource (baseValue, value, resource) public StatResource Get(StatResourceDefinitionSO resourceDefinitionSO) { #if UNITY_EDITOR if (!UnityEditor.EditorApplication.isPlaying) CreateResources(); #endif return _resources.GetValueOrDefault(resourceDefinitionSO); } /// /// Refreshing stats, every stat only once even if queued multiple times. /// To ensure that dependencies should be refreshed before definition that is using it. /// 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(); } /// /// Select along with its dependencies to refresh next frame. /// 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); } } }