This commit is contained in:
2026-04-25 23:37:10 +02:00
commit 19d6bd934a
476 changed files with 9198 additions and 0 deletions
+3
View File
@@ -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
@@ -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;
}
}
@@ -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;
}
}
@@ -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: