init
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad8746ba38f04d719dd84d28f09a8512
|
||||
timeCreated: 1773954611
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "RPGCore.Stats.Editor",
|
||||
"rootNamespace": "RPGCore.Stats.Editor",
|
||||
"references": [
|
||||
"RPGCore",
|
||||
"RPGCore.Stats",
|
||||
"RPGCoreCommon.Helpers"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2aa3b7d8e0b448e7881e3964a0435cdd
|
||||
timeCreated: 1773954617
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e8966dae25541459432287ae898c630
|
||||
timeCreated: 1773954606
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81ee3e16a1644004b6c4988b06c0a6fd
|
||||
timeCreated: 1773954672
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d9e8c1e38314f68b12ff35c5d870faa
|
||||
timeCreated: 1762712643
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 813519acd4a241f19fe7bec852b0acda
|
||||
timeCreated: 1763840115
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using RPGCore.Core.Objects;
|
||||
using RPGCore.ObjectModules.EventObjectModule;
|
||||
|
||||
namespace RPGCore.Stats.ObjectModules.StatsObjectModule.Events
|
||||
{
|
||||
public class StatResourceChangeEvent : BaseEvent<BaseObject>
|
||||
{
|
||||
public BaseObject target;
|
||||
public StatDefinitionSO statDefinition;
|
||||
public int delta;
|
||||
public int before;
|
||||
public int after;
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c00f342fabcb4aa6adce19862d88212e
|
||||
timeCreated: 1763840330
|
||||
@@ -0,0 +1,13 @@
|
||||
using RPGCore.Core.Objects;
|
||||
using RPGCore.ObjectModules.EventObjectModule;
|
||||
|
||||
namespace RPGCore.Stats.ObjectModules.StatsObjectModule.Events
|
||||
{
|
||||
public class StatValueChangeEvent : BaseEvent<BaseObject>
|
||||
{
|
||||
public BaseObject target;
|
||||
public StatDefinitionSO statDefinition;
|
||||
public int before;
|
||||
public int after;
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: baeb8bd8a9394f428240e3a7ca7b98b4
|
||||
timeCreated: 1763840139
|
||||
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RPGCore.Core.Objects;
|
||||
using RPGCoreCommon.DynamicValues;
|
||||
using RPGCoreCommon.Helpers;
|
||||
using RPGCoreCommon.Helpers.CustomTypes;
|
||||
using RPGCoreCommon.Helpers.PropertyAttributeDrawers;
|
||||
using RPGCoreCommon.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RPGCore.Stats.ObjectModules.StatsObjectModule
|
||||
{
|
||||
[CreateAssetMenu(menuName = "RPG Core/Object Stat Definition")]
|
||||
public sealed class StatDefinitionSO : ScriptableObject
|
||||
{
|
||||
[SerializableType(typeof(BaseObject), allowAbstract: true)] public SerializableType attachedTo;
|
||||
public StatType type;
|
||||
public StatDefinitionSO overrides;
|
||||
[ReadOnly(true)] public StatDefinitionSO[] dependencies = {};
|
||||
|
||||
[DynamicValue(DynamicValueType.ByDynamicTypes)] public DynamicValue dynamicValue = new();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
FixDynamicValueType();
|
||||
FixOverride();
|
||||
FixDependencies();
|
||||
CheckDependenciesLooping();
|
||||
}
|
||||
|
||||
private void FixDynamicValueType()
|
||||
{
|
||||
dynamicValue.RemoveDynamicTypes();
|
||||
dynamicValue.SetDynamicType("object", attachedTo);
|
||||
}
|
||||
|
||||
private void FixOverride()
|
||||
{
|
||||
if (!overrides) return;
|
||||
|
||||
// Without attachedTo selected we can't be sure if this overriding can be even done
|
||||
if (attachedTo == null)
|
||||
{
|
||||
Debug.LogWarning($"{nameof(attachedTo)} is required before stat overriding");
|
||||
overrides = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// We can override only child of "attachedTo" object, otherwise it is pointless
|
||||
if (overrides.attachedTo.type == attachedTo.type ||
|
||||
overrides.attachedTo.type.IsAssignableFrom(attachedTo.type) == false)
|
||||
{
|
||||
Debug.LogWarning($"When overriding this <b>{nameof(attachedTo)}</b> MUST BE child child type of <b>{nameof(attachedTo)}</b>");
|
||||
overrides = null;
|
||||
return;
|
||||
}
|
||||
|
||||
name = overrides.name;
|
||||
type = overrides.type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="StatsModule"/> has custom value provider <see cref="StatsModule.StatValueProvider"/> that matches stats by their name.
|
||||
/// We will match these names with marker's parameter to create list of other <see cref="StatDefinitionSO"/> that this one uses.
|
||||
/// Main purpose of dependencies is caching, specially when there is a lot of definitions and dependencies.
|
||||
/// </summary>
|
||||
private void FixDependencies()
|
||||
{
|
||||
var tempDependencies = new List<StatDefinitionSO>();
|
||||
var allDefinitions = SettingsManager.Get<StatsSettings>().allStats;
|
||||
|
||||
foreach (var marker in dynamicValue.GetMarkers())
|
||||
{
|
||||
var members = marker.GetMembers(out var membersParameters);
|
||||
for (var i = 0; i < members.Length; i++)
|
||||
{
|
||||
var member = members[i];
|
||||
var memberParameter = membersParameters[i];
|
||||
|
||||
// Dependencies are created only by StatsModule's StatValueProvider!
|
||||
if (member.DeclaringType != typeof(StatsModule)) continue;
|
||||
if (member.Name != nameof(StatsModule.StatValueProvider)) continue;
|
||||
|
||||
var dependency = allDefinitions.FirstOrDefault(definition =>
|
||||
definition.name.Equals((string)memberParameter, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (dependency)
|
||||
{
|
||||
tempDependencies.Add(dependency.GetBaseDefinition());
|
||||
break;
|
||||
}
|
||||
|
||||
Debug.LogError($"Stat definition with name <b>{(string)memberParameter}</b> doesn't exist " +
|
||||
$"or is not added to {nameof(StatsSettings)}!" +
|
||||
$"Marker <b>{marker.wholeMarker}</b> is invalid!", this);
|
||||
}
|
||||
}
|
||||
|
||||
dependencies = tempDependencies.Distinct().ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates via dependencies to check if none of them loops.
|
||||
/// If looping is found then dependencies will be cleared, preventing application freeze!
|
||||
/// </summary>
|
||||
/// <param name="definitionsChain"></param>
|
||||
private void CheckDependenciesLooping(List<StatDefinitionSO> definitionsChain = null)
|
||||
{
|
||||
definitionsChain ??= new List<StatDefinitionSO>();
|
||||
|
||||
if (definitionsChain.Contains(this))
|
||||
{
|
||||
definitionsChain[0].dependencies = Array.Empty<StatDefinitionSO>();
|
||||
var definitionsChainString = definitionsChain.Append(this).Select(d => d.name).StringJoin(" -> ");
|
||||
Debug.LogError($"Looped dependencies found! {definitionsChainString}", this);
|
||||
return;
|
||||
}
|
||||
|
||||
dependencies.ForEach(dependency => dependency.CheckDependenciesLooping(definitionsChain.Append(this).ToList()));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Finds first element in this overriding chain.
|
||||
/// Thanks to OnValidate its impossible to create endless overriding loop, it is possible to override only child object.
|
||||
/// </summary>
|
||||
/// <returns>Last overriden definition</returns>
|
||||
public StatDefinitionSO GetBaseDefinition()
|
||||
{
|
||||
var tempDefinition = this;
|
||||
while (tempDefinition.overrides)
|
||||
tempDefinition = tempDefinition.overrides;
|
||||
|
||||
return tempDefinition;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae9af538607d4890ba7d6e30b46efef7
|
||||
timeCreated: 1762712932
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RPGCore.Stats.ObjectModules.StatsObjectModule
|
||||
{
|
||||
public enum StatType
|
||||
{
|
||||
Value,
|
||||
Resource
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1800df1b40e6455abd599f5bb6808735
|
||||
timeCreated: 1762771772
|
||||
@@ -0,0 +1,109 @@
|
||||
using RPGCore.Core.Objects;
|
||||
using RPGCore.ObjectModules.EventObjectModule;
|
||||
using RPGCore.Stats.ObjectModules.StatsObjectModule.Events;
|
||||
using RPGCoreCommon.DynamicValues;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RPGCore.Stats.ObjectModules.StatsObjectModule
|
||||
{
|
||||
/// <summary>
|
||||
/// <b>You should never instantiate this by yourself!</b>
|
||||
/// Instantiated automatically by <see cref="StatsModule"/> for every available <see cref="StatDefinitionSO"/>.
|
||||
/// </summary>
|
||||
public sealed class StatValue
|
||||
{
|
||||
public readonly StatDefinitionSO definition;
|
||||
private readonly BaseObject _parent;
|
||||
public readonly DynamicValueSourceContext sourceContext = new();
|
||||
|
||||
/// <summary>
|
||||
/// Base value defined in <see cref="StatsModule"/> attached to <see cref="BaseObject"/>.
|
||||
/// Formula result will be added to this base value.
|
||||
/// Default base value is ZERO.
|
||||
/// </summary>
|
||||
public int baseValue { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This value is calculated by <see cref="DynamicValue"/> in <see cref="StatDefinitionSO"/> and can have two meanings:<br/>
|
||||
/// 1. If <see cref="StatType.Value"/> - just simple value stat<br/>
|
||||
/// 2. If <see cref="StatType.Resource"/> - this is maximum value for that resource
|
||||
/// </summary>
|
||||
public int value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Usable only when <see cref="StatDefinitionSO"/>.<see cref="StatType"/> is <see cref="StatType.Resource"/>.<br/>
|
||||
/// This value represent current resource of this stat. Always between <see cref="baseValue"/> and <see cref="value"/>
|
||||
/// </summary>
|
||||
public int resource { get; private set; }
|
||||
|
||||
internal StatValue(StatDefinitionSO definition, int baseValue, BaseObject parent)
|
||||
{
|
||||
this.definition = definition;
|
||||
_parent = parent;
|
||||
this.baseValue = baseValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes <see cref="value"/>, by calculating formula again.
|
||||
/// </summary>
|
||||
internal void Refresh()
|
||||
{
|
||||
var previous = value;
|
||||
value = baseValue + definition.dynamicValue.GetValue(sourceContext);
|
||||
_parent.events.Invoke(new StatValueChangeEvent
|
||||
{
|
||||
after = value,
|
||||
before = previous,
|
||||
statDefinition = definition,
|
||||
target = _parent
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets new <see cref="baseValue"/> and then do <see cref="Refresh"/> to recalculate whole <see cref="value"/>
|
||||
/// </summary>
|
||||
/// <param name="amount">New base value</param>
|
||||
public void SetBase(int amount)
|
||||
{
|
||||
baseValue = amount;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes (or simpler - uses) <see cref="resource"/> of this stat value by given amount.
|
||||
/// It just calls <see cref="SetResource(int)"/> with: <see cref="resource"/> + <see cref="amount"/>
|
||||
/// </summary>
|
||||
/// <param name="amount">Amount that will be added or subtracted from <see cref="resource"/></param>
|
||||
public void ChangeResource(int amount)
|
||||
{
|
||||
SetResource(resource + amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="resource"/> to given amount. Amount will be clamped between ZERO and <see cref="value"/>.
|
||||
/// </summary>
|
||||
/// <param name="amount">New amount</param>
|
||||
public void SetResource(int amount)
|
||||
{
|
||||
var old = resource;
|
||||
resource = Mathf.Clamp(amount, 0, value);
|
||||
_parent.events.Invoke(new StatResourceChangeEvent
|
||||
{
|
||||
delta = resource - old,
|
||||
after = resource,
|
||||
before = old,
|
||||
statDefinition = definition,
|
||||
target = _parent
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="resource"/> to given percentage. Percentage will be clamped between ZERO and ONE.
|
||||
/// </summary>
|
||||
/// <param name="percentage">New percentage</param>
|
||||
public void SetResource(float percentage)
|
||||
{
|
||||
SetResource(Mathf.CeilToInt(Mathf.Clamp01(percentage) * value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7012414211045ebbaf7f1d3b0cfc911
|
||||
timeCreated: 1763222304
|
||||
@@ -0,0 +1,154 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d36660a0fd2e489aad1fb57b951038d9
|
||||
timeCreated: 1762712656
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using RPGCoreCommon.Settings;
|
||||
|
||||
namespace RPGCore.Stats.ObjectModules.StatsObjectModule
|
||||
{
|
||||
[CustomSettings("RPG Core/Statistics")]
|
||||
public class StatsSettings : CustomSettingsSO
|
||||
{
|
||||
public List<StatDefinitionSO> allStats = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23bc82a39d2248aa9a78a75a5162e173
|
||||
timeCreated: 1762713013
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "RPGCore.Stats",
|
||||
"rootNamespace": "RPGCore.Stats",
|
||||
"references": [
|
||||
"RPGCore",
|
||||
"RPGCoreCommon.Settings",
|
||||
"RPGCoreCommon.Helpers",
|
||||
"RPGCoreCommon.DynamicValues",
|
||||
"Unity.InputSystem"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d6ef8d5372fc5544872a154e5063b07
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user