Implement Cards - HearthSim/SabberStone GitHub Wiki

Syntax for Scripting Cards

Basics

Implementing card means we are scripting its Power. Let's start with the definition of the Power class.

public class Power
{
	public string InfoCardId { get; set; } = null;

	public Aura Aura { get; set; }

	public Enchant Enchant { get; set; }

	public Trigger Trigger { get; set; }

	public ISimpleTask PowerTask { get; set; }

	public ISimpleTask DeathrattleTask { get; set; }

	public ISimpleTask ComboTask { get; set; }
}
  • InfoCardId : This indicates which Enchantment card might be related to this Card. InfoCardId doesn't have any influence at runtime.
  • Aura : This property can be used to implement some passive effects.
  • Enchant : Only Enchantment cards use this property. You can regard it as Buffs.
  • Trigger : If a card has text like "Whenever..." or "After..." its Power definitely has Trigger!
  • PowerTask : ISimpleTask to be processed whenever this card is played. This task is used to implement Battlecries, Spells, and HeroPowers.
  • DeathrattleTask : Self-explanatory.
  • ComboTask : Self-explanatory.

So, what we have to do is properly filling up these properties.

Cards without Power

The easiest kind must be the cards that doesn't really have any Powers. For example,

// --------------------------------------- MINION - NEUTRAL
// [CS2_182] Chillwind Yeti - COST:4 [ATK:4/HP:5] 
// - Fac: neutral, Set: core, Rarity: free
// --------------------------------------------------------
cards.Add("CS2_182", null);

// ---------------------------------------- MINION - SHAMAN
// [NEW1_010] Al'Akir the Windlord - COST:8 [ATK:3/HP:5] 
// - Race: elemental, Set: expert1, Rarity: legendary
// --------------------------------------------------------
// Text: <b>Charge, Divine Shield, Taunt, Windfury</b>
// --------------------------------------------------------
// GameTag:
// - ELITE = 1
// - WINDFURY = 1
// - TAUNT = 1
// - DIVINE_SHIELD = 1
// - CHARGE = 1
// --------------------------------------------------------
cards.Add("NEW1_010", null);

// --------------------------------------- MINION - PALADIN
// [CFM_815] Wickerflame Burnbristle - COST:3 [ATK:2/HP:2] 
// - Set: gangs, Rarity: legendary
// --------------------------------------------------------
// Text: <b>Divine Shield, Taunt, Lifesteal</b>
// --------------------------------------------------------
// GameTag:
// - ELITE = 1
// - TAUNT = 1
// - DIVINE_SHIELD = 1
// - LIFESTEAL = 1
// --------------------------------------------------------
cards.Add("CFM_815", null);

SabberStone supports these bold keywords by itself, so that you don't have to implement none of these. So cards with no texts's power is just null. But you should note that these cards should have null Power, to indicate its implementation.

BattleCry, Spell, and HeroPower

Syntax for Battlecry minions, Spell and HeroPower is also very simple. These effects usually use only the PowerTask property. For example,

// ------------------------------------------- SPELL - MAGE
// [CS2_029] Fireball - COST:4 
// - Fac: neutral, Set: core, Rarity: free
// --------------------------------------------------------
// Text: Deal $6 damage. @spelldmg
// --------------------------------------------------------
// PlayReq:
// - REQ_TARGET_TO_PLAY = 0
// --------------------------------------------------------
cards.Add("CS2_029", new Power {
	PowerTask = new DamageTask(6, EntityType.TARGET, true)
});

SabberStone 2.0.0 change There is no more EnchantmentActivation now.

What we have to do here is create an appropriate task and assign it to the PowerTask. There are three ways to create a task.

  1. Use one of SimpleTasks
    Currently there are lots of SimpleTasks in SabberStoneCore.Tasks.SimpleTasks namespace and you can choose one of them and create a new one with a proper constructor. Fireball is a good example.
  2. Create a ComplexTask with a list of SimpleTasks
    Some SimpleTasks usually represents small procedures such as "Add this entity to somewhere" or "Add some entities to the stack". These tasks are used as part of a sequence of tasks. This concept is implemented as ComplexTask. For example,

// ---------------------------------------- SPELL - WARRIOR
// [EX1_392] Battle Rage - COST:2 
// - Fac: neutral, Set: expert1, Rarity: common
// --------------------------------------------------------
// Text: Draw a card for each damaged friendly character.
// --------------------------------------------------------
// PlayReq:
// - REQ_MINION_TARGET = 0
// --------------------------------------------------------
cards.Add("EX1_392", new Power {
	PowerTask = ComplexTask.Create(
		new IncludeTask(EntityType.FRIENDS),
		new FilterStackTask(SelfCondition.IsDamaged),
		new CountTask(EntityType.STACK),
		new DrawNumberTask())
});

We provide a builder method ComplexTask.Create(params ISimpleTask tasks) to compose a sequence of tasks from ISimpleTask. Let's take a look into the example. First, the IncludeTask brings the entities of EntityType to the stack of the ComplexTask. ComplexTask uses memory stack to contain shared resources to be used within the sequence of tasks. So after the IncludeTask is completed, the stack of the sequence has entities of all friendly minions. Then, the FilterStackTask does its job. Next, CountTask does its job. Finally, DrawNumberTask reads the contained Number from the stack (which is brought by CountTask) and gives the player the appropriate amount of card draws.

  1. Scripting customised functions or creating new tasks.
    Once you figure out the inner structure of SabberStone and decide to implement some really complicated cards, you might need some new tasks or customised function. You can easily implement custom function with FuncPlayablesTask and FuncNumberTask, which allows you to pass delegate of anonymous functions to manipulate things in the stack. Usually implementations with many lines of custom function should be located in SpecificTask.

Triggers

SabberStone 2.0.0 changes The syntax for scripting Trigger is completely changed now.
To correctly implement Triggers first we should infer the precise TriggerType of the card. TriggerType determines when the effect is triggered. Fortunately, most of cards in Hearthstone have some degree of patterns in their text. For instance,

// --------------------------------------- MINION - NEUTRAL
// [EX1_095] Gadgetzan Auctioneer - COST:6 [ATK:4/HP:4] 
// - Fac: neutral, Set: expert1, Rarity: rare
// --------------------------------------------------------
// Text: Whenever you cast a spell, draw a card.
// --------------------------------------------------------
cards.Add("EX1_095", new Power {
	Trigger = new Trigger(TriggerType.CAST_SPELL)
	{
		TriggerSource = TriggerSource.FRIENDLY,
		SingleTask = new DrawTask()
	}
});

All cards having text of "Whenever ... cast a spell ..." have Trigger of TriggerType.CAST_SPELL. Triggers can have several properties.

  • TriggerActivation : This determines Where this trigger is activated. For example, Patches the Pirate's Trigger is activated in the deck. Most of triggers in Hearthstone are activated in Zone.PLAY, the default value of this property.
  • TriggerSource: This determines What kind of entities can trigger the effect.
  • Condition: Additional constraint for the trigger sources.
  • SingleTask: The task to be processed when this is triggered.
  • FastExecution: true if The effect of this trigger doesn't queue up.

Auras

SabberStone 2.0.0 changes The syntax for implementing Aura is completely changed.
Aura property can be utilised to implement auras like effect of Sorcerer's Apprentice or Stormwind Champion and Adaptive Effects.

Let's grasp the syntax with proper examples.

// --------------------------------------- MINION - NEUTRAL
// [CS2_222] Stormwind Champion - COST:7 [ATK:6/HP:6] 
// - Fac: alliance, Set: core, Rarity: free
// --------------------------------------------------------
// Text: Your other minions have +1/+1.
// --------------------------------------------------------
// GameTag:
// - AURA = 1
// --------------------------------------------------------
cards.Add("CS2_222", new Power {
	Aura = new Aura(AuraType.BOARD_EXCEPT_SOURCE, "CS2_222o")
});

// ------------------------------------------ MINION - MAGE
// [EX1_608] Sorcerer's Apprentice - COST:2 [ATK:3/HP:2] 
// - Fac: neutral, Set: expert1, Rarity: common
// --------------------------------------------------------
// Text: Your spells cost (1) less.
// --------------------------------------------------------
// GameTag:
// - AURA = 1
// --------------------------------------------------------
cards.Add("EX1_608", new Power {
	Aura = new Aura(AuraType.HAND, Effects.ReduceCost(1))
	{
		Condition = SelfCondition.IsSpell
	}
});

// ---------------------------------------- WEAPON - SHAMAN
// [KAR_063] Spirit Claws - COST:2 [ATK:1/HP:0] 
// - Set: kara, Rarity: common
// --------------------------------------------------------
// Text: [x]Has +2 Attack while you
//       have <b>Spell Damage</b>.
// --------------------------------------------------------
// GameTag:
// - DURABILITY = 3
// --------------------------------------------------------
// RefTag:
// - SPELLPOWER = 1
// --------------------------------------------------------
cards.Add("KAR_063", new Power {
	Aura = new AdaptiveEffect(GameTag.ATK, EffectOperator.ADD, p => p.Controller.CurrentSpellPower > 0 ? 2 : 0)
});
  1. Aura needs parameter AuraType, which indicates what entities will be applied by the aura.
  2. Aura needs Enchantment card or Effect to apply.
  3. Aura can have Condition property, which limits the targets of the aura.

You may confuse with 1 and 2. You have to search the cardset file to check if there is a matching enchantment card. (Or see Power.log)

AdaptiveEffect and AdaptiveCostEffect is implementation for effects that changes the effect owner's tag values as board environment changes. To implement these effects, assign them to the Aura property and provide a proper function.

Enchantments

SabberStone 2.0.0 changes When you implement cards creating Enchantments, you have to implement the referred Enchantment Card too.
Most of the enchantments just have one property: Enchant. Enchant contains instructions that how the Tag of the target should be manipulated. This instruction is implemented as a simple struct Effect. Fortunately, most of enchantment cards have really simple text. So most Enchant can be parsed from the text of the card. The static method Enchant Enchants.Enchants.GetAutoEnchantFromText(string enchantmentCardId) does this. This automatic parser supports:

  • +n/+m (increase the target's ATK and HEALTH n and m respectively)
  • n/m (set the target's ATK and HEALTH to n and m respectively)
  • +n Attack
  • +n HEALTH
  • <b> Keyword </b>
  • .... this turn ... (One turn Effect)

Otherwise you have to provide the proper enchant like this:

// ---------------------------------- ENCHANTMENT - NEUTRAL
// [NEW1_024o] Greenskin's Command (*) - COST:0 
// - Set: expert1, 
// --------------------------------------------------------
// Text: +1/+1.
// --------------------------------------------------------
cards.Add("NEW1_024o", new Power {
	Enchant = new Enchant(
		new Effect(GameTag.ATK, EffectOperator.ADD, 1),
		new Effect(GameTag.DURABILITY, EffectOperator.ADD, 1))
});

Enchantments can also have Trigger and Aura. A good example would be Preparation.

// ------------------------------------------ SPELL - ROGUE
// [EX1_145] Preparation - COST:0 
// - Fac: neutral, Set: expert1, Rarity: epic
// --------------------------------------------------------
// Text: The next spell you cast this turn costs (3) less.
// --------------------------------------------------------
cards.Add("EX1_145", new Power {
	PowerTask = new AddEnchantmentTask("EX1_145o", EntityType.CONTROLLER)
});
// ------------------------------------ ENCHANTMENT - ROGUE
// [EX1_145o] Preparation (*) - COST:0 
// - Set: expert1, 
// --------------------------------------------------------
// Text: The next spell you cast this turn costs (3) less.
// --------------------------------------------------------
// GameTag:
// - TAG_ONE_TURN_EFFECT = 1
// --------------------------------------------------------
cards.Add("EX1_145o", new Power {
	Aura = new Aura(AuraType.HAND, Effects.ReduceCost(3))
	{
		Condition = SelfCondition.IsSpell,
		RemoveTrigger = (TriggerType.CAST_SPELL, null)
	}
});

Preparation creates Controller Enchantment [EX1_145o] and the enchantment have aura that reduces cost of all spell in hand by 3.

Choose the approprate set of cards, where the card you want to implement is listed. All current sets are pregenerated under CardSets. Let's start with an example, "Greater Arcane Missiles".

Pregenerated Code

// ------------------------------------------- SPELL - MAGE
// [CFM_623] Greater Arcane Missiles - COST:7 
// - Set: gangs, Rarity: epic
// --------------------------------------------------------
// Text: Shoot three missiles at random enemies that deal $3 damage each. *spelldmg
// --------------------------------------------------------
cards.Add("CFM_623", new List<Enchantment> {
	// TODO [CFM_623] Greater Arcane Missiles && Test: Greater Arcane Missiles_CFM_623
	new Enchantment
	{
		Activation = EnchantmentActivation.SPELL,
		SingleTask = null,
	},
});

Implementing new Task

For this spell we will only add a single task, which will enqueue 3 random missiles.

SingleTask = 
   new EnqueueTask(
     3, // times we enqueue the task
     ComplexTask.DamageRandomTargets(3, EntityType.ENEMIES, 1), // 3dmg, targetslist, how many of them
     true) // if spell damage will increase enqueueing .... like arcane missiles

Implementing new Test

And for the last part we create a test for it, which also is pregenerated in this location CardSets.

[TestMethod]
public void GreaterArcaneMissiles_CFM_623()
{
	var game = new Game(new GameConfig
	{
		StartPlayer = 1,
		Player1HeroClass = CardClass.MAGE,
		Player2HeroClass = CardClass.MAGE,
		FillDecks = true
	});
	game.StartGame();
	game.Player1.BaseMana = 10;
	game.Player2.BaseMana = 10;
  var minion1 = Generic.DrawCard(game.CurrentPlayer, Cards.FromName("Ironfur Grizzly"));
  game.Process(PlayCardTask.Any(game.CurrentPlayer, (ICharacter)minion1));
  var minion2 = Generic.DrawCard(game.CurrentPlayer, Cards.FromName("Ironfur Grizzly"));
  game.Process(PlayCardTask.Any(game.CurrentPlayer, (ICharacter)minion2));
  game.Process(EndTurnTask.Any(game.CurrentPlayer));
  var totHealth = game.CurrentOpponent.Hero.Health;
  totHealth += ((ICharacter)minion1).Health;
  totHealth += ((ICharacter)minion2).Health;
  Assert.AreEqual(36, totHealth);
  var spell1 = Generic.DrawCard(game.CurrentPlayer, Cards.FromName("Greater Arcane Missiles"));
  game.Process(PlayCardTask.Spell(game.CurrentPlayer, spell1));
  totHealth = game.CurrentOpponent.Hero.Health;
  totHealth += ((ICharacter)minion1).IsDead ? 0 : ((ICharacter)minion1).Health;
  totHealth += ((ICharacter)minion2).IsDead ? 0 : ((ICharacter)minion2).Health;
  Assert.AreEqual(27, totHealth);
  var minion3 = Generic.DrawCard(game.CurrentPlayer, Cards.FromName("Dalaran Mage"));
  game.Process(PlayCardTask.Minion(game.CurrentPlayer, (ICharacter)minion3));
  game.Process(EndTurnTask.Any(game.CurrentPlayer));
  game.Process(EndTurnTask.Any(game.CurrentPlayer));
  var spell2 = Generic.DrawCard(game.CurrentPlayer, Cards.FromName("Greater Arcane Missiles"));
  game.Process(PlayCardTask.Spell(game.CurrentPlayer, spell2));
  totHealth = game.CurrentOpponent.Hero.Health;
  totHealth += ((ICharacter)minion1).IsDead ? 0 : ((ICharacter)minion1).Health;
  totHealth += ((ICharacter)minion2).IsDead ? 0 : ((ICharacter)minion2).Health;
  // Spellpower check
  Assert.AreEqual(1, game.CurrentPlayer.Hero.SpellPower);
  Assert.AreEqual(15, totHealth);
}
⚠️ **GitHub.com Fallback** ⚠️