Let's Make Em Fight
This is part 5 of a series, and expands on systems created in Part 2
Pokemon's battle system is actually rather unique for a turn-based game. Most games have a distinct turn for each side of combat, but with Pokemon, there's a "prep" phase and then a "combat" one.
Essentially, both sides pick their moves, the game determines which one goes first, then each one performs their moves. This has the advantage of incorperating "priority", where some moves can always go first/last, and some can have specific conditions relating to if they were indeed first or last.
For a traditional turn-based system, you're able to react to the enemy's turn (either previous or oncoming) without much issue. In a game like Pokemon, each turn is a risk. It makes speed singificantly more important, which gives fast Pokemon an edge over the slow ones.
In Legend's Arceus, there's an interesting hybrid approach with Turn Order.

This turns speed and priority into a concept where it determines that Pokemon can attack an additional time (I saw 3 times once as well)
Personally, I prefer this approach, so we're gonna do something like that.
The State Machine
State Machines serve as branches for code, allowing you to decide logic based on what is expected at that moment.
Our approach is going to be rather simple. We'll have an enum for the states (so it can be extended) and a dictionary that stores the functions that the states are tied to. If you remember Fen, we're going to basically do something similar to the way we set up functions. We'll also have a context class for states to share between.
public enum State { Start, Summon, SummonAnim };
public State state;
public struct Context
{
public Battler user;
}
public Context ctx = new();
public Dictionary<State, Action> states = new()
{
{State.Start, () => { } },
{State.Summon, () => Summon() },
{State.SummonAnim, () => SummonAnim() }
};
static Battler user => instance.ctx.user;To call up the state we want to do, we'll add:
private void Update()
{
states[state]();
}This invokes the action related to the state every frame
So what we're going to do now is a little "refactoring", basically cleaning up the code to make it more compatible with the way our new system works. So we're going to change StartBattle to this:
public Queue<Battler> queue = new();
public void StartBattle()
{
queue.Enqueue(enemy);
queue.Enqueue(player);
state = State.Summon;
}
Explanation
Queues are weird. You can only enqueue one item at a time, and when you retrieve an item via "Dequeue" you remove it from the queue. It's exactly the same as removing an item from the top of a list, just this strange little dedicated method.
Now for the states. First, Summon.
Summon
//Battler class
public MonsterCard card;
public enum Side { Player, Opponent };
public Side side;
public int index;
//BattleManager class
static IndividualMonster GetMonster(Battler.Side side, int index) => (side == Battler.Side.Player ? GameManager.party : instance.encounterParty)[index];
static void Summon()
{
if (instance.queue.Count > 0)
{
instance.ctx.user = instance.queue.Dequeue();
user.card.Set(GetMonster(user.side, user.index));
user.gameObject.SetActive(true);
user.transform.localScale = Vector3.zero;
instance.state = State.SummonAnim;
}
}Explanation
- We add the queued battler to the context
- GetMonster grabs the monster based on the set index and the "side". Side is defined via the inspector.
- The Battler gameobject is enabled, and its scale is set to zero
- The state is set to "SummonAnim"
Then SummonAnim.
SummonAnim
//Battler class
public Vector3 scale;
public float scaleDistance => Vector3.Distance(scale, transform.localScale);
//Battlemanager class
static void SummonAnim()
{
if (user.scaleDistance > .01f)
user.transform.localScale = Vector3.Lerp(user.transform.localScale, user.scale, Time.deltaTime * 7);
else
{
user.transform.localScale = user.scale;
instance.state = State.Summon;
}
}Explanation
- Lerps usually don't reach the goal, so we go for a Heuristic approach here.
- user.scale is set in the inspector and different for each battler
- if the goal is reached, we set the scale to the user.scale, then set the state back to State.Summon
This gives us...
Not bad, right? Now that that's done, we need to check the turn order.
Turn Order
//Battler class
public int priority;
//BattleManager class
public Image[] queueIcons;
public Color playerColor, enemyColor;
static void GetTurnOrder()
{
var q = instance.queue.ToArray();
for(int i = 0; i < q.Length; i++)
{
instance.queueIcons[i].color = q[i].side switch
{
Battler.Side.Player => instance.playerColor,
_ => instance.enemyColor,
};
instance.queueIcons[i].transform.GetChild(0).GetComponent<Image>().sprite = q[i].monster.data.face;
}
if (instance.queue.Count == 6) { instance.state = State.StartTurn; return; }
var averageSpeed = (instance.player.monster[StatType.Speed] + instance.enemy.monster[StatType.Speed]) / 2;
instance.player.priority += instance.player.monster[StatType.Speed];
instance.enemy.priority += instance.enemy.monster[StatType.Speed];
var next = instance.player.priority > instance.enemy.priority ? instance.player : instance.enemy;
instance.queue.Enqueue(next);
next.priority -= averageSpeed;
Debug.Log($"Next: {next}");
}Explanation
- We color the icons and give them face of the next monster (this repeats every time the queue is changed)
- Next, we check if there's 6 turns queued up. We reuse the queue we used for summoning
- We grab the average speed between both monsters
- The battler's priority is then added by the monster's speed stat
- we compare both the player and the monster's speed, then queue up the higher of the two "next"
- Finally, the priority is subtracted by the average speed.

Turns
With the queue implemented, it's time to actually do the turns.
Enemy Turn
At the start of the enemy's turn, we need to have the monster decide what it's going to do. There's a lot I could do here, but right now I'm just going to grab the first skill in their movelist, just to make sure everything is functional.
//Context struct
public Battler target;
public Skill skill;
//Battler class
public BattleManager.Context GetBestMove(BattleManager.Context ctx)
{
ctx.target = BattleManager.instance.player;
ctx.skill = monster.skills[0];
return ctx;
}
//BattleManager class
static void EnemyTurn()
{
instance.ctx = user.GetBestMove(instance.ctx);
instance.state = State.MoveTowards;
}One it decides the move, we'll have the enemy move towards the player.
//Battler class
public float moveDistance = 2;
//BattleManager class
static void MoveTowards()
{
var distance = Vector3.Distance(user.transform.position, target.transform.position);
if (distance > user.moveDistance)
{
user.transform.position = Vector3.Lerp(user.transform.position, target.transform.position, Time.deltaTime * (7 / user.moveDistance));
}
else
{
instance.state = State.PerformMove;
}
}Then we do the damage calculation. We're just gonna do some basic stuff for now.
//Skill class
public int Offense(Stat[] stats) => (range, type) switch
{
(Range.Melee, PowerType.Physical) => stats[(int)StatType.Attack].value + stats[(int)StatType.Strength].value,
(Range.Ranged, PowerType.Physical) => stats[(int)StatType.Attack].value + stats[(int)StatType.Dexterity].value,
(Range.Melee, PowerType.Magical) => stats[(int)StatType.Magic].value + stats[(int)StatType.Strength].value,
(Range.Ranged, PowerType.Magical) => stats[(int)StatType.Magic].value + stats[(int)StatType.Dexterity].value,
_ => 0,
};
public int Defense(Stat[] stats) => (range, type) switch
{
(Range.Melee, PowerType.Physical) => stats[(int)StatType.Defense].value + stats[(int)StatType.Luck].value,
(Range.Ranged, PowerType.Physical) => stats[(int)StatType.Defense].value + stats[(int)StatType.Speed].value,
(Range.Melee, PowerType.Magical) => stats[(int)StatType.Resistance].value + stats[(int)StatType.Luck].value,
(Range.Ranged, PowerType.Magical) => stats[(int)StatType.Resistance].value + stats[(int)StatType.Speed].value,
_ => 0,
};
//BattleManager class
public int Eval(BattleManager.Context ctx)
{
return (int)((Offense(ctx.user.monster.stats) - (Defense(ctx.target.monster.stats) / 2)) * (power / 100f));
}
Then we apply it.
//MonsterCard class
public void Update()
{
hpSlider.value = Mathf.Lerp(hpSlider.value, battler.monster.hp, Time.deltaTime * 3);
mpSlider.value = Mathf.Lerp(mpSlider.value, battler.monster.sp, Time.deltaTime * 3);
hpValue.text = ((int)hpSlider.value).ToString();
mpValue.text = ((int)mpSlider.value).ToString();
}
//Battler class
public void TakeDamage(int damage)
{
monster.hp -= damage;
card.hp = monster.hp;
}
//BattleManager class
static void PerformMove()
{
target.TakeDamage(skill.Eval(instance.ctx));
user.monster.sp -= skill.cost;
instance.state = State.Return;
}And finally, we return to our original position.
//Battler class
public Vector3 start;
void OnEnable()
{
start = transform.position;
}
//BattleManager class
static void Return()
{
var distance = Vector3.Distance(user.transform.position, user.start);
if (distance > .01f)
{
user.transform.position = Vector3.Lerp(user.transform.position, user.start, Time.deltaTime * 7);
}
else
{
instance.state = State.TurnOrder;
}
}Put it all together, and we get...

Player Turn
The good news is that we handled most of the logic to get the enemy's turn working, so all the player turn needs to be right now is hooking the skill cards to a function that adds our chosen skill to the context. And because literally everything has been set up, that's just:
//Skill Card
public void SetSkill()
{
BattleManager.instance.SetPlayerSkill(id);
}
//BattleManager
public void SetPlayerSkill(int id)
{
ctx.target = enemy;
ctx.skill = player.monster.skills[id];
state = State.MoveTowards;
skillPage.SetActive(false);
}

Which leads us to this:
And there we go. The groundwork for our battle system is done. Next time, we're going to tackle Attack, Guard, and the Party options.
Member discussion