Challenge
This section will rely heavily on Challenge Events. There will be a lot of calls to retrieve and save the cards, stats, playingField, and currentHand. Player Ids and Challenge Id will constantly be values to reference the rest of the game - these two Ids will allow you to do anything with the Challenge.
Create 4 Events:
- action_pullCard
- action_pushCard
- action_playCard
- action_endTurn
We're also going to be editing the ChallengeTurnTakenMessage.
Important! Make sure the Cloud Code for these Events is edited in the Challenge Events section of the Cloud Code>Scripts panel NOT the Events section.
challengeInstanceId? When calling Challenge Events you'll automatically get a challengeInstanceId string input value.
action_pullCard
This Challenge Event relies on the pullCard module. It adds a random card from the deck into the player's current hand. The player is unable to pull another card from the deck during this round after this Event is executed.
//Load challenge
var chal = Spark.getChallenge(Spark.getData().challengeInstanceId);
//Retrieve player stats
var playerStats = chal.getScriptData("playerStats");
//Retrieve player Id
var pId = Spark.getPlayer().getPlayerId();
if(playerStats[pId].hasPulled === false){
//Retrieve current hands
var currentHand = chal.getScriptData("currentHand");
//Run the sequence to pull a new card
require("pullCardModule");
//Player can't pull another card this round
playerStats[pId].hasPulled = true;
//Save current hand and player stats
chal.setScriptData("currentHand", currentHand);
chal.setScriptData("playerStats", playerStats);
}
else{
Spark.setScriptError("Error", "Already pulled card this round");
}
action_pushCard
This Event will have a single input value:
- slotNum which is the name of the card (example c0,c1).
This Event will move cards from the currentHand to the playField if the player has enough mana to play the card. Using the unique Id given to the card (for example c1 or c0), we move the card from one object to another. If cards have certain effects, such as charge or direct attack, that will trigger a sequence of code.
//Load the challenge
var chal = Spark.getChallenge(Spark.getData().challengeInstanceId);
//Retrieve player Id
var pId = Spark.getPlayer().getPlayerId();
//Retrieve the slot
var cardName = Spark.getData().slotNum;
//Retrieve the current hand
var currentHand = chal.getScriptData("currentHand");
//Retrieve the current player's hand
var playerHand = currentHand[pId];
//Get the cards name or type
var cardObj = playerHand[cardName];
//Retrieve player stats
var playerStats = chal.getScriptData("playerStats");
//Retrieve Mana
var playerMana = playerStats[pId].currentMana;
if(cardObj == null){
Spark.setScriptError("result", "Card doesn't exist");
Spark.exit();
}
//Is there enough mana to spawn the card?
if( playerMana >= cardObj.spawnCost){
var playerStats = chal.getScriptData("playerStats");
//New mana
playerStats[pId].currentMana = playerStats[pId].currentMana - cardObj.spawnCost;
//Retrieve the playing field
var playField = chal.getScriptData("playField");
//Set value in the correct position (playField -> Players ID -> The card's name/number) with correct name
playField[pId][cardName] = {"type" : cardObj.type, "atk" : cardObj.atk, "hp" : cardObj.hp, "maxHP": cardObj.hp, "canAtk": false } ;
//Remove that card from current hand
delete currentHand[pId][cardName];
//Return script message with card pushed
Spark.setScriptData("result", "Card " + cardName + " of type " + cardObj.type + " is on the playing field");
//Save jsons
chal.setScriptData("currentHand", currentHand);
chal.setScriptData("playField", playField);
chal.setScriptData("playerStats", playerStats);
}
else{
//if there isn't enough mana, send error report through scriptData
Spark.setScriptData("Error", "Not enough mana");
}
action_playCard
This Event will have 3 input values:
- playState - To distinguish between the current action, we configured "attack" and "heal". This means we don't have to have an Event for every action but instead take a modular approach.
- name - The player's chosen card (example c0, c1, and so on).
- targetName - Could either be the heal target or attack target depending on playState.
Using this Event, a player can heal a friendly target or attack an enemy opponent or their cards depending on playState and the targetName.
//Load challenge
var chal = Spark.getChallenge(Spark.getData().challengeInstanceId);
//Load data
var playState = Spark.getData().playState;
var cardName = Spark.getData().name;
var targetName = Spark.getData().targetName;
//Retrieve player stats
var playerStats = chal.getScriptData("playerStats");
//Retrieve player Id
var pId = Spark.getPlayer().getPlayerId();
//Determine enemy Id
if(Spark.getPlayer().getPlayerId() === chal.getChallengerId()){
//If equal to challenger, then Id is challenged[0] Id
var opponentId = chal.getChallengedPlayerIds()[0];
}
else{
//If not equal to challenger then other player Id is challenger Id
var opponentId = chal.getChallengerId();
}
//Load playField
var playField = chal.getScriptData("playField");
//Load player play field
var playerField = playField[pId];
//Load the attacking card
var attackingCard = playerField[cardName];
//If the attacking card isn't null
if(attackingCard == null){
Spark.setScriptError("Error", "Attacking card does not exist");
Spark.exit();
}
//Depending on playState of card
if(playState === "attack"){
if(attackingCard.canAtk === true){
//Create object for output
var result = {};
//If our target is the opponent, damage opponent
if(targetName === "opponent"){
//Retrieve the playerHealth object
var health = playerStats[opponentId].currentHealth;
//Lower the opponent health points based on card's attack
playerStats[opponentId].currentHealth = health - attackingCard.atk;
//Save the health stats
// chal.setScriptData("playerStats", playerStats);
//Output result
result = cardName + " attacked opponent";
}
else{
//If our target is not an opponent, then we're aiming for another card
//Retrieve opponent field
var opponentField = playField[opponentId];
//Retrieve target card
var targetCard = opponentField[targetName];
if(targetCard == null){
Spark.setScriptError("Error", "Attacking card does not exist");
Spark.exit();
}
//If our attacking card has more attack than the target card hit points
if(attackingCard.atk >= targetCard.hp){
delete playField[opponentId][targetName];
result.case1 = cardName + " destroyed " + targetName;
}
else {
//else if our attacking card cannot kill target card with one hit, lower hit points
playField[opponentId][targetName].hp = targetCard.hp - attackingCard.atk;
result.case1 = cardName + " hurt " + targetName;
}
//If our attacking card has less hit points than target card, destroy our card
if(targetCard.atk >= attackingCard.hp){
delete playField[pId][cardName];
result.case2 = cardName + " was destroyed by " + targetName;
}
else{
//Else if our attacking card has more hit points than the target card's attack, then lower hit points
playField[pId][cardName].hp = attackingCard.hp - targetCard.atk;
result.case2 = cardName + " was hurt by " + targetName;
}
}
}
if( playField[pId][cardName] != null){
//Disallow this card to be able to attack again this round if it still exists
playField[pId][cardName].canAtk = false;
}
//Save playing field
chal.setScriptData("playField", playField);
chal.setScriptData("playerStats", playerStats);
Spark.setScriptData("result", result);
}
else{
Spark.setScriptError("Error", "Card cannot attack this round")
Spark.exit();
}
//If instead our playsState was to heal instead of attack, we won't need to load the opponent's playField and just work
//With this player's playField exclusively
if(playState === "heal"){
if(playField[pId][targetCard] != null){
//If targetCard has less hit points than max allowed
if(playField[pId][targetCard].hp <= playField[pId][targetCard].maxHP){
//Heal card by adding hit points
playField[pId][targetCard].hp = playField[pId][targetCard].hp + 1;
//Save playField
chal.setScriptData("playField", playField);
}
}
else{
//Stop script if card does not exist
Spark.setScriptData("Error", "Can't find target card");
Spark.exit();
}
}
action_endTurn
A player needs to end their turn to allow:
- The other player to have a turn.
- The player's cards to be able to attack.
- Mana to recharge.
- Extra mana gems to be earned.
This also is a Challenge Event because it needs to make a reference to the Event, so make sure you edit the Challenge Event Cloud Code.
//Load challenge
var chal = Spark.getChallenge(Spark.getData().challengeInstanceId);
//Retrieve player Id
var pId = Spark.getPlayer().getPlayerId();
//Retrieve player's details and play field
var playerStats = chal.getScriptData("playerStats");
var playField = chal.getScriptData("playField");
//If we have less than 10 mana gems
if(playerStats[pId].overallMana < 10){
//Add a mana gem
playerStats[pId].overallMana = playerStats[pId].overallMana + 1;
}
//Current mana will be filled again
playerStats[pId].currentMana = playerStats[pId].overallMana;
//Get all the cards on the player's play field
var cards = Object.keys(playField[pId]);
//Use the allowAtk function on every card
cards.forEach(allowAtk)
//Reset card pulled boolean
playerStats[pId].hasPulled = false;
//Save JSONs
chal.setScriptData("playField", playField);
chal.setScriptData("playerStats", playerStats);
var turnCountVar = chal.getScriptData("turnCount");
//Finish player turn
chal.takeTurn(pId);
//chal.consumeTurn(pId);
function allowAtk(obj){
//Set the canAtk value to true, to allow the card to attack next turn
playField[pId][obj].canAtk = true;
}
ChallengeTurnTakenMessage
This is a brilliant message to place a check for player's health and act upon it because it's called after every Challenge Event is executed. We'll have three checks:
- Whether the challenger's health is 0 or lower and the challenged player's health is over 0.
- Whether the challenged player's health is 0 or lower and the challenger's health is over 0.
- If both player's health statuses are equal to or lower than 0.
Each condition will trigger a different response, either the challenged winning, the challenger winning, or a draw.
Global Messages not User Messages! Make sure you attach this Cloud Code logic to the ChallengeTurnTakenMessage under the Global Messages section NOT the User Messages section of the Cloud Code>Scripts panel.
//Load challenge
var chal = Spark.getChallenge(Spark.getData().challenge.challengeId);
//load playerStats
var playerStats = chal.getScriptData("playerStats");
//If challenger reaches 0 or lower health while challenged has more than 0 health, challenged wins
if(playerStats[chal.getChallengerId()].currentHealth <= 0 && playerStats[chal.getChallengedPlayerIds()[0]].currentHealth > 0){
chal.winChallenge(Spark.loadPlayer(chal.getChallengedPlayerIds()[0]));
}
//If challenged reaches 0 or lower health while challenger has more than 0 health, challenger wins
if(playerStats[chal.getChallengedPlayerIds()[0]].currentHealth <= 0 && playerStats[chal.getChallengerId()].currentHealth > 0){
chal.winChallenge(Spark.loadPlayer(chal.getChallengerId()));
}
//If both players reach zero or lower health, a draw is in order
if(playerStats[chal.getChallengedPlayerIds()[0]].currentHealth <= 0 && playerStats[chal.getChallengerId()].currentHealth <= 0){
chal.drawChallenge();
}
Conclusion
In combination, these Events and structure can start and end a game the same way Hearthstone works. You can add more cards, more effects, and extra functionality easily because this tutorial was built with modularity and expansion in mind. You can test this using two tabs that have our Test Harness open, then by registering two players and pitting them against each other (both controlled by you).
This concludes the Hearthstone example. You can customize, add, remove, and reinvent this system. This is only one way of achieving this kind of game but by playing around with our components and further understanding the way they work you can achieve brilliant results.