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;
}
}