From 4a8874b6d5761497403e36c387287acab801233c Mon Sep 17 00:00:00 2001 From: LP Date: Wed, 23 Apr 2025 19:44:05 +0100 Subject: [PATCH] Allowing things to have abilities and updating attacks This supports critical damage as well as varying types of attacks now. --- src/OpenRpg.Combat/Abilities/Ability.cs | 7 ++ src/OpenRpg.Combat/Attacks/Attack.cs | 7 +- ...s.cs => CombatEntityVariableExtensions.cs} | 2 + .../CombatTemplateVariableExtensions.cs | 21 +++++ .../Extensions/IRandomizerExtensions.cs | 20 +++++ .../Processors/Attacks/IAttackGenerator.cs | 3 + .../Types/CombatTemplateVariableTypes.cs | 11 +++ .../DI/OpenRpgModule.cs | 2 +- .../Templates/ModificationTemplate.cs | 1 - .../Combat/BasicAttackGenerator.cs | 21 ----- .../Combat/FantasyAttackGenerator.cs | 76 +++++++++++++++++++ .../Combat/ShipAttackGenerator.cs | 14 ++++ .../ScifiCombatReferencesExtensions.cs | 4 +- 13 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 src/OpenRpg.Combat/Abilities/Ability.cs rename src/OpenRpg.Combat/Extensions/{IItemEntityVariableExtensions.cs => CombatEntityVariableExtensions.cs} (92%) create mode 100644 src/OpenRpg.Combat/Extensions/CombatTemplateVariableExtensions.cs create mode 100644 src/OpenRpg.Combat/Extensions/IRandomizerExtensions.cs create mode 100644 src/OpenRpg.Combat/Types/CombatTemplateVariableTypes.cs delete mode 100644 src/OpenRpg.Genres.Fantasy/Combat/BasicAttackGenerator.cs create mode 100644 src/OpenRpg.Genres.Fantasy/Combat/FantasyAttackGenerator.cs diff --git a/src/OpenRpg.Combat/Abilities/Ability.cs b/src/OpenRpg.Combat/Abilities/Ability.cs new file mode 100644 index 0000000..5e66473 --- /dev/null +++ b/src/OpenRpg.Combat/Abilities/Ability.cs @@ -0,0 +1,7 @@ +using OpenRpg.Core.Templates; + +namespace OpenRpg.Combat.Abilities +{ + public class Ability : TemplateInstance + {} +} \ No newline at end of file diff --git a/src/OpenRpg.Combat/Attacks/Attack.cs b/src/OpenRpg.Combat/Attacks/Attack.cs index 41ff3fb..dd11b5f 100644 --- a/src/OpenRpg.Combat/Attacks/Attack.cs +++ b/src/OpenRpg.Combat/Attacks/Attack.cs @@ -1,15 +1,18 @@ +using System; using System.Collections.Generic; namespace OpenRpg.Combat.Attacks { public class Attack { - public ICollection Damages { get; set; } = new List(); + public bool IsCritical { get; set; } + public IReadOnlyList Damages { get; set; } = Array.Empty(); public Attack(){} - public Attack(ICollection damages) + public Attack(IReadOnlyList damages, bool isCritical = false) { Damages = damages; + IsCritical = isCritical; } } } \ No newline at end of file diff --git a/src/OpenRpg.Combat/Extensions/IItemEntityVariableExtensions.cs b/src/OpenRpg.Combat/Extensions/CombatEntityVariableExtensions.cs similarity index 92% rename from src/OpenRpg.Combat/Extensions/IItemEntityVariableExtensions.cs rename to src/OpenRpg.Combat/Extensions/CombatEntityVariableExtensions.cs index b8213e1..98771a1 100644 --- a/src/OpenRpg.Combat/Extensions/IItemEntityVariableExtensions.cs +++ b/src/OpenRpg.Combat/Extensions/CombatEntityVariableExtensions.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using OpenRpg.Combat.Abilities; using OpenRpg.Combat.Effects; using OpenRpg.Combat.Types; using OpenRpg.Entities.Entity.Variables; diff --git a/src/OpenRpg.Combat/Extensions/CombatTemplateVariableExtensions.cs b/src/OpenRpg.Combat/Extensions/CombatTemplateVariableExtensions.cs new file mode 100644 index 0000000..f2cd4f7 --- /dev/null +++ b/src/OpenRpg.Combat/Extensions/CombatTemplateVariableExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using OpenRpg.Combat.Abilities; +using OpenRpg.Combat.Types; +using OpenRpg.Core.Extensions; +using OpenRpg.Core.Templates.Variables; + +namespace OpenRpg.Combat.Extensions +{ + public static class CombatTemplateVariableExtensions + { + public static bool HasAbilities(this ITemplateVariables vars) => + vars.ContainsKey(CombatTemplateVariableTypes.Abilities); + + public static IReadOnlyCollection Abilities(this ITemplateVariables vars) => + vars.GetAsOrDefault(CombatTemplateVariableTypes.Abilities, Array.Empty); + + public static void Abilities(this ITemplateVariables vars, IReadOnlyCollection abilities) => + vars[CombatTemplateVariableTypes.Abilities] = abilities; + } +} \ No newline at end of file diff --git a/src/OpenRpg.Combat/Extensions/IRandomizerExtensions.cs b/src/OpenRpg.Combat/Extensions/IRandomizerExtensions.cs new file mode 100644 index 0000000..48e40f3 --- /dev/null +++ b/src/OpenRpg.Combat/Extensions/IRandomizerExtensions.cs @@ -0,0 +1,20 @@ +using OpenRpg.Core.Utils; +using OpenRpg.CurveFunctions.Extensions; + +namespace OpenRpg.Combat.Extensions +{ + public static class IRandomizerExtensions + { + /// + /// This is a quick way to check if you have made a critical hit + /// + /// The randomizer to user for this + /// The critical chance between 0-1 + /// true if a critical attack was made, false if not + public static bool ShouldCritical(this IRandomizer randomizer, float criticalChance) + { + var sanitizedCritChance = criticalChance.SanitizeAndClamp(); + return randomizer.Random() <= sanitizedCritChance; + } + } +} \ No newline at end of file diff --git a/src/OpenRpg.Combat/Processors/Attacks/IAttackGenerator.cs b/src/OpenRpg.Combat/Processors/Attacks/IAttackGenerator.cs index 4550143..5485c94 100644 --- a/src/OpenRpg.Combat/Processors/Attacks/IAttackGenerator.cs +++ b/src/OpenRpg.Combat/Processors/Attacks/IAttackGenerator.cs @@ -1,3 +1,4 @@ +using OpenRpg.Combat.Abilities; using OpenRpg.Combat.Attacks; using OpenRpg.Entities.Stats; @@ -6,5 +7,7 @@ namespace OpenRpg.Combat.Processors.Attacks public interface IAttackGenerator where T : IStatsVariables { Attack GenerateAttack(T stats); + Attack GenerateAttack(Ability ability, T stats); + Attack GenerateAttack(Damage damage, T stats); } } \ No newline at end of file diff --git a/src/OpenRpg.Combat/Types/CombatTemplateVariableTypes.cs b/src/OpenRpg.Combat/Types/CombatTemplateVariableTypes.cs new file mode 100644 index 0000000..c8da1e7 --- /dev/null +++ b/src/OpenRpg.Combat/Types/CombatTemplateVariableTypes.cs @@ -0,0 +1,11 @@ +namespace OpenRpg.Combat.Types +{ + public interface CombatTemplateVariableTypes + { + // Unknown + public static int Unknown = 0; + + // For adding the notion of procedural effects + public static int Abilities = 6000; + } +} \ No newline at end of file diff --git a/src/OpenRpg.Demos.Infrastructure/DI/OpenRpgModule.cs b/src/OpenRpg.Demos.Infrastructure/DI/OpenRpgModule.cs index 0509570..9ab3bcb 100644 --- a/src/OpenRpg.Demos.Infrastructure/DI/OpenRpgModule.cs +++ b/src/OpenRpg.Demos.Infrastructure/DI/OpenRpgModule.cs @@ -30,7 +30,7 @@ public void Setup(IServiceCollection services) services.AddSingleton(new FantasyStatsPopulator([new DamageStatPopulator(), new DefenseStatPopulator()])); services.AddSingleton(); services.AddSingleton(x => new DefaultRandomizer(new Random())); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton, DefaultAttackProcessor>(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/OpenRpg.Entities/Modifications/Templates/ModificationTemplate.cs b/src/OpenRpg.Entities/Modifications/Templates/ModificationTemplate.cs index 0b9b826..c31c469 100644 --- a/src/OpenRpg.Entities/Modifications/Templates/ModificationTemplate.cs +++ b/src/OpenRpg.Entities/Modifications/Templates/ModificationTemplate.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using OpenRpg.Core.Effects; using OpenRpg.Core.Templates; -using OpenRpg.Entities.Effects; namespace OpenRpg.Entities.Modifications.Templates { diff --git a/src/OpenRpg.Genres.Fantasy/Combat/BasicAttackGenerator.cs b/src/OpenRpg.Genres.Fantasy/Combat/BasicAttackGenerator.cs deleted file mode 100644 index 4db8c80..0000000 --- a/src/OpenRpg.Genres.Fantasy/Combat/BasicAttackGenerator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Linq; -using OpenRpg.Combat.Attacks; -using OpenRpg.Combat.Processors.Attacks.Entity; -using OpenRpg.Entities.Stats.Variables; -using OpenRpg.Genres.Fantasy.Extensions; - -namespace OpenRpg.Genres.Fantasy.Combat -{ - public class BasicAttackGenerator : IEntityAttackGenerator - { - public Attack GenerateAttack(EntityStatsVariables stats) - { - var damages = (stats as EntityStatsVariables)?.GetDamageReferences() - .Where(x => x.StatValue != 0) - .Select(x => new Damage(x.StatType, x.StatValue)) - .ToArray(); - - return new Attack(damages); - } - } -} \ No newline at end of file diff --git a/src/OpenRpg.Genres.Fantasy/Combat/FantasyAttackGenerator.cs b/src/OpenRpg.Genres.Fantasy/Combat/FantasyAttackGenerator.cs new file mode 100644 index 0000000..d3b8135 --- /dev/null +++ b/src/OpenRpg.Genres.Fantasy/Combat/FantasyAttackGenerator.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using OpenRpg.Combat.Abilities; +using OpenRpg.Combat.Attacks; +using OpenRpg.Combat.Extensions; +using OpenRpg.Combat.Processors.Attacks.Entity; +using OpenRpg.Core.Extensions; +using OpenRpg.Core.Utils; +using OpenRpg.Entities.Stats.Variables; +using OpenRpg.Genres.Extensions; +using OpenRpg.Genres.Fantasy.Extensions; + +namespace OpenRpg.Genres.Fantasy.Combat +{ + public class FantasyAttackGenerator : IEntityAttackGenerator + { + public float DamageVariance { get; set; } = 0.05f; + public IRandomizer Randomizer { get; set; } + + public FantasyAttackGenerator(IRandomizer randomizer) + { Randomizer = randomizer; } + + public Damage ApplyVariance(Damage damage) + { + if(damage.Value == 0) { return damage; } + + var varianceAmount = damage.Value * DamageVariance; + var randomVariance = Randomizer.Random(-varianceAmount, varianceAmount); + damage.Value += randomVariance; + return damage; + } + + public virtual (float scaledCritRate, float scaledCritMultiplier) GetScaledCriticalRateAndMultiplier( + EntityStatsVariables stats) + { + return (stats.CriticalDamageChance(), stats.CriticalDamageMultiplier()); + } + + public (bool DidCrit, Damage[] Damages) AttemptToCritical(Damage[] damages, EntityStatsVariables stats) + { + if(damages?.Length == 0) { return (false, Array.Empty()); } + + var scaledCritData = GetScaledCriticalRateAndMultiplier(stats); + var isCritical = Randomizer.ShouldCritical(scaledCritData.scaledCritRate); + if (!isCritical) { return (false, damages); } + + damages.ForEach(x => x.Value *= scaledCritData.scaledCritMultiplier); + return (true, damages); + } + + public virtual Attack GenerateAttack(EntityStatsVariables stats) + { + var damages = stats?.GetDamageReferences() + .Where(x => x.StatValue != 0) + .Select(x => ApplyVariance(new Damage(x.StatType, x.StatValue))) + .ToArray(); + + var outcome = AttemptToCritical(damages, stats); + return new Attack(outcome.Damages, outcome.DidCrit); + } + + public virtual Attack GenerateAttack(Ability ability, EntityStatsVariables stats) + { + var baseDamage = ability.Template.Variables.Damage(); + return GenerateAttack(baseDamage, stats); + } + + public virtual Attack GenerateAttack(Damage damage, EntityStatsVariables stats) + { + damage.Value += stats.GetDamageFromDamageType(damage.Type); + ApplyVariance(damage); + var outcome = AttemptToCritical(new[] { damage }, stats); + return new Attack(outcome.Damages, outcome.DidCrit); + } + } +} \ No newline at end of file diff --git a/src/OpenRpg.Genres.Scifi/Combat/ShipAttackGenerator.cs b/src/OpenRpg.Genres.Scifi/Combat/ShipAttackGenerator.cs index 93a6579..ffefb1b 100644 --- a/src/OpenRpg.Genres.Scifi/Combat/ShipAttackGenerator.cs +++ b/src/OpenRpg.Genres.Scifi/Combat/ShipAttackGenerator.cs @@ -1,5 +1,7 @@ using System.Linq; +using OpenRpg.Combat.Abilities; using OpenRpg.Combat.Attacks; +using OpenRpg.Combat.Extensions; using OpenRpg.Combat.Processors.Attacks; using OpenRpg.Genres.Scifi.Extensions; using OpenRpg.Genres.Scifi.Variables; @@ -17,5 +19,17 @@ public Attack GenerateAttack(ShipStatsVariables stats) return new Attack(damages); } + + public Attack GenerateAttack(Ability ability, ShipStatsVariables stats) + { + var baseDamage = ability.Template.Variables.Damage(); + return GenerateAttack(baseDamage, stats); + } + + public Attack GenerateAttack(Damage damage, ShipStatsVariables stats) + { + damage.Value += stats.GetDamageFor(damage.Type); + return new Attack(new[] { damage }); + } } } \ No newline at end of file diff --git a/src/OpenRpg.Genres.Scifi/Extensions/ScifiCombatReferencesExtensions.cs b/src/OpenRpg.Genres.Scifi/Extensions/ScifiCombatReferencesExtensions.cs index 2815c24..19e449a 100644 --- a/src/OpenRpg.Genres.Scifi/Extensions/ScifiCombatReferencesExtensions.cs +++ b/src/OpenRpg.Genres.Scifi/Extensions/ScifiCombatReferencesExtensions.cs @@ -42,7 +42,7 @@ public static float GetDefenseFor(this ShipStatsVariables stats, int effectType) return 0; } - public static float GetDefenseFromDamageType(this EntityStatsVariables stats, int damageType) + public static float GetDefenseFromDamageType(this ShipStatsVariables stats, int damageType) { if (damageType == ScifiDamageTypes.Ballistic) { return stats.BallisticDamage(); } if (damageType == ScifiDamageTypes.Explosive) { return stats.ExplosiveDamage(); } @@ -52,7 +52,7 @@ public static float GetDefenseFromDamageType(this EntityStatsVariables stats, in return 0; } - public static float GetDamageFor(this EntityStatsVariables stats, int effectType) + public static float GetDamageFor(this ShipStatsVariables stats, int effectType) { if (effectType == ScifiEffectTypes.BallisticDamageAmount) { return stats.BallisticDamage(); } if (effectType == ScifiEffectTypes.ExplosiveDamageAmount) { return stats.ExplosiveDamage(); }