Make a CCG – Damage

In the last lesson, we implemented the Fatigue stat – caused by attempting to draw cards from an empty deck. However, although we could watch the stat change, it had no real effect on the game itself. In this lesson, we will add a reaction to fatigue which applies damage to the player’s hero by the fatigue amount. As a result, we have the means to determine when a game will end – whenever a hero’s hit points have dropped to zero.

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.

Damage Action

We’ll begin by creating the Game Action itself. It is pretty simple:

[csharp]
public class DamageAction : GameAction {
public List targets;
public int amount;

public DamageAction(IDestructable target, int amount) {
targets = new List (1);
targets.Add (target);
this.amount = amount;
}

public DamageAction(List targets, int amount) {
this.targets = targets;
this.amount = amount;
}
}
[/csharp]

For this action, we really need to know two bits of information: what to apply the damage to, and how much damage to apply. You might be wondering why I made the “targets” into a “List” instead of just a single “IDestructable” reference. Primarily, this is because I am thinking to the future about how damage will be applied in various kinds of actions. For example, a regular attack from one minion to another would only need the single reference, and could also animate the damage one at a time. At the other end of the spectrum, a card like “Arcane Explosion” deals damage to all enemy minions. When the sequence plays out, you expect to see all of the minions be damaged simultaneously, and afterward any sort of damage reaction can take place. If I applied damage one at a time, then other actions might react and interrupt the first action giving it a different sort of feel.

Even though the action always works with a List of targets, I decided it would be nice to provide two constructors. Obviously there is one where you can pass the List directly, but I also created one which allows you to pass a single target instead of requiring you to wrap it in a list first.

Destructable System

As always, we will need a system to handle the application of our new action. In this case, the action applies specifically to any card that implements the “IDestructable” interface, so I decided to create a similarly named system to handle any interaction with that interface.

[csharp]
public class DestructableSystem : Aspect, IObserve {
public void Awake () {
this.AddObserver (OnPerformDamageAction, Global.PerformNotification (), container);
}

public void Destroy () {
this.RemoveObserver (OnPerformDamageAction, Global.PerformNotification (), container);
}

void OnPerformDamageAction (object sender, object args) {
var action = args as DamageAction;
foreach (IDestructable target in action.targets) {
target.hitPoints -= action.amount;
}
}
}
[/csharp]

Following the usual pattern, we made the system an aspect, so that we can add it to the same container as our other systems. We also implement the “IObserve” interface so that it has a convenient place to register for, and unregister from, our action notifications.

In the notification handler method, I have provided a simple implementation for the application of damage. More complete versions will need to consider exceptions like armor as well as abilities that might be active such as if a card is immune to damage. For now, all I have done is to loop through all of the actions targets and reduce their “hitPoints” stat by the “amount” indicated in the action.

Player System

Now that we have a new action to try, and a system that can handle it, we need a place to trigger it. The only place available at the moment is as a reaction to the fatigue action occuring in the player system. At the end of the “OnPerformFatigue” notification handler, add the following:

[csharp]
var damageTarget = action.player.hero [0] as IDestructable;
var damageAction = new DamageAction (damageTarget, action.player.fatigue);
container.AddReaction (damageAction);
[/csharp]

Victory System

There may be any number of rules which you may wish to use to lead to a game over scenario in your own game. In this game, there is only one – a hero’s hit points dropping to zero. In order to check for this condition I created a new system called the Victory System.

[csharp]
public class VictorySystem : Aspect {
public bool IsGameOver () {
var match = container.GetMatch ();
foreach (Player p in match.players) {
Hero h = p.hero [0] as Hero;
if (h.hitPoints <= 0) { return true; } } return false; } } [/csharp]

The code is very simple – I simply loop through all of the players and determine if any of their hitpoints has dropped to zero or less. If so, I return “true” indicating that the game has ended. If the loop completes without finding any KO’d players, then I can return “false” – game on!

Game Factory

We have created two new systems, but in order for them to be connected with the rest of the project we will have to remember to add them to the game container. We can add them in the GameFactory “Create” method where we add the other systems. I add the systems alphabetically so its easier to find them later if I want to toggle one off, but the order doesn’t actually matter, and a “find” would work just as well.

[csharp]
game.AddAspect ();
game.AddAspect ();
[/csharp]

Game Over State

Since we can determine when a game has ended, let’s add a placeholder state to represent it.

[csharp]
public class GameOverState : BaseState {
public override void Enter () {
base.Enter ();
Debug.Log (“Game Over”);
}
}
[/csharp]

All I have done is print a message to the console when this state actually enters. This will serve well enough for now since I haven’t created any special views, animations, etc that might normally appear at this time. At least if you notice the message, you will understand why clicking the buttons to change turns has no effect.

Global Game State

You might have noticed that the Victory System was not implemented as an observer. If I activated the “Game Over” mode immediately following a KO’d player (such as by observing the damage action itself), there may still be many actions and reactions occuring that could compete with whatever other animations I might want to trigger as a result of the Game Over state itself.

I decided to let the Global State handle the transition to the Game Over state. It currently would handle transitioning back to the “PlayerIdleState” after all actions and reactions are complete, but now I check for a game over and can potentially change to the “GameOverState” instead:

[csharp]
void OnCompleteAllActions (object sender, object args) {
if (container.GetAspect ().IsGameOver ()) {
container.ChangeState ();
} else {
container.ChangeState ();
}
}
[/csharp]

Game View System

There are a few more steps we will need to take before we can try out the new systems. First, we haven’t actually created a hero card yet. Inside of the “Temp_SetupSinglePlayer” method, and inside the “foreach” loop which creates the deck for each player, add the following code just after the nested “for” loop to create a hero card as well:

[csharp]
var hero = new Hero ();
hero.hitPoints = hero.maxHitPoints = 30;
p.hero.Add (hero);
[/csharp]

During testing, you may want to set the hitPoints to something lower than 30 so that you can trigger a game over by fatigue a little quicker, but that is up to you.

Next, move the invoke of the “Temp_SetupSinglePlayer” from the “Start” method to the “Awake” method. This is because I have decided to do other setup work in other Start methods that will need the setup to have already run. I could have chained them by notifications or by grabbing a reference and manually triggering the Setup, but this felt a little easier.

Board View

One step down the hierarchy from the “Game View System” is the “Board View” component. We will use the “Start” method of this script as an opportunity to assign the game match players to their respective views:

[csharp]
void Start () {
var match = GetComponentInParent ().container.GetMatch ();
for (int i = 0; i < match.players.Count; ++i) { playerViews [i].SetPlayer (match.players[i]); } } [/csharp]

Player View

Next, we will add a property to the “Player View” that caches the reference to the match player that is driving its data. We will use a “SetPlayer” method to assign the reference so that we can make sure to also propogate data to other views in the hierarchy as needed.

[csharp]
public Player player { get; private set; }

public void SetPlayer (Player player) {
this.player = player;
var heroCard = player.hero [0] as Hero;
hero.SetHero (heroCard);
}
[/csharp]

Hero View

Finally, we have worked our way down to the view that represents a player’s hero. This view shows the stats of the hero, including its “hitPoints”, and so it will be important to update this view so we can know whether or not our damage action is working.

[csharp]
public Hero hero { get; private set; }

public void SetHero (Hero hero) {
this.hero = hero;
Refresh ();
}

void OnEnable () {
this.AddObserver (OnPerformDamageAction, Global.PerformNotification ());
}

void OnDisable () {
this.RemoveObserver (OnPerformDamageAction, Global.PerformNotification ());
}

void Refresh () {
if (hero == null)
return;
avatar.sprite = inactive; // TODO: Add activation logic
attack.text = hero.attack.ToString ();
health.text = hero.hitPoints.ToString ();
armor.text = hero.armor.ToString ();
}

void OnPerformDamageAction (object sender, object args) {
var action = args as DamageAction;
if (action.targets.Contains (hero)) {
Refresh ();
}
}
[/csharp]

We added a property to cache the reference to the Hero card model to display in the view. We also use a “SetHero” method so that any time the hero is set, that we can also update the view to match its new data.

I use the “OnEnable” and “OnDisable” methods as an opportunity to register and unregister for notifications. Currently we are interested in the perform phase of the “DamageAction”. In the handler for this action we check to see if the cached hero was included in the list of targets or not. If so, we need to refresh the view because the stats may have changed.

The “Refresh” method itself updates all of the stuff in the view that is used to represent the card, such as its various stats and whether or not the sprite should be highlighted showing that it is able to be used offensively.

Demo

Before you run the scene, feel free to modify values such as the “maxHand” and “maxDeck” of the Player class, or the number of hitPoints assigned to the hero card. These changes can enable you to more quickly see the effects of damage from fatigue which leads to a game over from KO.

Run the scene and continually tap the “End Turn” button. When a player’s deck has been depleted and you see the Fatigue message appear, you should also notice that the hero card’s hit points have dropped by the same amount. If you continue until one of their hit points has dropped to zero or less, then a “Game Over” message should appear in the console, and no further actions will be accepted.

Summary

In this lesson we have added another action and a couple of new systems. We can now apply damage to anything which implements the “IDestructable” interface – which currently includes things like Hero and Minion cards. We also have a game that can end – whenever a hero is knocked out, the game will ultimately transition to a Game Over State. If you are feeling inspired, you may wish to try and add some sort of “viewer” to the damage action.

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!

2 thoughts on “Make a CCG – Damage

  1. Hi, another great lesson, thanks for sharing your knowledge. Just a quick correction, the code to create the player’s Hero must be inside the foreach and after the for, not after the foreach as the post says.

Leave a Reply

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