Making simple cards (relative)
Blueprint
A card blueprint contains many different fields.
Common
name: "Example Card",
The name of the card
text: "This card will bake you a cookie!",
The text should describe what the card does. Take inspiration from how Hearthstone by Blizzard (hereby referred to as "Vanilla") describes their cards in the card's description, giving just enough information, without the nuances.
It is also possible to add tags to text:
text: "<b>This is bold!</b> <i>This is italic</i> <bg:#FF0000>This has a red background</bg> <bg:bright:yellow blue>This has a bright yellow background with blue text</> <bg:#00FF00 fg:red>This has a green background with red text</bg fg>",
More information about tags in the tags section.
cost: 1,
This is how much the card costs. This is usually in mana.
type: Type.Minion,
This is the type of the card. As of writing this, the valid types are: here.
classes: [Class.Neutral],
This is the classes that the card belongs to. As of writing this, the valid classes are: here.
You can have multiple classes like this:
classes: [Class.Mage, Class.Paladin],
rarity: Rarity.Legendary,
As of writing this, the valid rarities are: here.
collectible: false,
If a card is not collectible (uncollectible), you cannot add it to the deck, and it won't appear in card pools (handled by the game automatically). An uncollectible card can still be explicitly created by other cards.
id: 30,
The id of the blueprint. Blueprints should get an id assigned automatically by the card creator library. If it doesn't, the game will warn about it when starting. This is unique per blueprint (Very important)
All of the above fields are REQUIRED for every blueprint. If you forget one, the typescript compiler will be mad.
Type Specific
There also exists type-specific fields. Whether these fields are required or not depends on the type of the blueprint. If you forget one, or add one that shouldn't be there, the Blueprint Validator that runs at start will be mad, and will tell you what you need to change to get it working.
// Required for Minions AND Weapons
attack: 1,
health: 2,
This is the Attack / Health of the minion.
// Required for Minions
tribes: [MinionTribe.Beast],
This is the tribes / races of the minion. As of writing this, the valid tribes are here.
// Required for Spells
spellSchools: [SpellSchool.Fire],
This is the spell schools of the spell. As of writing this, the valid spell schools are here.
// Required for Location Cards
durability: 2,
This is how many times a location card can trigger before breaking.
// Required for Location Cards
cooldown: 2,
This is how long you have to wait before using the location card after using it. Afaik, this is always 2 in vanilla.
// Required for Hero Cards
heropowerId: game.cardIds.fireblast114,
This is the id of the heropower blueprint, which should be of type Heropower.
// Required for Enchantment Cards
enchantmentPriority: EnchantmentPriority.Normal,
This is how the game should prioritize the enchantment. See a more complete example here.
And that is everything you can put in a blueprint. Of course, there are more niche things you would want to do, like colossal cards. If you want to do more specific things, you need abilities.
But first, let's look at a full example card
// Created by Hand
import { type Blueprint } from '@Game/types.js';
export const blueprint: Blueprint = {
name: "Minion Example",
text: "Just an example card (Does nothing)",
cost: 1,
type: Type.Minion,
classes: [Class.Neutral],
rarity: Rarity.Free,
collectible: false,
id: 29,
attack: 1,
health: 2,
tribes: [MinionTribe.None],
};
Create
Abilities will be explained in more detail later.
The second argument will be explained later so we won't use it here.
// This ability is triggered when the card is created (e.g. At the start of the game, when the players' decks are populated.)
async create(self, _) {
// The first argument is the card itself (not the blueprint).
// The second argument will be explained later so we won't use it here.
// Let's add a keyword
self.addKeyword(Keyword.Taunt);
self.addKeyword(Keyword.DivineShield);
// If we want colossal
// The 0 will be replaced by this minion.
// The other elements are the ids of the other minions.
// This will first summon the card with id 30, then this card, then the card with id 31.
// The board will then look like this:
// ...
// leftArm_30 (Example: Left Arm)
// null_0 [This] (Example: Colossal Minion)
// rightArm_31 (/Example: Right Arm)
self.addKeyword(Keyword.Colossal, [
game.cardIds.leftArm_30,
game.cardIds.null_0,
game.cardIds.rightArm_31,
]);
// The `game.cardIds` look like this:
const cardIds = {
null_0: 0,
// ...
leftArm_30: 30,
rightArm_31: 31,
// ...
};
self.runes = "FFB";
}
Abilites
Here is an example of a battlecry ability.
// ...
text: "<b>Battlecry:</b> Gain +1/+1.",
// ...
async battlecry(self, _) {
await self.addStats(1, 1);
},
That was a pretty simple ability example. Here is a more complicated one:
// ...
text: "Deal 2 damage to a Minion",
// ...
type: Type.Spell,
// ...
async cast(self, _) {
// The `game` variable is a super-global variable (you can access it from any file without getting it from anywhere)
// `prompt.targetCard` prompts the player to select a card target.
// The prompt is 'Deal 2 damage to a Minion' and the player can select any side (alignment).
const target = await game.prompt.targetCard("Deal 2 damage to a Minion", self);
// It's important to make sure that the player actually chose a target instead of choosing to go back.
if (!target) {
// By returning `Card.REFUND`, the game will give the card back to the player and refund the cost. It would be like they never played the card at all.
return Card.REFUND;
}
// Use the `game.attack` method to deal damage. This handles all edge-cases for you, so use this unless you have a good reason not to.
await game.attack(2, target);
}
There is also the passive ability, which is out of the scope for this page. (Learn more here)
Here is a finished example card (gotten from 'cards/Examples/3/2-discover.ts'):
// Created by Hand
import assert from "node:assert";
import { Card } from "@Game/card.ts";
import {
Ability,
type Blueprint,
Class,
Rarity,
SpellSchool,
Type,
} from "@Game/types.ts";
export const blueprint: Blueprint = {
name: "Discover Example",
text: "Discover a spell.",
cost: 1,
type: Type.Spell,
classes: [Class.Neutral],
rarity: Rarity.Free,
collectible: false,
tags: [],
id: 51,
spellSchools: [SpellSchool.None],
// The second argument is the card's owner. It allows you to mess with the card's player.
async cast(self, owner) {
// Discover a spell.
/*
* The discover function needs a list of cards to choose from.
* This list will act like a pool of cards.
*/
// This gets every card from the game, excluding uncollectible cards.
let pool = await Card.all();
// Filter the pool to only include spells.
pool = pool.filter((c) => c.type === Type.Spell);
// discover(prompt, pool, ifItShouldFilterAwayCardsThatAreNotThePlayersClass = true, amountOfCardsToChooseFrom = 3)
const spell = await game.prompt.discover(
"Discover a spell.",
pool,
);
// If no card was chosen, refund.
if (!spell) {
return Card.REFUND;
}
// Now we need to actually add the card to the player's hand.
await owner.addToHand(spell);
return true;
},
async test(self, owner) {
owner.inputQueue = "1";
owner.hand = [];
for (let i = 0; i < 50; i++) {
await self.trigger(Ability.Cast);
const card = await owner.popFromHand();
assert(card);
assert.equal(card.type, "Spell");
assert(
Boolean(card) &&
game.functions.card.validateClasses(card.classes, owner.heroClass),
);
}
},
};
Tags
Tags can be used for abilities / keywords in descriptions:
text: "<b>Battlecry:</b> Gain +1/+1 and <b>Rush</b>. <i>(Gets discarded in 2 turns)</i>",
Tags can also be used in the console.log function which is a wrapper for console.log:
console.log("<red>Error: <yellow>%s</yellow> is not a valid answer</red>", answer);
// Outputs: (In red)Error: (In yellow){answer}(In red) is not a valid answer
You can also manually use it in any string you want
import { parseTags } from "chalk-tags";
let textToParse = "<green>Success!</green>";
let text = parseTags(textToParse);
console.log(text);