-
Notifications
You must be signed in to change notification settings - Fork 23
Adding Cards
Before working on cards, I would recommend making sure you understand the action queue.
The process of making cards has two steps.
Step 1: Make the card.
Step 2: Register the card.
AutoAdd is a feature of BaseMod that allows you to avoid manually registering every card you make, which is why I'm going over it first.
First, in your main mod file (the one originally called BasicMod.java
), add EditCardsSubscriber
to the subscribers at the top, and then implement the receiveEditCards
method. You'll know you did it right if you don't have any errors.
public class MyMod implements
EditCardsSubscriber, //up at the top
EditStringsSubscriber,
@Override
public void receiveEditCards() { //somewhere in the class
}
Next, put this block of code into receiveEditCards
.
new AutoAdd(modID) //Loads files from this mod
.packageFilter(BaseCard.class) //In the same package as this class
.setDefaultSeen(true) //And marks them as seen in the compendium
.cards(); //Adds the cards
You may need to use Alt+Enter
to import some classes. And you're done! Any cards you make should now be added automatically, as long as they're in the same package as BaseCard
. This includes sub-packages, like cards.attacks.SomeCard
. If you want the details of how AutoAdd works, check the BaseMod wiki page for documentation. This code is made to work with how BasicMod is setup.
You should make your first card from scratch to understand all the parts, but after that feel free to copy and paste it and just change what you need.
The first step is to make a new class in the cards
package. For this, we'll start by making a Strike card, since every character should have one.
You could also organize your cards using packages, such as having a package for basic, common, uncommon, and rare cards, or maybe organizing by type.
Every card class in Slay the Spire extends from AbstractCard
. For example, the player's hand is a list of AbstractCard
s. If something isn't an AbstractCard
, it can't be in the player's hand. And of course, everywhere else in the game that holds cards.
Your cards will extend the class BaseCard
. BaseCard
is a class that extends AbstractCard
with some quality-of-life features to make card creation easier. To be specific, BaseCard extends CustomCard
, which extends AbstractCard
. The CustomCard
class has a lot of functionality that is required for custom cards to be displayed properly.
public class Strike extends BaseCard {
}
You will have errors until the class is mostly complete, so don't worry about them until then.
The first thing to put inside your class will be the card's general information, starting with the ID. The ID is used to differentiate each card from others, and is used for saving the player's deck along with a variety of other checks. When making a mod, we add a prefix based on the mod's ID to the card's ID. This greatly decreases the likelihood of two different mods having content with the same ID.
public class Strike extends BaseCard {
public static final String ID = makeID("Strike"); //makeID adds the mod ID, so the final ID will be something like "modID:MyCard"
An alternative for this is to use the class name, like makeID(Strike.class.getSimpleName())
. This is useful when copying and renaming the class, as the ID will also be changed.
Next, you'll need the card color, type, rarity, target, and cost. These are all contained in a single CardStats
object for the purposes of organization. You don't have to organize things this way; it's just how I prefer to do it. You can also define each value individually, and use them as parameters.
public class Strike extends BaseCard {
public static final String ID = makeID(Strike.class.getSimpleName());
private static final CardStats info = new CardStats(
MyCharacter.Meta.CARD_COLOR, //The card color. If you're making your own character, it'll look something like this. Otherwise, it'll be CardColor.RED or similar for a basegame character color.
CardType.ATTACK, //The type. ATTACK/SKILL/POWER/CURSE/STATUS
CardRarity.BASIC, //Rarity. BASIC is for starting cards, then there's COMMON/UNCOMMON/RARE, and then SPECIAL and CURSE. SPECIAL is for cards you only get from events. Curse is for curses, except for special curses like Curse of the Bell and Necronomicurse.
CardTarget.ENEMY, //The target. Single target is ENEMY, all enemies is ALL_ENEMY. Look at cards similar to what you want to see what to use.
1 //The card's base cost. -1 is X cost, -2 is no cost for unplayable cards like curses, or Reflex.
);
}
The ID
and info
will be used in the card's constructor to set the card up appropriately.
Next are a few constants, which will also be used to set up the card.
//These will be used in the constructor. Technically you can just use the values directly,
//but constants at the top of the file are easy to adjust.
private static final int DAMAGE = 6;
private static final int UPG_DAMAGE = 3;
The constructor of the card is what actually "makes" it. The values you've defined so far won't do anything unless you actually use them in the constructor.
public Strike() {
super(ID, info); //Pass the required information to the BaseCard constructor.
setDamage(DAMAGE, UPG_DAMAGE); //Sets the card's damage and how much it changes when upgraded.
}
setDamage
is a methods provided by BaseCard
. Normally, you would need to set baseDamage = DAMAGE;
, then override the upgrade
method to handle adjusting these values correctly when the card is upgraded. BaseCard
handles upgrade code for you, so all you have to do is set this information up in the constructor. There are similar methods for block and magicNumber (anything other than damage and block), and other card properties.
Ctrl+Click on BaseCard
to see everything it offers, or find the details here at the bottom of this page. For example, to make a card upgrade to innate, you would use setInnate(false, true);
.
Since this card is specifically a Strike, there's one more thing to add. Basic strikes and all strike cards are "tagged", so that the game knows what they are for the purposes of relics like Pandora's Box, or a card like Perfected Strike. These tags are added by calling tags.add
.
public Strike() {
super(ID, info); //Pass the required information to the BaseCard constructor.
setDamage(DAMAGE, UPG_DAMAGE); //Sets the card's damage and how much it changes when upgraded.
tags.add(CardTags.STARTER_STRIKE);
tags.add(CardTags.STRIKE);
}
You can find more details about what these tags mean at the bottom of this page.
By default, cards have three number variables for you to use. These are damage
, block
, and magicNumber
. damage
is used for damage, block
is used for block, and magicNumber
is used for anything else that changes on upgrade, like card draw, or number of orbs channeled, or strength gained. These variables each come paired with "base" variables, which are baseDamage
, baseBlock
, and baseMagicNumber
. While cards are in your hand, the applyPowers
method of AbstractCard is used to calculate their damage
and block
based on baseDamage
and baseBlock
, applying things like Strength, Dexterity, and Frail. When targeting an enemy with a card, the calculateCardDamage
method is used instead, which also applies the targeted enemy's effects, such as Vulnerable.
In card text, these numbers are displayed using !D!
for damage, !B!
for block, and !M!
for magicNumber respectively. You can make additional variables if necessary. See this page for details.
You don't need to use a variable for every number, but it's nice to do so when possible. Using magicNumber
on your cards instead of fixed numbers allows for more interactions with certain other mods that might mess with these values.
Right now, your card just sets some variables. For it to actually do anything, you need to override the use
method. This is what is called when a card is played. In the use
method, cards add actions to the action queue, which will then do whatever the card is supposed to do. As a Strike, this card is just going to deal some damage.
@Override
public void use(AbstractPlayer p, AbstractMonster m) {
addToBot(new DamageAction(m, new DamageInfo(p, damage, DamageInfo.DamageType.NORMAL), AbstractGameAction.AttackEffect.SLASH_VERTICAL));
}
The addToBot
method adds whatever action it is given to the end of the action queue.
DamageAction
is an action that deals damage. The first parameter is the target to deal damage to, which in this case is m
. When use
is called, p
is always the player and m
is the target enemy. For cards that don't target an enemy, m
will be null
. While this is fine in most cases, you have to be careful with certain actions, as this could result in a NullPointerException
if you don't account for this.
The second parameter of DamageAction
is a DamageInfo
. This has the required information about the damage to deal: the source (the player in this case), the amount (the card's damage
variable), and the damage type.
There are three types of damage in the game. NORMAL
, THORNS
, and HP_LOSS
. ALL attacks deal NORMAL
damage. Any blockable damage that isn't from an attack is THORNS
damage (such as from Thorns). Damage that ignores block is HP_LOSS
.
Finally, the last parameter of DamageAction
is an AttackEffect
. This is just the visual/sound effect that will be used for the damage. This supports a number of basic effects that are used for the majority of attacks. For fancier effects, AttackEffect.NONE
is used and visual/sound effects are added separately. See Bludgeon
for an example of this.
public class Strike extends BaseCard {
public static final String ID = makeID(Strike.class.getSimpleName());
private static final CardStats info = new CardStats(
MyCharacter.Meta.CARD_COLOR,
CardType.ATTACK,
CardRarity.BASIC,
CardTarget.ENEMY,
1
);
private static final int DAMAGE = 6;
private static final int UPG_DAMAGE = 3;
public Strike() {
super(ID, info);
setDamage(DAMAGE, UPG_DAMAGE); //Sets the card's damage and how much it changes when upgraded.
tags.add(CardTags.STARTER_STRIKE);
tags.add(CardTags.STRIKE);
}
@Override
public void use(AbstractPlayer p, AbstractMonster m) {
addToBot(new DamageAction(m, new DamageInfo(p, damage, DamageInfo.DamageType.NORMAL), AbstractGameAction.AttackEffect.SLASH_VERTICAL));
}
@Override
public AbstractCard makeCopy() { //Optional
return new Strike();
}
}
With that, you have a finished card... almost. While your card should appear in-game in the compendium and should also work, it's missing its description, and the image is just a default image that comes with the mod.
If you don't have it enabled, you'll have to do that first. This is done in the in-game "Mods" option from the main menu, choosing BaseMod and opening the Config. BaseMod Console
The command to add a card is hand add [id] {cardcount} {upgrades}
In the case of your card, the ID is your mod's ID followed the card's ID, with a colon between. For example, modid:MyCard
.
You probably want your card to actually have a name and description. To fix this, you'll have to edit the localization/eng/CardStrings.json
file. Make a copy of the existing "ExampleCard" entry.
{
"${modID}:ExampleCard": {
"NAME": "Name",
"DESCRIPTION": "Description.",
"UPGRADE_DESCRIPTION": "This will automatically be used if the card is upgraded. Remove it if unneeded.",
"EXTENDED_DESCRIPTION": [
"You can put more text in here.",
"If you need to use it for stuff.",
"Blizzard is a good example for this."
]
},
"${modID}:ExampleCard": {
"NAME": "Name",
"DESCRIPTION": "Description.",
"UPGRADE_DESCRIPTION": "This will automatically be used if the card is upgraded. Remove it if unneeded.",
"EXTENDED_DESCRIPTION": [
"You can put more text in here.",
"If you need to use it for stuff.",
"Blizzard is a good example for this."
]
}
}
It's nice to leave the example in to reference later. Next, edit the copy you've made. You'll need the ID to match the ID of your card, which in this example is Strike
. The full ID includes whatever your mod's ID is, but that bit is handled by ${modID}:
in the json file. Next, update the name and description. NL
(with the spaces) is used for a new line. You can view the base game's localization files for examples of card text. You can just delete the UPGRADE_DESCRIPTION
and EXTENDED_DESCRIPTION
, you don't need it for this card.
{
"${modID}:ExampleCard": {
"NAME": "Name",
"DESCRIPTION": "Description.",
"UPGRADE_DESCRIPTION": "This will automatically be used if the card is upgraded. Remove it if unneeded.",
"EXTENDED_DESCRIPTION": [
"You can put more text in here.",
"If you need to use it for stuff.",
"Blizzard is a good example for this."
]
},
"${modID}:Strike": {
"NAME": "Strike",
"DESCRIPTION": "Deal !D! damage."
}
}
If you package and test again, you should be able to see your new card description if you've set it up correctly.
A full explanation of writing card descriptions can be found at the bottom of this page.
Last step. Find (or make) a nice image, and crop it down/resize it to 500x380 (that's the size of the game's card images). Then, you can use this tool to crop your images correctly. The game renders the images as-is, so if they aren't cropped they'll stick out past the image outline (the rarity banner border).
BaseCard
loads images from the resources folder based on their type and ID. For an attack with the ID Strike
, it'll look for the file modid/images/cards/attack/Strike.png
. If this image doesn't exist, it'll use default.png
from that same folder instead. For a Curse card called "Illness", it would be modid/images/cards/curse/Illness.png
.
The 500x380 images should saved as CardName_p.png
, and then a 50% size (250x190) should be saved as CardName.png
. The larger image is used when viewing a single card in detail.
So, take the image you've made and put it in the correct location. Make sure you capitalize the image name correctly; Java cares.
The rest of this page is additional information that may be useful for making more cards.
You can find some examples here.
Most things can be setup in the constructor. This includes cost, card variables, Ethereal, Exhaust, Innate, and Retain. You can find the methods for doing this near the top of the class. These handle both the base value and a single upgrade.
A list of all methods for constructor use:
setDamage
setBlock
setMagic
setCustomVar (see the "Quick Custom Card Variables" page)
setCostUpgrade (the cost to upgrade to, not how much to change it by)
setExhaust
setEthereal
setInnate
setSelfRetain
While you can override the upgrade
method and use the same method as basegame cards, the upgrade
method of BaseCard
will handle almost everything you could need. Just setting their upgrade values using the set____
methods in the constructor will be enough. The upgrade description will be used if one is provided in the card strings.
If a card can be upgraded multiple times or has some other unique change on upgrade that isn't supported by the set
methods and requires overriding the upgrade
method, make sure to call super.upgrade()
if you don't intend to fully change the upgrade behavior. An example of overriding upgrade
to add a tag to the card. An example of an infinite upgrade attack. Completely overriding and handling the upgrades yourself is also an option.
Copy the class, edit as necessary, add localization, and an image. Usually I go through and do images later.
If you're not sure how to do something, look at a base game card that does what you want. If there's no example in the base game, you might have to write it yourself- but don't do it right away. StSLib has a lot of card-related features. If you're not sure how to do something and can't find an example, try asking in the #modding-technical channel of the Slay the Spire discord.
The general appearance of a card (everything besides the card-specific image) is based on the card's color enum value. If you're using a custom color, you can change this by adjusting the images in the cardback
folder. Changing the name/color variables in the player class will have no effect on the appearance. energy_orb.png
is the orb on the top left of the card where cost is displayed. energy_orb_p.png
is the same, but when viewing a single card in the larger view. small_orb.png
is used in card text to represent energy. The various bg_
images are the backgrounds for the various card types, with the _p
versions for portrait (single card) view. Note that all cards other than attacks and powers use the skill background (curses, statuses).
- Attack: Use this only if you card has an effect that's affected by damage modifiers when the card is played. If it works differently, you'll end up with weird interactions with relics like Pen Nib, or powers like Vigor.
- Power: Generally for cards that apply a permanent buff. Removed from combat on play.
- Skill: Anything else.
- BASIC: For basic Strikes, Defends, any cards that appear in your starter deck. Functionally, cards with this rarity will not appear in card rewards.
- COMMON, UNCOMMON, RARE: As named, these are for cards that you want in card rewards.
- SPECIAL: For cards that are generated, such as Shivs. Normally, these will also be colorless.
For basic Strike and Defend cards to work properly with things like Pandora's Box, they have to be tagged. In the constructor (the chunk of code with the same name as the class), use tags.add
to add the tag. For example, the constructor of a Strike might look like:
public Strike() {
super(ID, info);
setDamage(DAMAGE, UPG_DAMAGE);
tags.add(CardTags.STARTER_STRIKE); //This tag marks it as a basic Strike
tags.add(CardTags.STRIKE); //This tag marks it as a Strike card for the purposes of Perfected Strike and any similar modded effects
}
Other tags to be aware of are:
-
CardTags.STARTER_DEFEND
: for your basic defend -
BaseModCardTags.FORM
: for a Form card, used for the My True Form custom run modifier -
CardTags.HEALING
: for any card that gives permanent benefits to prevent it from being obtained through random card generation, to discourage stalling
If a card gives block and deals damage, block always comes first. This makes the card more usable against enemies with Thorns.
If a card has an effect when played that scales with Strength and any other damage modifiers, it's an Attack. Try not to make cards that scale but aren't played/aren't attacks. These will have awkward interactions with things like Pen Nib.
Try to make sure the order in which your cards effects are applied matches the order of the description.
For wording choices, use base game examples where possible.
Small errors and strange wording in the text of your cards may not ruin the gameplay, but they have a notable effect on how "polished" your mod seems.
The first letters of keywords are capitalized. The word ALL
is fully capitalized in most cases, such as referring to ALL enemies, or ALL cards in your deck (Apotheosis).
Important: Your custom keywords must be prefixed. Use ${modID}:YourKeyword
. Just like other modded content, keywords are prefixed with your mod id to avoid having them show up on cards from other mods that use the same word. This is also the reason the tutorial says to avoid any spaces in your mod id, as that would break with how descriptions are processed by Slay the Spire.
Block is a keyword, damage is not, don't capitalize it.
!D! for damage, !B! for block, and !M! for the magicNumber variable. If you need more variables, see Quick Custom Card Variables.
NL is used to go to the next line. It must have a space on either side. Most phrases are separated by new lines, but feel free to omit them for the sake of saving space on the card. But try to at least give keywords like Exhaust and Ethereal their own lines.
For energy icons in text, use [E]
.
Text can be colored using a *
before a word *Like *This
, which will cause them to be highlighted like keywords. You can also use [#rrggbb]red/green/blue[]
hex color codes to color text whatever color you want. Be aware that coloring text this way will prevent keywords from being detected.
For cards that generate other cards, you can use the cardsToPreview
variable (feature added in the Watcher update). The name is plural, but it only holds one card. It's possible that the devs were considering supporting multiple cards but ended up sticking with just one. Since the names of cards aren't keywords, you should use *
to highlight the name. Look at any Shiv generating card like Blade Dance to see how to set up cardsToPreview
.
Try to keep your card descriptions relatively short. If you're reaching 6 or 7+ lines, there's a decent chance that your card is doing too many things. If you're fine with that, it's not a problem, but try to avoid having too many complicated cards. Most people don't like reading that much.
Keyword ordering:
Innate.
Retain/Ethereal. (Retain overrides ethereal, so don't put both on one card.)
Card Description.
Exhaust.
In general, "passive" effects such as Innate and Retain go at the start of the card's text and things that happen on play go at the end. You can treat it like happening in order; Innate
happens at the start of the combat, so it goes first. Retain
and Ethereal
are always happening, so they go next. The card is Exhausted
after you play it, so it goes at the end.
BaseMod also provides an additional feature not found in the base game that allows for text that is automatically changed based on certain variables. The details for this can be found here.