Spells and abilities system

Hi.
Have some trouble with abilities architecture.
I started with base class

class Ablity{
  public int mana_cost;
  public int cool_down;
}
class DirectDamageAbilit:Ability{
  virtual int calc_damage(){}
}

let’s say I’ve got 3 abilities : Swift Strike, Power Strike, Calculated strike.
They share common pattern : got mana cost, cooldown, and damage output.
However the method calc_damage is different for each one (first scale with dexterity, second with strength, last with weapon damage).
So, what is the best way to organize this?
Simplest decision is to inherit :

class SwiftStrike:DirectDamageAbility{
override int calc_damage(){}
}

But I will end with 40++ files, one for each ability. It doesn’t look like good idead to me.
I can’t see how ScriptableObject can help here, because of dynamic function for damage calcalution.

You’ll still end up with 40+ Scriptable files with this method, but to clarify your last concern you could use a scriptable and still have a dynamic function for the damage calculation. But it could help with organizing a bucket load of abilities in the project and simplify balancing them.

.

The benefit to this is that Scriptable objects can be easily created and Ability will just become a script you can slap on when you have a character that needs another ability, as opposed to creating a class for each one. Also it gives you access to change damage,cast times, mana cost…etc in the project window for very quick balancing.

.

Also note that I added a public string id to the scriptable object. You could create a Singleton Instance for an AbilityManager that has a public Dictionary< string,Ability>(), and then reference the Manager whenever you need an ability in the game from a character or mob. (Instead of having the abilities on the object directly). This will help manage the large amount of files!

.

So, the scriptable:

[CreateAssetMenu(filename = "New Ability", menuName = "Ability")]
class AbilityData : ScriptableObject
{
  public string id;
  public int baseDamage;
  public int elementalDamage;
  [0,100] public float critChance;
  public int damageMultiplier;
  public int manaCost;
  public int cooldown;

  public GameObject castEffectPrefab;
  public GameObject hitEffectPrefab;
  public GameObject critEffect;

  // I'll put this here but you'll probably want the enum publicly reference-able elsewhere
  public enum ElementalDamageType { Lightning, Fire, Ice, Bludgeoning, Slashing }
  public ElementalDamageType myElementalDamageType;
}

And then the ability class which you can drag onto an empty gameObject in heirarchy either on to the player/enemy directly or into an AbilityManager.

class Ablity : MonoBehaviour
{
  public AbilityData data;
  private bool weCrit;

  // If AbilityManager
  private void Start()
  {
     // Make sure we don't add the same ability twice
     if(AbilityManager.Instance.Abilities.Contains(data.id) == false)
       AbilityManager.Instance.Abilities.Add(data.id, this);
     else Debug.LogError("[Ability] Attempted to add a duplicate of " + data.id);
  }

  // What to do if we Cast
  public int Cast(/*params CastPoint, Caster*/)
  {
     if(Player.totalMana > data.manaCost)
     {
       Caster.totalMana -= data.manaCost;  
       Instantiate(data.castEffectPrefab, CastPoint.pos, CastPoint.rot);
       return data.cooldown;
     }
     else return 0;
  }

  // What to do if we hit something
  public int OnHit(/*params TransformHit, HitResistances, Caster*/)
  {
    int Damage = CalculateDamage(HitResistances, Caster);
    Instantiate(data.hitEffectPrefab, TransformHit.pos, TransformHit.rot);
    if(weCrit) Instantiate(data.critEffectPrefab, TransformHit.pos, TransformHit.rot);
     // Or maybe a sparkle on the caster 
     //Instantiate(data.critEffectPrefab, Caster.transform.pos, Caster.transform.rot);
    weCrit = false; // reset
    return Damage;
  }

  // Example Calculation
  private int DamageCalculation(/*params Resistances, Caster*/)
  {
    // int damage = data.baseDamage;
    // if not resistant to data.myElementalDamageType
       // damage += resultantElementalDamage;
    // else Calculate residual damage after resistance from elemental?
   
    // float baseCritChance = data.CritChance
    // Maybe add some crit chance from items: baseCritChance += Caster.CritFromItems;
    // if(Random.Range(0, 100) < data.critChance)
      // damage *= damageMultiplier;
      // weCrit = true;
    // else didn't crit

    return damage;
  }
}