Make a CCG – Prepare to Attack

The only way to win a match is to reduce your opponent’s hit points to zero. We currently support fatigue damage as one path toward this goal, but that is merely a battle of attrition. If we were to implement the ability for minions and heroes to attack each other, the level of strategy and fun would increase dramatically. We will lay some foundational work toward this end goal, and will create a working demo that highlights minions which are attack ready.

Go ahead and open the project from where we left off, or download the completed project from last week here. I have also prepared an online repository here for those of you familiar with version control.

Attack System

Create a new system, called the AttackSystem. The class is pretty straight forward, it is just another system that inherits from Aspect because it will be added to our system container. The class definition will look like the following:

public class AttackSystem : Aspect {
	// Add code here...
}

There will be a few notifications and public properties declared as follows:

public const string FilterAttackersNotification = "AttackSystem.ValidateAttackerNotification";
public const string FilterTargetsNotification = "AttackSystem.ValidateTargetNotification";
public const string DidUpdateNotification = "AttackSystem.DidUpdateNotification";

public List<Card> validAttackers { get; private set; }
public List<Card> validTargets { get; private set; }

This system will have the ability to grab a collection of cards that it considers “Active” for either player. In this case, an active card is one which has been played and can potentially be used to attack, or targeted in an attack. It will consist of the first hero card, and any card played to the battlefield.

public List<Card> GetActive (Player player) {
	List<Card> list = new List<Card> ();
	list.Add (player [Zones.Hero] [0]);
	list.AddRange (player [Zones.Battlefield]);
	return list;
}

With any complex system, there are of course going to be exceptions to the general rules. For example, in Hearthstone, even though a card has been played to the battlefield, it may not be able to be targeted in an attack because of another taunt minion in play. Likewise, just because there is a card on the battlefield doesn’t automatically mean it should be able to attack. It may already have attacked in the current turn, or it may be under the influence of summoning-sickness, have been frozen, or any number of other special cases that could restrict it from acting. Rather than try to account for every possibility in this one class, I decided to distribute the work by notifications. The system prepares the general list of attack and target candidates, and then other systems can subscribe to a notification that gives them a chance to filter the list based on their own criteria.

List<Card> GetFiltered (Player player, string filterNotificationName) {
	List<Card> list = GetActive (player);
	container.PostNotification (filterNotificationName, list);
	return list;
}

During a player’s turn, whenever we enter the PlayerIdle state, we will want to refresh the lists that determine what units are considered valid for attacking and for defense. This will be immediately helpful in providing an opportunity to highlight minion views for the player to know that an action can be taken, and will be helpful in the future when we try to implement a rudimentary A.I. that can take a turn as well.

public void Refresh () {
	var match = container.GetMatch ();
	validAttackers = GetFiltered (match.CurrentPlayer, FilterAttackersNotification);
	validTargets = GetFiltered (match.OpponentPlayer, FilterTargetsNotification);
	container.PostNotification (DidUpdateNotification);
}

Whenever the PlayerIdle state exits, we will want to clear these lists, because the player wont be able to take any action, and also because whatever is currently happening might affect the lists of what can attack or be attacked – they will need to be re-evaluated.

public void Clear () {
	validAttackers.Clear ();
	validTargets.Clear ();
	container.PostNotification (DidUpdateNotification);
}

Combat System

Add another new system called the CombatantSystem to handle logic specific to cards with the “ICombatant” interface.

public class CombatantSystem : Aspect, IObserve {
	// Add Code here...
}

Like usual, the class inherits from Aspect, and in this case it also implements the IObserver interface because it will be handling a couple of notifications.

public void Awake () {
	this.AddObserver (OnFilterAttackers, AttackSystem.FilterAttackersNotification, container);
	this.AddObserver (OnPerformChangeTurn, Global.PerformNotification<ChangeTurnAction> (), container);
}

public void Destroy () {
	this.RemoveObserver (OnFilterAttackers, AttackSystem.FilterAttackersNotification, container);
	this.RemoveObserver (OnPerformChangeTurn, Global.PerformNotification<ChangeTurnAction> (), container);
}

Implement the “IObserve” interface by adding the “Awake” and “Destroy” methods. We will want to observe notifications from the Attack System (to filter a list of attack candidates) and from performing a “ChangeTurnAction”.

void OnFilterAttackers (object sender, object args) {
	var candidates = args as List<Card>;
	for (int i = candidates.Count - 1; i >= 0; --i) {
		var combatant = candidates [i] as ICombatant;
		if (!CanAttack(combatant)) {
			candidates.RemoveAt (i);
		}
	}
}

In the “OnFilterAttackers” we are able to filter the list of attack target candidates that our AttackSystem provided. We will loop over the list of cards and remove any entry that doesn’t implement the “ICombatant” interface and meet some criteria necessary to be a valid attacker.

bool CanAttack (ICombatant combatant) {
	return combatant != null && combatant.attack > 0 && combatant.remainingAttacks > 0;
}

The “CanAttack” method lists the criteria for an attacker that we consider valid. First is that the combatant is not null (which would be the case for a card that didn’t implement the “ICombatant” interface). Next is that the attack stat is greater than zero (otherwise you cant do any damage anyway), and finally is that the “remainingAttacks” stat is also greater than zero.

void OnPerformChangeTurn (object sender, object args) {
	var action = args as ChangeTurnAction;
	var player = container.GetMatch ().players [action.targetPlayerIndex];
	var active = container.GetAspect<AttackSystem> ().GetActive (player);
	foreach (Card card in active) {
		var combatant = card as ICombatant;
		if (combatant == null)
			continue;
		combatant.remainingAttacks = combatant.allowedAttacks;
	}
}

In the “OnPerformChangeTurn” notification, we will do a little preparation work for the next player’s turn. We use the “AttackSystem” to get the list of “Active” cards for the player that is becoming the current player, and loop over each card to reset some stats. Assuming that the card has implemented the “ICombatant” interface, we will reset the “remainingAttacks” to be the same as the “allowedAttacks”. Note that I am not using a constant value here like ‘1’, because there are special conditions like “Windfury” that can enable a minion to attack more than once in a turn.

Destructable System

Much like we used the CombatantSystem to filter a candidate list for the Attack System, we are going to use this system to filter a list – this time the list of potential attack targets. Add the proper notifications like so:

// Add to Awake
this.AddObserver (OnFilterAttackTargets, AttackSystem.FilterTargetsNotification, container);

// Add to Destroy
this.RemoveObserver (OnFilterAttackTargets, AttackSystem.FilterTargetsNotification, container);

Then add the notification handler. We will loop through the candidates in the list and remove any that do not implement the “IDestructable” interface:

void OnFilterAttackTargets (object sender, object args) {
	var candidates = args as List<Card>;
	for (int i = candidates.Count - 1; i >= 0; --i) {
		var destructable = candidates [i] as IDestructable;
		if (destructable == null)
			candidates.RemoveAt (i);
	}
}

Game Factory

We have added two new systems and need to remember to add them to our container. Open the GameFactory class and add the following to the Create method:

game.AddAspect<AttackSystem> ();
game.AddAspect<CombatantSystem> ();

Player Idle State

The trigger for the Attack System’s updating of its lists will happen as a direct result of this state entering and exiting. I considered allowing them to trigger by the observation of a notification (like a DidEnter and DidExit state notification), but in this case, there is a specific order of operations that is needed. For example, an A.I. taking its turn, and the AttackSystem refreshing its candidate lists would both need to happen as a direct result of this state entering. If both occured as a result of a notification, then the one which was added as an observer first, would be invoked first, and a change of the order could break the expectations. Instead, I will manually refresh the attack systems list, and then allow the A.I. to take a turn knowing that all of the information it will need will already be available.

Update the “Enter” method and add an “Exit” method as follows:

public override void Enter () {
	container.GetAspect<AttackSystem> ().Refresh ();
	Temp_AutoChangeTurnForAI ();
}

public override void Exit () {
	container.GetAspect<AttackSystem> ().Clear ();
}

Game View System

The temporary game configuration we have added here (“Temp_SetupSinglePlayer”) needs to be updated once again for our demo to work. The “allowedAttacks” stat of each and every card will use a default of ‘0’ if we don’t specify something. In the loop where we create and add cards to the deck, also set the stat:

card.allowedAttacks = 1;

We can also give the hero card an ability to attack after creating it:

hero.allowedAttacks = 1;

Minion View

We will update the minion view component so that it can toggle between different sprites based on whether or not it is allowed to attack. We will track it’s “active” state via a new field:

bool isActive;

We will also want to observe the notifications from our AttackSystem for when it has finished updating its lists. We can use the “OnEnable” and “OnDisable” to add and remove the observer, and “OnAttackSystemUpdate” as our handler. We can assign our local “isActive” value based on whether or not the system’s list contains the minion that the view is representing.

void OnEnable () {
	this.AddObserver (OnAttackSystemUpdate, AttackSystem.DidUpdateNotification);
}

void OnDisable () {
	this.RemoveObserver (OnAttackSystemUpdate, AttackSystem.DidUpdateNotification);
}

void OnAttackSystemUpdate (object sender, object args) {
	var container = sender as Container;
	isActive = container.GetAspect<AttackSystem> ().validAttackers.Contains (minion);
	Refresh ();
}

Finally, update the “Refresh” method so that the sprite we assign to the avatar will be based on the current value of the “isActive” field:

avatar.sprite = isActive ? active : inactive;

Hero View

We will also need to update the code here, and it will look very similar to the updates we created for the “MinionView”. I will leave its implementation as an exercise to the reader, though if you get stuck, you can always find the solution in the sample project at the end of the lesson.

Minion View Prefab

The project asset prefab for our “Minion View” had the script disabled. This was probably due to me caching a reference in an Awake method or something and needing the hierarchy to be structured in a special way before the method was able to run. Go ahead and enable the script on the prefab and then save the project.

Demo

Run the scene. Change turns to draw cards and eventually summon some minions. On subsequent turns you should see the minion become highlighted during your idle state to show that you can attack with it (or at least will be able to when we finish implementing everything).

As a side note, on the first turn of which a minion is summoned, it will not be “active”. This is just a coincidence of our implementation and is not the result of intentionally implemented “summoning sickness”. It occurs because a card’s “remainingAttacks” stat also defaults to ‘0’ and wont be reset to the same value as the “allowedAttacks” stat until it has been in a players battlefield at the beginning of a new turn. If the feature were fully implemented, you would probably want it to exist as its own action, so that you could also display a viewer in some way to a user. This would also help cover for edge cases, such as if a card was re-summoned from a graveyard and may have already had some “remainingAttacks” value applied.

Summary

In this lesson we laid some important foundations to help support attacking. When everything is complete, cards that implement the “ICombatant” interface should be able to Attack and deal damage to other cards which implement the “IDestructable” interface. At the moment, we have a flexible system in place that determines some basic rules of what is considered an attacker or defender, and other systems can further filter that list as needed. We also display the pool of valid attackers to the user by highlighting the sprites that represent them.

You can grab the project with this lesson fully implemented right here. If you are familiar with version control, you can also grab a copy of the project from my repository.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

Leave a Reply

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