Unofficial Pokemon Board Game – Team Management

Our game supports combat now, but with no way to restore lost hitpoints (outside of cheating in the inspector panel) you wont be able to fight for long. We need some ways to manage our team including healing them, training them and even evolving them. We will implement several new systems and screens to handle all of this.

Pokemon Center

The first new system to look at is the Pokemon Center. This appears on the very first tile of the game board. It’s kind of like the “Pass Go” tile on a Monopoly board, and just like you got rewards for passing there, you get some good rewards for passing here as well.

public static class PokeCenterSystem {

	public static bool HealPokemon (Game game, Board board) {
		var currentTile = board.tiles[game.CurrentPlayer.tileIndex];
		if (currentTile.GetComponent<PokeCenter>() == null)
			return false;

		// Award candies just for visiting
		game.CurrentPlayer.candies += 100;

		bool didApply = false;
		foreach (Pokemon pokemon in game.CurrentPlayer.pokemon) {
			if (pokemon.hitPoints < pokemon.maxHitPoints) {
				HealSystem.FullHeal (pokemon);
				didApply = true;
			}
		}

		return didApply;
	}
}

The code here is pretty short because we really aren’t doing all that much. First, we figure out where the current player is located on the game board. Then we check for the “PokeCenter” component on that tile. If the component is not found we return early. Remember that we must do this step because our journey flow loop calls the PokeCenterSystem with each pass as an alternative to a notification system that would listen for moved pieces. Either approach would accomplish the same thing, but it is easier to control the order of execution and display of relevant screens by using our game loop.

When a PokeCenter tile is reached, the code is allowed to continue. As a fun little bonus, I decided to award the current player extra candies just for visiting this location. This will be helpful in evolving our Pokemon later.

The original purpose of this tile is for healing your injured Pokemon. It will examine all of your team and revive and fully heal each one no matter how much damage has been taken. This can be especially helpful later in the game, such as when you have multiple injured high level pokemon or are lacking the right items. Without this center it could take many potions and revives to get your team ready for another battle.

The code for healing your pokemon first creates a local variable called “didApply” which stores whether or not any Pokemon actually needed to be healed or not. It loops through all of the current player’s team and checks whether or not any of them have less than their maximum hit points, and if so, heals them and marks “didApply” as true. The “didApply” is then returned from the method. We will use it later to display an alert only when we actually healed something.

Poke Center State

Next, let’s take a look at the state that is responsible for invoking the Poke Center System. Here is the code:

public partial class FlowController : MonoBehaviour {

	State PokeCenterState {
		get {
			if (_pokeCenterState == null)
				_pokeCenterState = new State(OnEnterPokeCenterState, null, "Poke Center");
			return _pokeCenterState;
		}
	}
	State _pokeCenterState;

	void OnEnterPokeCenterState () {
		if (PokeCenterSystem.HealPokemon(game, board)) {
			dialogViewController.Show ("Poke Center", "Your Pokemon have all been restored!", delegate {
				dialogViewController.didComplete = delegate {
					dialogViewController.didComplete = null;
					dialogViewController.Hide(delegate {
						stateMachine.ChangeState (PokestopState);
					});
				};
			});
		} else {
			stateMachine.ChangeState (PokestopState);
		}
	}
}

By now this code hopefully looks very familiar to you. When the state enters we begin with a branch that checks whether or not the Pokemon Center System was needed for healing any Pokemon or not. When we did actually heal something we show the reusable dialog view controller with a relevant message. After the dialog is dismissed we move on to the next state. Also if no healing took place then we quietly move on to the next state without showing anything special.

Supply Award

While the Pokecenter is nice, having to wait until you have fully traversed the board to restore hit points makes the game feel a little slow. It would be frustrating to miss out on an opportunity to capture a rarely encountered pokemon just because your hit points were low. To help with this problem I have also scattered Pokestops on each side of the board. These can award a variety of items such as Pokeballs, Potions, and Revives.

In order to support the idea that some items are more rare than others, or perhaps because you simply won’t need some items as frequently as others, I wanted a way to have a weighted pool of the items you can receive at a Pokestop. To handle this I created another class called a SupplyAward. It holds the weighted “chance” of getting the item, a string that describes what you’ve earned, and a simple action that performs the giving of the item to the player.

public class SupplyAward {
	public readonly double chance;
	public readonly string award;
	public readonly Action<Player> apply;

	public SupplyAward (double chance, string award, Action<Player> apply) {
		this.chance = chance;
		this.award = award;
		this.apply = apply;
	}
}

Pokestop System

The Supply Award is used primarily by the Pokestop system. Here, I added a lazily loaded property that is a list of these awards, as well as a field to serve as the backer and another field to store the overal sum of the award weights like so:

static List<SupplyAward> AwardPool {
	get {
		if (_awardPool == null) {
			_awardPool = new List<SupplyAward>();
			_awardPool.Add( new SupplyAward(0.5, "Pokeball", delegate(Player player) { player.pokeballs++; }) );
			_awardPool.Add( new SupplyAward(0.25, "Potion", delegate(Player player) { player.potions++; }) );
			_awardPool.Add( new SupplyAward(0.15, "Revive", delegate(Player player) { player.revives++; }) );

			foreach (SupplyAward award in _awardPool) {
				wheel += award.chance;
			}
		}
		return _awardPool;
	}
}
static List<SupplyAward> _awardPool;
static double wheel;

In order to do a random weighted “roll” for one of our awards I will generate a random value called “spin” that is somewhere between 0 and the sum of the weights which is stored in the “wheel” field. Then with an incrementing field called “sum” starting from 0, I loop over each award adding the chance of catching that award. If our “sum” value is now greater than or equal to the “spin” value we generated earlier, then that is the award that our roll has selected.

static SupplyAward RandomAward () {
	double spin = UnityEngine.Random.value * wheel;
	double sum = 0;
	foreach (SupplyAward award in AwardPool) {
		sum += award.chance;
		if (sum >= spin)
			return award;
	}
	return AwardPool.First();
}

When a player passes a Pokestop, I actually provide several awards, just like in Pokemon Go. For this, which is also the method invoked by the relevant state, we will call a “GetRefreshments” method:

public static List<SupplyAward> GetRefreshments (Game game, Board board) {
	var currentPlayer = game.CurrentPlayer;
	int tileIndex = currentPlayer.tileIndex;
	if (board.tiles[tileIndex].GetComponent<Pokestop>() == null)
		return null;

	int numberOfAwards = UnityEngine.Random.Range(3, 8);
	List<SupplyAward> toGive = new List<SupplyAward>(numberOfAwards);
	for (int i = 0; i < numberOfAwards; ++i) {
		SupplyAward award = RandomAward();
		toGive.Add(award);
		award.apply(currentPlayer);
	}
	return toGive;
}

Just like with the PokeCenter, we have to check that the player is actually on a Pokestop tile, because this system will be invoked on each step of a player’s journey as part of the flows loop. For non-pokestop tiles we will simply return a null value to indicate that no refreshments can be obtained here.

Otherwise I generate a random number that indicates how many awards to give out. I create a new list to hold the awards and can initialize it with the correct capacity based on the number I decided. Then I use a loop to populate the list with random awards at each slot. I also “apply” the award at this time.

When awards are given, I wanted to provide a way to summarize what was obtained. Rather than seeing a new line item for each award, I also wanted to group similar items together so that you could simply see how many were obtained. I created a “ToString” method to handle this:

public static string ToString (List<SupplyAward> awards) {
	Dictionary<string, int> sorted = new Dictionary<string, int> ();
	foreach (SupplyAward award in awards) {
		int count = sorted.ContainsKey (award.award) ? sorted [award.award] : 0;
		sorted [award.award] = count + 1;
	}
	var items = new string[sorted.Keys.Count];
	int index = 0;
	foreach (string key in sorted.Keys) {
		int count = sorted [key];
		items [index] = string.Format ("{0}x{1}", key, count);
		index++;
	}
	return string.Join (", ", items);
}

The general idea is that I create a dictionary where the “key” is a string which happens to hold the title of the award, and the “value” is an int which holds the count of that award that has been obtained. I loop over all of the awards and add them to the dictionary or increment the value in the dictionary as necessary. Once all of the awards have been added, I loop over the keys in the dictionary and create an array of formatted strings that shows the name and count of the item. Finally I can join the array using a comma as a separator.

Pokestop State

Next we will take a look at the state which invokes the Pokestop system.

public partial class FlowController : MonoBehaviour {

	State PokestopState {
		get {
			if (_pokestopState == null)
				_pokestopState = new State(OnEnterPokestopState, null, "Pokestop");
			return _pokestopState;
		}
	}
	State _pokestopState;

	void OnEnterPokestopState () {
		List<SupplyAward> supplies = PokestopSystem.GetRefreshments(game, board);
		if (supplies != null) {
			var title = "You Collected:";
			var message = PokestopSystem.ToString (supplies);
			dialogViewController.Show (title, message, delegate {
				dialogViewController.didComplete = delegate {
					dialogViewController.didComplete = null;
					dialogViewController.Hide(delegate {
						stateMachine.ChangeState (RandomEncounterState);
					});
				};
			});
		} else {
			stateMachine.ChangeState (RandomEncounterState);
		}
	}
}

This looks very similar to the state which managed the Pokecenter. When it enters we do a branch that checks whether or not we can get any refreshments or not. If so, we display a dialog with an appropriate message and move on to the next state when the dialog is dismissed. If no refreshments are obtained we can move on to the next state quietly.

Flow Control

Now that we have some new states and systems, we can easily insert them into our current flow and bring it more in line with the one in the diagrams that I created earlier. To begin, open the “MoveState” and look for the line with the “TODO:” comment. We need to change that line from this:

stateMachine.ChangeState (RandomEncounterState); // TODO: PokeCenterState

… to this…

stateMachine.ChangeState (PokeCenterState);

… and if you played the game now, you would already see the Pokestops and Pokecenter features added to the game! But we’re not done yet. Even though you can obtain potions and revives from Pokestops we haven’t given the user a way to activate them yet. Let’s add a couple more pieces.

Potion System

Even though we have a “Heal” system, I will add another “Potion” system because the use of potions follows its own set of rules. For example, you can’t use a potion on a KO’d Pokemon, and you would actually have to have a potion in your inventory as well. However, assuming that you can use a Potion we will still tie into the Heal system to perform the effect.

public static class PotionSystem {

	public static bool CanApply (Player player, Pokemon pokemon) {
		return player.potions > 0 && pokemon.hitPoints > 0 && pokemon.hitPoints < pokemon.maxHitPoints;
	}

	public static void Apply (Player player, Pokemon pokemon) {
		HealSystem.Heal(pokemon, 20);
		player.potions--;
	}
}

Revive System

Let’s also create a system for managing the use of the revive item. Like with the Potion system, there are rules like making sure we have the correct item in the inventory. In this case we do want the pokemon’s hitpoints to be zero as a requirement.

public static class ReviveSystem {

	public static bool CanApply (Player player, Pokemon pokemon) {
		return (player.revives > 0 && pokemon.hitPoints == 0);
	}

	public static void Apply (Player player, Pokemon pokemon) {
		HealSystem.Heal (pokemon, 1);
		player.revives--;
	}
}

Power Up System

Your starter buddy will be sufficient to defeat most of the common pokemon, but perhaps not all of them, at least not without powering up first. This system allows us a way to represent the idea of you training your Pokemon so that it can perform better in combat. This system will allow the “level” stat to increase which also effects other stats like “hit points” and “CP”.

As usual, this system has its own set of rules such as that there is a “max” level, and that you must have a certain number of candies in your inventory to go along with the training. In this case, the cost to power up a pokemon is constantly changing because it is based on the level you are trying to obtain.

public static class PowerUpSystem {

	public static bool CanApply (Player player, Pokemon pokemon) {
		int nextLevel = pokemon.level + 1;
		return (nextLevel <= Pokemon.MaxLevel) && player.candies >= nextLevel;
	}

	public static int GetCost (Pokemon pokemon) {
		return pokemon.level + 1;
	}

	public static void Apply (Player player, Pokemon pokemon) {
		int oldMax = pokemon.maxHitPoints;
		pokemon.SetLevel(pokemon.level + 1);
		int delta = pokemon.maxHitPoints - oldMax;
		HealSystem.Heal (pokemon, delta);
		player.candies -= pokemon.level;
	}
}

Evolution System

Although the power up system is very helpful for gaining combat strength on your pokemon, it still wont be enough to take on the strongest pokemon in the game. In order to take on the best of the best, you will need to evolve your team into their stronger forms. This ability is controlled by another new system with its own set of rules. Not all Pokemon can evolve, and for those that can, you must have enough candy to trigger it.

public class EvolutionSystem {
	public static bool CanApply (Player player, Pokemon pokemon) {
		Evolvable evolvable = pokemon.Evolvable;
		return evolvable != null && evolvable.cost <= player.candies;
	}

	public static int GetCost (Pokemon pokemon) {
		Evolvable evolvable = pokemon.Evolvable;
		return evolvable != null ? evolvable.cost : 0;
	}

	public static void Apply (Player player, Pokemon pokemon) {
		Evolvable evolvable = pokemon.Evolvable;
		player.candies -= evolvable.cost;
		Entity target = DataController.instance.pokemonDatabase.connection.Table<Entity>()
			.Where(x => x.id == evolvable.entity_id)
			.FirstOrDefault();

		pokemon.SetEntity (target);
		pokemon.SetMoves ();
		pokemon.SetLevel (pokemon.level);
		pokemon.hitPoints = pokemon.maxHitPoints;
	}
}

Team View Controller

On the Get Set State, players have a couple of buttons they can press to direct the flow. One button is supposed to allow them to manage their team of Pokemon, and is where we will actually make use of our potions, heals, candy based evolution, and more. If you’ve been following along and added the graphics, then your team screen should look like this:

Team_zpsfuk5mnjl

Open the “TeamViewController” script. There are already a lot of fields for the various view elements like Image, Text and Button component references. In addition you will see a lot of method stubs which will be triggered by button presses. All of the links are already made in the prefab in the project for you. Some of the text is static, but most of these elements will be modified based on the currently selected Pokemon. This all occurs inside a new method called “Display” – add the following code to your script:

void Display () {

	avatarImage.sprite = CurrentPokemon.GetAvatar();
	speciesLabel.text = CurrentPokemon.Entity.label;
	cpLabel.text = string.Format("CP: {0}", CurrentPokemon.CP);
	levelLabel.text = string.Format("Level: {0}", (CurrentPokemon.level + 1));
	healthLabel.text = string.Format("HP: {0}/{1}", CurrentPokemon.hitPoints, CurrentPokemon.maxHitPoints);

	candiesLabel.text = string.Format ("Candies: {0}", currentPlayer.candies);
	evolveButton.interactable = EvolutionSystem.CanApply (currentPlayer, CurrentPokemon);
	evolutionCostLabel.text = string.Format ("Evolve ({0})", EvolutionSystem.GetCost (CurrentPokemon));

	powerUpButton.interactable = PowerUpSystem.CanApply (currentPlayer, CurrentPokemon);
	powerUpCostLabel.text = string.Format ("Power ({0})", PowerUpSystem.GetCost (CurrentPokemon));

	potionsLabel.text = string.Format("Potions: {0}", currentPlayer.potions);
	healButton.interactable = PotionSystem.CanApply(currentPlayer, CurrentPokemon);

	revivesLabel.text = string.Format("Revives: {0}", currentPlayer.revives);
	reviveButton.interactable = ReviveSystem.CanApply(currentPlayer, CurrentPokemon);

	buddyButton.interactable = pokemonIndex != 0;
	pageLabel.text = string.Format ("{0} of {1}", (pokemonIndex + 1), PokemonCount);
}

We will need a few more fields and properties for the rest of the code we will be adding soon. We will add an action, “didComplete” to let other scripts know when we are done with the screen. We will add a “pokemonIndex” to keep track of which pokemon we are currently looking at. We will need a reference to the “GameViewController” so we can update the buddy pokemon there as well. I’ll make a “CurrentPokemon” property for convenience that uses our “pokemonIndex” in the current player’s pokemon list. I will also add a “PokemonCount” property for convenience that returns the count of that same list.

public Action didComplete;
int pokemonIndex;
public GameViewController gameViewController;
Pokemon CurrentPokemon { get { return currentPlayer.pokemon[pokemonIndex]; }}
int PokemonCount { get { return currentPlayer.pokemon.Count; }}

The “Display” method above will be invoked from a variety of places, but initially it will be triggered as soon as the view controller is enabled. In addition we will use the “OnEnable” method as an opportunity to initialize our new fields.

void OnEnable () {
	gameViewController = GetComponentInParent<FlowController> ().gameViewController;
	pokemonIndex = 0;
	Display();
}

Along the top of the screen there is an “X” button to close the window. This button is also tied to the “QuitButtonPressed” method. From there we can invoke our “didComplete” action:

public void QuitButtonPressed () {
	if (didComplete != null)
		didComplete();
}

After the screen title, “TEAM”, we show some arrow buttons surrounding a “1 of 1” label. This shows which pokemon you are viewing and how many pokemon you have. Use the arrows to select other pokemon from your collection. Add the following for the arrow button functionality – it will cause the “pokemonIndex” to increment or decrement, but also to loop so that it always remains between zero and the value of PokemonCount. After selecting a new Pokemon, we need to update the screen so we also call “Display”:

public void NextButtonPressed () {
	pokemonIndex = (pokemonIndex + 1) % PokemonCount;
	Display();
}

public void PreviousButtonPressed () {
	pokemonIndex = (pokemonIndex - 1 + PokemonCount) % PokemonCount;
	Display();
}

In the body of the screen there is a tall sub section on the left with a preview of the pokemon and its basic stats as well as a button that can allow you to “Make Buddy” which is linked to the “BuddyButtonPressed” method. In the screen grab the button is disabled because the Pokemon is already the buddy. Your buddy pokemon will be sorted as the first in your list, will be used in random encounter battles, and will also be displayed as your pawn on the game board.

public void BuddyButtonPressed () {
	Pokemon current = currentPlayer.pokemon[pokemonIndex];
	currentPlayer.pokemon.RemoveAt(pokemonIndex);
	currentPlayer.pokemon.Insert(0, current);
	pokemonIndex = 0;
	gameViewController.UpdateBuddy(game.currentPlayerIndex);
	Display();
}

On the right side of the screen at the top are candy related options. A label at the top shows how many candies remain in your inventory, and the buttons beneath it show the cost to use the button in parenthesis. These buttons will only be enabled if you can use them.

When you use the “Power” button we will use the “PowerUpSystem” to let your Pokemon gain a level and enjoy higher stats. This button is tied to the “PowerUpButtonPressed” method and is implemented as follows:

public void PowerUpButtonPressed () {
	PowerUpSystem.Apply(currentPlayer, CurrentPokemon);
	Display();
}

When you use the “Evolve” button we will use the “EvolutionSystem” to let your Pokemon turn into a different kind of Pokemon. This button is tied to the “EvolveButtonPressed” method and is implemented as follows:

public void EvolveButtonPressed () {
	EvolutionSystem.Apply(currentPlayer, CurrentPokemon);
	gameViewController.UpdateBuddy(game.currentPlayerIndex);
	Display();
}

In the middle right of the screen you can see the number of potions you have. Assuming you can use the Potion system, the button will be active. Using a potion will restore hit points.

public void HealButtonPressed () {
	PotionSystem.Apply(currentPlayer, CurrentPokemon);
	Display();
}

At the bottom right of the screen you can see the number of revives you have. If you can use this system, a Pokemon will go from a KO state (zero HP) to a critical state (one HP). Then you will be able to further heal it with potions.

public void ReviveButtonPressed () {
	ReviveSystem.Apply(currentPlayer, CurrentPokemon);
	Display();
}

Team State

For showing the “TeamViewController”, we will use the “TeamState”. It is a simple state that upon “Enter” will show the view controller, and exit back to the “GetSetState” when the user dismisses that screen.

public partial class FlowController : MonoBehaviour {

	State TeamState {
		get {
			if (_teamState == null)
				_teamState = new State(OnEnterTeamState, null, "Team");
			return _teamState;
		}
	}
	State _teamState;

	void OnEnterTeamState () {
		teamViewController.Show (delegate {
			teamViewController.didComplete = delegate {
				teamViewController.didComplete = null;
				teamViewController.Hide(delegate {
					stateMachine.ChangeState(GetSetState);
				});
			};
		});
	}
}

Get Set State

One last step and we’ll be done. Our “GetSetState” needs to properly link to the “TeamState”. There is already a “TODO” comment that marks what needs to be updated. Simply change this:

stateMachine.ChangeState (NextPlayerState); // TODO: TeamState

… to this:

stateMachine.ChangeState (TeamState);

Demo

Now would be a great time to play the game. It feels almost complete by now! Pass a pokestop and gather supplies. Then, capture a pokemon from a random encounter. On your next turn, use the “GetSetState” to look at your team. You can view your starter pokemon as well as your newly captured pokemon. If you want you can change your buddy to the new pokemon. As you travel and catch pokemon you are also obtaining candy which you can use to power up and even evolve your pokemon. You should be able to defeat any pokemon in the game by now, but there still isn’t a way to actually win. You’ll just have to enjoy the journey for now.

Summary

In this lesson we added several new features each of which was built from smaller pieces like systems, states, and view controllers. We added a PokeCenter for healing your entire team, PokeStops for collecting inventory items, and all of the systems and the screen necessary for using inventory items like Potions and Revives, and to handle Powering up and Evolving your Pokemon. All of these play an important part of team management, and add significantly to the experience while playing the game.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *