If you’ve been playing the game much since the previous lesson, you may have encountered a few Pokemon that you really wanted to capture. In this game, I decided to make capturing a Pokemon harder than simply throwing a Pokeball at it. You actually have to fight with the wild Pokemon to weaken it before you can capture it. Implementing combat completely is pretty involved, so in this lesson we will just do some setup work by creating a few models, factories and systems we will want to use to complete a capture battle.
ControlModes
All battles are turn-based and will occur between a player and a very simple computer controlled A.I. opponent. I will need a way to differentiate between the two so that I can show UI for the player and automate attacking for the computer. For this, I added another enum. Update the script locaed at Scripts/Enums/ControlModes.cs with the following:
public enum ControlModes { Player, Computer }
Combatant
Update the script located at Scripts/Model/Combatant.cs with the following:
public class Combatant { public List<Pokemon> pokemon = new List<Pokemon>(); public ControlModes mode; public double waitTime; public int currentPokemonIndex; public Pokemon CurrentPokemon { get { return pokemon[currentPokemonIndex]; }} }
The combatant holds information specific to a participant in a battle. A player can be used to create a combatant, but I don’t use a player object directly – any given player may actually have a team of many Pokemon, but a combatant will have at most a team of two specifically chosen pokemon. Likewise, I need a combatant representation for a gym leader, and even for a lone pokemon that you have encountered in the wild and wish to try to capture. This class will be able to represent all of the above.
Besides the list of pokemon that are actively engaged in battle, this will also hold information such as the “mode” which indicates whether battle decisions are made by the current player or by an AI, the “waitTime” which indicates the cooldown time before the current active pokemon can attack again, and “currentPokemonIndex” to indicate which of the pokemon is the active one.
Combatant Factory
We’ll be using a factory to create our combatants. For now we will add two methods. First we will add a “Create” method that creates a combatant out of the current player. You pass the player to the factory and it will automatically use the player’s first Pokemon (the buddy) as the attacker. The control mode will be set to the “Player” enum type so that we will know to let the player decide how to act on his turns. We will also start with a bit of a waitTime. This will be used so that the speed of a Pokemon’s attack will determine who gets to go first.
public static Combatant Create (Player player) { Combatant retValue = new Combatant(); retValue.pokemon.Add(player.pokemon[0]); retValue.mode = ControlModes.Player; retValue.waitTime = retValue.CurrentPokemon.FastMove.duration; return retValue; }
Next we will create another “Create” method, but this time we will pass a lone Pokemon instead. This will be used to create a combatant to represent the wild Pokemon that we are entering a capture battle against. We will pass the wild Pokemon as a parameter which will also become the attacker on this team. The control mode is set to the “Computer” enum type so that on its turn we will know not to show a command menu and can simply automate an attack instead. Like before I update the waitTime based on the speed of its attack.
public static Combatant Create (Pokemon pokemon) { Combatant retValue = new Combatant(); retValue.pokemon.Add(pokemon); retValue.mode = ControlModes.Computer; retValue.waitTime = pokemon.FastMove.duration; return retValue; }
Battle
Update the script located at Scripts/Model/Combatant.cs with the following:
using System.Collections.Generic; using UnityEngine; using ECS; using System; public class Battle { public List<Combatant> combatants = new List<Combatant>(); public Move move; public int lastDamage; public Combatant attacker { get { return combatants [0]; } } public Combatant defender { get { return combatants [1]; } } }
This class holds information specific to any given battle. It is used both for gym battles and for random encounter battles. It holds the list of combatants that are fighting, and holds “move” and “lastDamage” fields so that my various states and screens can display or operate on whatever information is needed. As a convenience and to help make some of the code more readable, I added properties for the “attacker” and “defender” instead of needing to grab them elsewhere by index which is a bit less intuitive. You might have guessed that this means that the combatants list will be sorted and the items in the list will change indicies as the battle progresses.
Battle Factory
We’ll be creating battles using a factory as well. Like before we will provide a “Create” method with parameters that indicate what kind of battle to make. For a capture battle we will provide an instance of “Player” for the current player, and an instance of “Pokemon” for the wild spawned Pokemon that we wish to catch. This method will then pass those parameters on to the Combatant Factory in order to generate the list of participating combatants.
public static Battle Create (Player player, Pokemon wildPokemon) { Battle retValue = new Battle (); retValue.combatants.Add (CombatantFactory.Create(player)); retValue.combatants.Add (CombatantFactory.Create(wildPokemon)); return retValue; }
Data Controller
As we have done with the “Game” and “Board”, let’s add a field to the “DataController” for our “Battle” so that it is easily accessible from anywhere that needs it:
public Battle battle;
Flow Controller
To help simplify some code, I also make wrapper properties in the FlowController for the reference to the DataController’s “battle” field like so:
Battle battle { get { return dataController.battle; } set { dataController.battle = value; } }
Base View Controller
Once again, we will add more wrapper properties to the BaseViewController
protected Battle battle { get { return DataController.instance.battle; } } protected Player currentPlayer { get { return game.CurrentPlayer; } }
Capture System
It is possible to attempt a capture of a Pokemon without KO’ing it first. However the more of a Pokemon’s hit points you can reduce, the better your chances to capture it. When the capture is actually attempted, the Pokemon will be allowed up to three attempts to try and escape. In this phase I basically roll a random number and see if that number minus its hit point ratio ever falls below zero, and if so, the escape will succeed.
For example, let’s suppose that you have KO’d the opponent. Then the chance of escape is the random roll (a value between 0 and 1) minus the HPRatio (which will be zero). Regardless of the roll, you will never get a value less than zero, so the Pokemon will be unable to escape.
In another scenario, imagine that you only reduced the opponents HP by half. Now the chance of escape is the random roll (a value between 0 and 1) minus the HPRatio (which will be 0.5). A low roll could result in an escape (0 – 0.5 is less than 0), but a high roll would not (1 – 0.5 is greater than 0).
public static bool TryEscape (Pokemon pokemon) { var chance = UnityEngine.Random.value - EasingEquations.EaseInOutCubic(0f, 1f, pokemon.HPRatio); return chance < 0; }
When all of the attempts to escape have failed, we can use the same system to complete the capture. In this method we decrement the number of pokeballs available to the user, award a bonus amount of candies, add the newly captured Pokemon to the player’s team, and just to be nice, we go ahead and heal the newly captured Pokemon so it is immediately usable if desired.
public static void Capture (Player player, Pokemon pokemon) { player.pokeballs--; player.candies += 25; player.pokemon.Add (pokemon); HealSystem.FullHeal(pokemon); }
Heal System
The heal system is pretty simple – it merely boosts the hitPoints stat of a Pokemon while making sure to clamp the amount it can award to the maxHitPoints stat. This system can be invoked from a variety of sources, such as a Pokecenter, or Potion. In this lesson we use it after capture.
public static class HealSystem { public static void FullHeal (Pokemon pokemon) { Heal(pokemon, pokemon.maxHitPoints); } public static void Heal (Pokemon pokemon, int amount) { pokemon.hitPoints = Mathf.Min(pokemon.maxHitPoints, pokemon.hitPoints + amount); } }
Combat System
The system needed for combat is a little more complex than the other systems. It handles things like turn order, how the computer picks moves, damage algorithms, etc. Since there is quite a bit of material, we will just look at one bit at a time.
First, is a method called “Next”. This method is called between “turns” of battle where either the player or opponent gets to perform a move. Here we sort the battle’s list of combatants in an ascending order based on the “waitTime” of each combatant. Then, we reduce the waitTime of both combatants by the least amount of remaining waitTime we found. This is sort of a battle timer. After a combatant performs a move their waitTime field will be incremented again which likely means that on the next turn the defender will become the attacker. However when a particularly slow move such as a charge move is performed, it may allow the other Pokemon a chance to attack repeatedly before the first Pokemon can attack again.
public static void Next (Battle battle) { battle.combatants.Sort((c1, c2) => c1.waitTime.CompareTo(c2.waitTime)); battle.defender.waitTime -= battle.attacker.waitTime; battle.attacker.waitTime = 0; }
The “PickMove” method is the A.I. for our computer opponent. It couldn’t be much simpler. If the Pokemon has enough energy to perform a charge move, then that is what it will pick. Otherwise, it will simply use the fast move.
public static Move PickMove (Battle battle) { var pokemon = battle.attacker.CurrentPokemon; return (pokemon.energy >= Mathf.Abs(pokemon.ChargeMove.energy)) ? pokemon.ChargeMove : pokemon.FastMove; }
Regardless of how a move is picked (whether by human input or by A.I. selection), the “ApplyMove” method provides a single place to apply the result. This means using a complex algorithm for calculating the damage, then updating the hit points on the defender as well as the energy levels of the attacker. Once the final damage has been calculated, we store the value in the battle object’s “lastDamage” field so that we can display the value in the battle screen. Finally, we also need to increment the waitTime of the attacker based on the duration of whichever move was chosen so that the attacker may need to wait a turn(s) before attacking again.
public static void ApplyMove (Battle battle) { var attacker = battle.attacker; var defender = battle.defender; var move = battle.move; var attack = attacker.CurrentPokemon.Attack; var defense = defender.CurrentPokemon.Defense; var stab = GetSameTypeAttackBonus(move, attacker.CurrentPokemon.Stats); var weakness = GetTypeMultiplier(move, defender.CurrentPokemon.Stats); var damage = Mathf.Floor(0.5f * attack / defense * move.power * stab * weakness) + 1; battle.lastDamage = (int)damage; defender.CurrentPokemon.hitPoints = (int)Mathf.Max(0, defender.CurrentPokemon.hitPoints - damage); attacker.CurrentPokemon.energy = (int)Mathf.Min(attacker.CurrentPokemon.energy + move.energy, 100); attacker.waitTime += move.duration; }
As part of the algorithm for determining an attack’s damage, we check for a bonus based on the type of the move and the type of the Pokemon which used it. When they share the same type, then the damage will be greater. For example, a fire pokemon using a fire type move should be better at using that kind of move.
static float GetSameTypeAttackBonus (Move move, SpeciesStats attacker) { return (move.type == attacker.typeA || move.type == attacker.typeB) ? 1.25f : 1f; }
Besides the bonus for checking the move’s type with the attacker, there is also a potential multiplier when considering the type of the defender. Different Pokemon may have a weakness to certain move types, or a resistance to other move types.
static float GetTypeMultiplier (Move move, SpeciesStats defender) { double multiplier = 1; var connection = DataController.instance.pokemonDatabase.connection; var typeMultiplier = connection.Table<TypeMultiplier>() .Where(x => x.attack_type_id == move.type && x.defend_type_id == defender.typeA) .FirstOrDefault(); if (typeMultiplier != null) { multiplier = typeMultiplier.value; } if (defender.typeA != defender.typeB) { typeMultiplier = connection.Table<TypeMultiplier>() .Where(x => x.attack_type_id == move.type && x.defend_type_id == defender.typeB) .FirstOrDefault(); if (typeMultiplier != null) { multiplier *= typeMultiplier.value; } } return (float)multiplier; }
After a KO, we need an opportunity to swap out the active Pokemon for any given combatant. The “SwapIfNeeded” method will check for KO, and when finding one, will increment the “currentPokemonIndex” where possible. This wont apply to our “one on one” capture battles, but will be needed later on when we add gym battles.
public static bool SwapIfNeeded (Battle battle) { var defender = battle.defender; if (defender.CurrentPokemon.hitPoints == 0) { if (defender.currentPokemonIndex + 1 < defender.pokemon.Count) { defender.currentPokemonIndex++; defender.waitTime = defender.CurrentPokemon.FastMove.duration; return true; } } return false; }
Summary
In this lesson we started some of the pre-requisite work for combat. We added enum, model, factory and system classes that will all be used. There was a lot of setup done, but don’t have anything to show for it just yet because this feature is such a large one. Soon we will add the view controllers and states necessary to tie everything together, but I felt like the lesson had already gotten long enough for now.
Don’t forget that there is a repository for this project located here. Also, please remember that this repository is using placeholder (empty) assets so attempting to run the game from here is pretty pointless – you will need to follow along with all of the previous lessons first.