diff --git a/ModiBuff/ModiBuff.Tests/ModifierRecipeDataTests.cs b/ModiBuff/ModiBuff.Tests/ModifierRecipeDataTests.cs new file mode 100644 index 0000000..6ccef93 --- /dev/null +++ b/ModiBuff/ModiBuff.Tests/ModifierRecipeDataTests.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using ModiBuff.Core; +using ModiBuff.Core.Units; +using NUnit.Framework; + +namespace ModiBuff.Tests +{ + public sealed class ModifierRecipeDataTests : ModifierTests + { + [Test] + public void LegalActionUnitType() + { + const EnemyUnitType enemyType = EnemyUnitType.Goblin; + AddEnemySelfBuff("AddDamage", enemyType) + .Effect(new AddDamageEffect(5), EffectOn.Init); + AddRecipe("AddDamageApplier" + enemyType) + .Effect(new AddDamageEffect(5), EffectOn.Init) + .Data(new AddModifierCommonData(ModifierAddType.Applier, enemyType)); + AddEnemySelfBuff("AddDamage", EnemyUnitType.Slime) + .Effect(new AddDamageEffect(5), EffectOn.Init); + AddRecipe("AddDamageAdvanced" + enemyType) + .Effect(new AddDamageEffect(5), EffectOn.Init) + .Data(new AddModifierCommonData( + GoblinModifierActionType.OnSurrender, EnemyUnitType.Goblin)); + Setup(); + + Unit.AddModifierSelf("AddDamage" + enemyType); + + Assert.AreEqual(UnitDamage + 5, Unit.Damage); + + var enemySelfModifiers = new List(); + var addModifierCommonData = ModifierRecipes.GetModifierData>(); + Assert.AreEqual(addModifierCommonData.Length, 3); + Assert.AreEqual(addModifierCommonData.Count(d => d.Data.UnitType == enemyType), 2); + foreach ((int id, var data) in addModifierCommonData) + if (data.UnitType == enemyType && data.ModifierType == ModifierAddType.Self) + enemySelfModifiers.Add(id); + + Assert.AreEqual(enemySelfModifiers.Count, 1); + Assert.AreEqual(enemySelfModifiers[0], IdManager.GetId("AddDamage" + enemyType)); + + ModifierRecipe AddEnemySelfBuff(string name, EnemyUnitType enemyUnitType) => + AddRecipe(name + enemyUnitType) + .Data(new AddModifierCommonData(ModifierAddType.Self, enemyUnitType)); + } + + [Test] + public void SpecialUnitEvent() + { + const EnemyUnitType enemyType = EnemyUnitType.Goblin; + AddGoblinModifier("RemoveDamage", GoblinModifierActionType.OnSurrender) + .Effect(new AddDamageEffect(-5), EffectOn.Init); + Setup(); + + Unit.AddModifierSelf("RemoveDamage" + enemyType); + + Assert.AreEqual(UnitDamage - 5, Unit.Damage); + + var goblinSurrenderModifiers = new List(); + foreach ((int id, var data) in ModifierRecipes + .GetModifierData>()) + if (data.UnitType == enemyType && data.ModifierType == GoblinModifierActionType.OnSurrender) + goblinSurrenderModifiers.Add(id); + + Assert.AreEqual(goblinSurrenderModifiers.Count, 1); + Assert.AreEqual(goblinSurrenderModifiers[0], IdManager.GetId("RemoveDamage" + enemyType)); + + ModifierRecipe AddGoblinModifier(string name, GoblinModifierActionType modifierActionType) => + AddRecipe(name + EnemyUnitType.Goblin) + .Data(new AddModifierCommonData(modifierActionType, + EnemyUnitType.Goblin)); + } + + [Test] + public void LegalActionUnitTypeGenerator() + { + const EnemyUnitType enemyType = EnemyUnitType.Goblin; + AddGenerator("AddDamage" + enemyType, (id, genId, name, tag) => + { + var addDamageEffect = new AddDamageEffect(5); + var initComponent = new InitComponent(false, new IEffect[] { addDamageEffect }, null); + + return new Modifier(id, genId, name, initComponent, null, null, null, + new SingleTargetComponent(), new EffectStateInfo(addDamageEffect), null); + }, Core.Units.TagType.Default, + customModifierData: new AddModifierCommonData(ModifierAddType.Self, enemyType)); + Setup(); + + Unit.AddModifierSelf("AddDamage" + enemyType); + + Assert.AreEqual(UnitDamage + 5, Unit.Damage); + + var enemySelfModifiers = new List(); + foreach ((int id, var data) in ModifierRecipes.GetModifierData>()) + if (data.UnitType == enemyType && data.ModifierType == ModifierAddType.Self) + enemySelfModifiers.Add(id); + + Assert.AreEqual(enemySelfModifiers.Count, 1); + Assert.AreEqual(enemySelfModifiers[0], IdManager.GetId("AddDamage" + enemyType)); + } + } +} \ No newline at end of file diff --git a/ModiBuff/ModiBuff.Tests/ModifierTests.cs b/ModiBuff/ModiBuff.Tests/ModifierTests.cs index 83a6637..6c852d1 100644 --- a/ModiBuff/ModiBuff.Tests/ModifierTests.cs +++ b/ModiBuff/ModiBuff.Tests/ModifierTests.cs @@ -72,9 +72,10 @@ public virtual void IterationSetup() protected void AddEffect(string name, params IEffect[] effects) => Effects.Add(name, effects); - protected void AddGenerator(string name, in ModifierGeneratorFunc createFunc, TagType tag = TagType.Default) + protected void AddGenerator(string name, in ModifierGeneratorFunc createFunc, TagType tag = TagType.Default, + int auraId = -1, object customModifierData = null) { - Recipes.Add(name, name, "", in createFunc, tag.ToInternalTag()); + Recipes.Add(name, name, "", in createFunc, tag.ToInternalTag(), auraId, customModifierData); } /// diff --git a/ModiBuff/ModiBuff.Tests/PartialUnitTests/PartialUnitModifierTests.cs b/ModiBuff/ModiBuff.Tests/PartialUnitTests/PartialUnitModifierTests.cs index 94e6e0b..888c443 100644 --- a/ModiBuff/ModiBuff.Tests/PartialUnitTests/PartialUnitModifierTests.cs +++ b/ModiBuff/ModiBuff.Tests/PartialUnitTests/PartialUnitModifierTests.cs @@ -74,9 +74,10 @@ public void IterationSetup() protected void AddEffect(string name, params IEffect[] effects) => Effects.Add(name, effects); - protected void AddGenerator(string name, in ModifierGeneratorFunc createFunc, TagType tag = TagType.Default) + protected void AddGenerator(string name, in ModifierGeneratorFunc createFunc, TagType tag = TagType.Default, + int auraId = -1, object customModifierData = null) { - Recipes.Add(name, name, "", in createFunc, tag.ToInternalTag()); + Recipes.Add(name, name, "", in createFunc, tag.ToInternalTag(), auraId, customModifierData); } /// diff --git a/ModiBuff/ModiBuff.Units/Recipe/AddModifierCommonData.cs b/ModiBuff/ModiBuff.Units/Recipe/AddModifierCommonData.cs new file mode 100644 index 0000000..ffa41d0 --- /dev/null +++ b/ModiBuff/ModiBuff.Units/Recipe/AddModifierCommonData.cs @@ -0,0 +1,22 @@ +namespace ModiBuff.Core.Units +{ + public enum ModifierAddType + { + Self = 1, + Applier, + } + + public record AddModifierCommonData(ModifierAddType ModifierType, TUnit UnitType); + + /// + /// Custom AddModifierCommonData for non-standard/non-generic modifier add actions + /// + public record AddModifierCommonData(TModifier ModifierType, TUnit UnitType); +} + +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit + { + } +} \ No newline at end of file diff --git a/ModiBuff/ModiBuff.Units/UnitType.cs b/ModiBuff/ModiBuff.Units/UnitType.cs index bf2e7d8..fdab9bf 100644 --- a/ModiBuff/ModiBuff.Units/UnitType.cs +++ b/ModiBuff/ModiBuff.Units/UnitType.cs @@ -11,6 +11,35 @@ public enum UnitType Neutral = 3, } + /// + /// Example of how to use custom modifier data for advanced tagging purposes + /// + public enum AllyUnitType + { + Warrior, + Archer, + Mage, + } + + /// + /// Example of how to use custom modifier data for advanced tagging purposes + /// + public enum EnemyUnitType + { + Slime, + Goblin, + Orc, + } + + public enum GoblinModifierActionType + { + /// + /// Example special goblin mechanic, that says that these modifiers should be triggered on goblins event "OnSurrender" + /// + OnSurrender, + OnRetreat, + } + public static class UnitTypeExtensions { public static bool IsLegalTarget(this UnitType unitType, UnitType target) diff --git a/ModiBuff/ModiBuff/Core/Modifier/Creation/Generation/ManualModifierGenerator.cs b/ModiBuff/ModiBuff/Core/Modifier/Creation/Generation/ManualModifierGenerator.cs index de24c56..8c50205 100644 --- a/ModiBuff/ModiBuff/Core/Modifier/Creation/Generation/ManualModifierGenerator.cs +++ b/ModiBuff/ModiBuff/Core/Modifier/Creation/Generation/ManualModifierGenerator.cs @@ -8,12 +8,13 @@ public sealed class ManualModifierGenerator : IModifierGenerator public string Description { get; } public TagType Tag { get; } public int AuraId { get; } + public object Data { get; } private readonly ModifierGeneratorFunc _createFunc; private int _genId; public ManualModifierGenerator(int id, string name, string displayName, string description, - in ModifierGeneratorFunc createFunc, TagType tag, int auraId) + in ModifierGeneratorFunc createFunc, TagType tag, int auraId, object customModifierData) { Id = id; Name = name; @@ -27,6 +28,8 @@ public ManualModifierGenerator(int id, string name, string displayName, string d if (auraId != -1) tag |= TagType.IsAura; Tag = tag; + AuraId = auraId; + Data = customModifierData; } public Modifier Create() => _createFunc(Id, _genId++, Name, Tag); diff --git a/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/IModifierRecipe.cs b/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/IModifierRecipe.cs index bd3b2a2..78aadbc 100644 --- a/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/IModifierRecipe.cs +++ b/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/IModifierRecipe.cs @@ -9,6 +9,7 @@ public interface IModifierRecipe ModifierInfo CreateModifierInfo(); TagType GetTag(); int GetAuraId(); + object GetData(); //TODO Move/refactor ModifierRecipe.SaveData SaveState(); diff --git a/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipe.cs b/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipe.cs index c31ebd8..925ed37 100644 --- a/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipe.cs +++ b/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipe.cs @@ -22,6 +22,8 @@ public sealed partial class ModifierRecipe : IModifierRecipe, IEquatable(T data) + { + _data = data; + //_saveInstructions.Add(new SaveInstruction.Data(@object)); + return this; + } + private void AddRemoveEffect(EffectOn effectOn) { if (_removeEffectWrapper != null) @@ -581,6 +590,8 @@ public ModifierInfo CreateModifierInfo() public int GetAuraId() => _auraId; + public object GetData() => _data; + private ModifierRecipe DurationInternal(float duration) { _duration = duration; diff --git a/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipeSaveLoad.cs b/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipeSaveLoad.cs index 8ef8aa9..95e1220 100644 --- a/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipeSaveLoad.cs +++ b/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipeSaveLoad.cs @@ -120,10 +120,12 @@ public void LoadState(SaveData saveData) ModifierAction(action.ModifierActionFlags, action.EffectOn); #endif break; -// case SaveInstruction.Event.Id: -//#if MODIBUFF_SYSTEM_TEXT_JSON -//#endif -// break; + case SaveInstruction.Data.Id: +#if MODIBUFF_SYSTEM_TEXT_JSON + var data = (SaveInstruction.Data)instruction; + Data(data.SaveData); +#endif + break; default: Logger.LogError($"Unknown instruction with id {instruction.InstructionId}"); break; @@ -184,6 +186,7 @@ public void LoadState(SaveData saveData) [System.Text.Json.Serialization.JsonDerivedType(typeof(CallbackUnit), CallbackUnit.Id)] [System.Text.Json.Serialization.JsonDerivedType(typeof(Effect), Effect.Id)] [System.Text.Json.Serialization.JsonDerivedType(typeof(ModifierAction), ModifierAction.Id)] + [System.Text.Json.Serialization.JsonDerivedType(typeof(Data), Data.Id)] #endif public record SaveInstruction { @@ -413,6 +416,18 @@ public ModifierAction(ModiBuff.Core.ModifierAction modifierActionFlags, EffectOn EffectOn = effectOn; } } + + public sealed record Data : SaveInstruction + { + public const int Id = ModifierAction.Id + 1; + + public readonly object SaveData; + +#if MODIBUFF_SYSTEM_TEXT_JSON + [System.Text.Json.Serialization.JsonConstructor] +#endif + public Data(object saveData) : base(Id) => SaveData = saveData; + } } public readonly struct SaveData diff --git a/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipes.cs b/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipes.cs index a80087b..811e522 100644 --- a/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipes.cs +++ b/ModiBuff/ModiBuff/Core/Modifier/Creation/Recipe/ModifierRecipes.cs @@ -29,6 +29,9 @@ public class ModifierRecipes : IModifierRecipes private ModifierInfo[] _modifierInfos; private TagType[] _tags; private int[] _auraIds; + private object[] _modifierData; + + private readonly List<(int, object)> _modifierDataList; public ModifierRecipes(ModifierIdManager idManager, EffectTypeIdManager effectTypeIdManager) { @@ -40,6 +43,7 @@ public ModifierRecipes(ModifierIdManager idManager, EffectTypeIdManager effectTy _manualGenerators = new Dictionary(64); _modifierGenerators = new Dictionary(64); _registeredNames = new List(16); + _modifierDataList = new List<(int, object)>(16); } //TODO TEMP @@ -61,6 +65,7 @@ public void CreateGenerators() _auraIds = new int[_recipes.Count + _manualGenerators.Count]; for (int i = 0; i < _auraIds.Length; i++) _auraIds[i] = -1; + _modifierData = new object[_recipes.Count + _manualGenerators.Count]; foreach (var generator in _manualGenerators.Values) { _modifierGenerators.Add(generator.Name, generator); @@ -69,6 +74,7 @@ public void CreateGenerators() _tags[generator.Id] = generator.Tag; if (generator.Tag.HasFlag(TagType.IsAura)) _auraIds[generator.Id] = generator.AuraId; + _modifierData[generator.Id] = generator.Data; } foreach (var recipe in _recipes.Values) @@ -78,6 +84,7 @@ public void CreateGenerators() _tags[recipe.Id] = recipe.GetTag(); if (recipe.GetTag().HasFlag(TagType.IsAura)) _auraIds[recipe.Id] = recipe.GetAuraId(); + _modifierData[recipe.Id] = recipe.GetData(); } GeneratorCount = _modifierGenerators.Count; @@ -100,6 +107,21 @@ public ModifierInfo GetModifierInfo(int id) public static ref readonly TagType GetTag(int id) => ref _instance._tags[id]; public static int GetAuraId(int id) => _instance._auraIds[id]; + public static T GetModifierData(int id) => (T)_instance._modifierData[id]; + + public static (int Id, T Data)[] GetModifierData() + { + for (int i = 0; i < _instance._modifierData.Length; i++) + if (_instance._modifierData[i] is T data) + _instance._modifierDataList.Add((i, data)); + + var modifierData = new (int, T)[_instance._modifierDataList.Count]; + for (int i = 0; i < modifierData.Length; i++) + modifierData[i] = ((int, T))_instance._modifierDataList[i]; + _instance._modifierDataList.Clear(); + return modifierData; + } + public IModifierGenerator GetGenerator(string name) => _modifierGenerators[name]; public IModifierGenerator[] GetGenerators() => _modifierGenerators.Values.ToArray(); @@ -135,8 +157,8 @@ public ModifierRecipe Add(string name, string displayName = "", string descripti return recipe; } - public void Add(string name, string displayName, string description, - in ModifierGeneratorFunc createFunc, TagType tag = TagType.Default, int auraId = -1) + public void Add(string name, string displayName, string description, in ModifierGeneratorFunc createFunc, + TagType tag = TagType.Default, int auraId = -1, object customModifierData = null) { if (_recipes.ContainsKey(name)) { @@ -169,7 +191,7 @@ public void Add(string name, string displayName, string description, id = _idManager.GetFreeId(name); var modifierGenerator = new ManualModifierGenerator(id, name, displayName, description, - in createFunc, tag, auraId); + in createFunc, tag, auraId, customModifierData); _manualGenerators.Add(name, modifierGenerator); } diff --git a/README.md b/README.md index c2ea917..56d0732 100644 --- a/README.md +++ b/README.md @@ -530,8 +530,7 @@ In this example we add 5 damage to unit on Init, and the modifier can only be re StrongHit". Essentially a hit that deals more than half units health in damage (ex. game logic). -> Important: there can only be one callback `CallbackUnit` per modifier, but there can be -> multiple effects that trigger on that callback. +> Important: all versions before 0.4/latest master can only have one callback `CallbackUnit` per modifier. ```csharp Add("InitAddDamageRevertibleHalfHealthCallback") @@ -846,6 +845,86 @@ Add("InitDamageEnemyOnly") .Effect(new DamageEffect(5f), EffectOn.Init); ``` +### Custom Data + +Sometimes the tag system is too limited for our needs, and we want to store custom modifier identification with data. +That's why every recipe stores a custom object, that can be accessed from anywhere like the tag. +It is mostly designed to store basic information about the modifier, one example of this is adding a modifier to every +unit of type X (ex. Goblin). +Instead of storing that information on the goblin unit data itself, we delegate it to the modifiers, also it makes it so +we don't need arbitrary naming conventions for our modifiers. + +```csharp +public enum EnemyUnitType +{ + Slime, + Goblin, + Orc, +} + +public enum ModifierAddType +{ + Self = 1, + Applier, +} + +public record AddModifierCommonData(ModifierAddType ModifierType, TUnit UnitType); + +Add("Damage") + .Data(new AddModifierCommonData(ModifierAddType.Self, EnemyUnitType.Goblin)) + .Effect(new DamageEffect(5), EffectOn.Init); +``` + +It also allows for a more standardized recipe creation, by unit types, modifier types, etc, reducing code duplication. +And allowing for a set of standards, making the modifier creation less prone to errors. + +```csharp +AddEnemySelfBuff("Damage", EnemyUnitType.Goblin) + .Effect(new DamageEffect(5), EffectOn.Init); + +ModifierRecipe AddEnemySelfBuff(string name, EnemyUnitType enemyUnitType, string displayName = "", string description = "") => + Add(name + enemyUnitType, displayName, description) + .Data(new AddModifierCommonData(ModifierAddType.Self, enemyUnitType)); +``` + +Then to apply all the modifiers to the units, we filter through them. + +```csharp +var goblinSelfModifiers = new List(); +foreach ((int id, var data) in ModifierRecipes.GetModifierData>()) + if (data.UnitType == EnemyType.Goblin && data.ModifierType == ModifierAddType.Self) + goblinSelfModifiers.Add(id); +``` + +It's also possible to create `ModifierRecipe` extensions instead, for ease of use. +This is recommended if you have multiple different entity type combinations, and actions. + +```csharp +public static ModifierRecipe Data(this ModifierRecipe recipe, ModifierAddType modifierAddType, EnemyUnitType enemyUnitType) => + recipe.Data(new AddModifierCommonData(modifierAddType, enemyUnitType)); + +ModifierRecipe AddEnemySelfBuffExtension(string name, EnemyUnitType enemyUnitType, string displayName = "", string description = "") => + AddRecipe(name + enemyUnitType, displayName, description) + .Data(ModifierAddType.Self, enemyUnitType); +``` + +> Note: That modifier appliers should still be two separate modifiers, the applier and the applied modifier. +> Then we just add the appliers based on what we get from `ModifierRecipes.GetModifierData`, and apply them on +> actions/events. + +#### Advanced Custom Data + +It's possible to use custom action types as well, possibly for non-generic actions tied to a unit type. +Ex. goblins having a special event to surrender, and us wanting to trigger some modifiers on certain goblin types. + +```csharp +public record AddModifierCommonData(TModifier ModifierType, TUnit UnitType); + +Add("RemoveDamageOnSurrender") + .Data(new AddModifierCommonData(GoblinModifierActionType.OnSurrender, EnemyUnitType.Goblin) + .Effect(new DamageEffect(-5), EffectOn.Init); +``` + ### Custom Stack Stack is always triggered when we try to add the same type of modifier again. @@ -919,6 +998,34 @@ Add("Full") Each modifier should have at least one effect, unless it's used as a flag. +### Encapsulating same code in method extensions + +When a game has many modifiers that share the same core logic, it's recommended to encapsulate the logic in method +through method extensions. +An example of this from Chillu's test game, where whenever an enemy drops a resource their carrying, they lose that +resources buff/debuff. + +```csharp +public static ModifierRecipe RemoveOnResourceDrop(this ModifierRecipe recipe, ResourceType resourceType) => + recipe.Remove(RemoveEffectOn.CallbackEffect) + .CallbackEffect(CallbackType.EnemyDropResource, effect => + new ResourceDroppedEvent((target, resource) => + { + if (effect is RemoveEffect removeEffect && resource.IsPrimary(resourceType)) + removeEffect.Effect(target, null); + })); + +AddResourceModifier(ResourceType.Red, ResourceModifierAction.OnDigested, "Curse", "Resource eating curse", + "Deal damage with every resource eaten") + .Effect(new DamageEffect(new MutableDamageStatData(10)), EffectOn.CallbackUnit) + .CallbackUnit(CallbackType.EnemyEatResource) + .Remove(15).Refresh() + .RemoveOnResourceDrop(ResourceType.Red); //Then we just call the method extension each time we we need to add the modifier of this type +``` + +This allows for lower cognitive load, since it's easier to understand what "RemoveOnResourceDrop(ResourceType.Red)" +does. + ## Recipe Limitations > Note that these limitations don't matter for 95% of the use cases.