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 { [SerializeField] [SerializableDictionary("Stat Definition", "Base Value", isKeyEditable: false)] private SerializableDictionary _serializedStats; private Dictionary _stats = new(); private List _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().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(); } /// /// Really important part - we create stats for runtime usage. /// can also override another one, in that case /// uses main definition, but it is visible as that overriden one. /// 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); }); } /// /// Returns runtime values for given stat definition. /// /// Runtime stats will be found by this definition (or overriden definition if given) /// Stat values (baseValue, value, resource) public StatValue Get(StatDefinitionSO statDefinitionSO) { #if UNITY_EDITOR if (!UnityEditor.EditorApplication.isPlaying) CreateStats(); #endif return _stats.GetValueOrDefault(statDefinitionSO.GetBaseDefinition()); } /// /// 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(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); } } }