Before answering I'd like to stress 2 things: 1) I am not familiar with Unity, but AFAIK it uses ECS, so some implementation details may vary; 2) it is an overengineering, but points out seperate concepts and it is for you to decide whether they must be separated or can be merged for simplicity.
I'd structure it as follows:
1. It is not only skills and spells. A command is executed after pressing a key. So you need a class that maps keys to commands and commands to execute. Commands are classes derived from an interface with only one virtual function "execute". You can read about what command pattern is from here: (http://gameprogrammingpatterns.com/command.html it actually partially describes your problem).
interface Command
{
void execute();
}
class CommandsMap
{
Dictionary<Keys, Command> m_commands;
public MapCommand(Keys k, Command c) { m_commands[k] = c; }
public ProcessInput(Keys k) { m_commands[k].execute(); }
}
In appropriate place (during loading, etc) you fill CommandsMap with Commands and associated keys.
2. Spells/skills are not necessarily commands. Spells/skills are stand-alone entities that are used by Commands. Spells be like:
class TargetedSpell
{
protected GfxEffect m_targetEffect, m_casterEffect;
public Actor Caster { get; set; };
public Actor Target { get; set; };
protected virtual bool CheckPreconditions(); // check if immune etc.
protected virtual void DoSpellOnTarget(); // apply damage etc.
public void CastSpell()
{
if (CheckPreconditions())
{
m_targetEffect->PlayOn(m_target);
m_casterEffect->PlayOn(m_caster);
DoSpellOnTarget();
}
}
public TargetedSpell(GfxEffect casterEffect, GfxEffect targetEffect)
...
}
class DamagingTargetedSpell : public TargetedSpell
{
protected DamageType m_damageType;
protected int m_damageAmmount;
protected override bool CheckPreconditions() { return m_target.IsImmuneTo(m_damageType); }
protected override void DoSpellOnTarget() { m_target.ApplyDamage(m_damageAmmount); }
public DamagingTargetedSpell(DamageType damageType, int damageAmmount, GfxEffect actorEffect...)
}
If you have 100 spells it is not necessary to have 100 spell classes. Here for Fireball, Lighting, Meteor you only need one DamagingTargetedSpell with 3 different gfx effects.
One thing to mention. Spells here work with Caster and Target of Actor type, but Commands (may) know nothing about them. Well, some Commands may know that the Caster is a player, but Target is unknown during creation of commands. Thus we need a class that will give as an information about a target. For example, a Pointer class that returns current actor under cursor (if any). This way creation of commands may look like this:
CommandsMap map = new CommandsMap; // somewhere
...
Pointer pointerToGetTarget = new Pointer;
...
map.MapCommand(ReadKeyForCommandFromOptions("CastFireball"), new CommandThatKnowsHowToCastSpells(pointerToGetTarget, new DamagingTargetedSpell(DamageType::Fire, 20, new GfxEffectFlames, ...)));
map.MapCommand(ReadKeyForCommandFromOptions("CastLighting"), new CommandThatKnowsHowToCastSpells(pointerToGetTarget, new DamagingTargetedSpell(DamageType::Electricity, 20, new GfxEffectShock, ...)));
3. CommandsMap should not know about whether spell/skill is unlocked (it is now obvious, cause instead of spells/skills we have Commands). But spell/skill class should too not know whether it is locked or unlocked, as it is out of its responsibilities. Command should. Thus we introduce CheckIfUnlockedCommand that uses chain-of-responsibility pattern.
class CheckIfUnlockedCommand
{
protected int m_unlockTableID; // index in locks table
protected Command m_nextCommand;
protected LocksTable m_locks;
public void execute()
{
if (m_locks.IsLocked(m_unlockTableID))
{
m_nextCommand.execute();
}
}
public CheckIfUnlockedCommand(int id, Command nextCommand)
...
}
Then the command creation code is:
map.MapCommand(ReadKeyForCommandFromOptions("CastFireball"), new CheckIfUnlockedCommand(GetIDFor("CastFireball"), new CommandThatKnowsHowToCastSpells(pointerToGetTarget, new DamagingTargetedSpell(DamageType::Fire, 20, new GfxEffectFlames, ...))));
In future you might want to add cooldown alongside with unlock check and you'll not need to modify command or spell classes, just an initialization code or if you'll implement command creation from a data (xml, for example), you'll only need to modify a data.
As I already said it is overengineering. For example, if you know that every spell and skill is unlockable and you only need spells and skills you may end up with something like:
class SpellsSkillsMap
{
...
void ProcessKey(Keys k)
{
SpellOrSkillClass c = m_commands[k];
if (c != null && m_locksTable.IsUnlocked(c.ID))
{
c.Perform();
}
}
}