Custom Spell Cards - Monster-Train-2-Modding-Group/Trainworks-Reloaded GitHub Wiki
In this tutorial, we'll create several increasingly complex spell cards. For now, we'll add them to the Banished clan.
The first card we’ll make is the simplest kind of spell card — one that deals damage to an enemy unit.
It will also use a keyword, which from this point on we’ll refer to as a CardTrait.
To illustrate, we’ll make a card similar to the Hellhorned clan’s Horn Break. It's a card that deals 5 damage and has the Piercing trait.
The card will use fully custom (non-animated) art.
Here’s the complete JSON for Not Horn Break. Don’t worry if it looks long — we’ll go through it step by step afterward.
"cards": [
{
"id": "NotHornBreak",
"cost": 1,
"names": {
"english": "Not Horn Break",
},
"descriptions": {
"english": "Deal [effect0.power] damage"
},
"rarity": "common",
"targets_room": true,
"targetless": false,
"class": "ClassBanished",
"card_art": "@NotHornBreakCardArt",
"pools": ["MegaPool"],
"traits": ["@PiercingTrait"],
"effects": ["@Deal5Damage"]
}
],
"traits": [
{
"id": "PiercingTrait",
"name": "CardTraitIgnoreArmor"
}
],
"effects": [
{
"id": "Deal5Damage",
"name": "CardEffectDamage",
"target_mode": "drop_target_character",
"target_team": "both",
"param_int": 5
}
],
"game_objects": [
{
"id": "NotHornBreakCardArt",
"type": "card_art",
"extensions": {
"card_art": {
"sprite": "@NotHornBreakSprite"
}
}
}
],
"sprites": [
{
"id": "NotHornBreakSprite",
"path": "textures/NotHornBreak.png"
}
]Let’s understand what each field in the cards section means:
-
id— The internal ID for the card. Everything you define (Cards, Traits, Effects, etc.) must have a unique ID. -
names— Localized card names. The game supports 10 languages: english, french, german, russian, portuguese, chinese, spanish, chinese_traditional, korean, and japanese. -
descriptions— The visible card text. Use placeholders like[effect0.power]instead of hardcoding numbers — this keeps upgrades and scaling working properly. -
cost— Ember cost to play the card. -
rarity— Can becommon,uncommon,rare, orchampion. -
targets_room/targetless— Define how the player targets with this card. These don’t specify what is targeted, only how targeting behaves. -
class— The clan this card belongs to (e.g.,ClassBanished,ClassPyreborne, or your custom clan). -
traits,effects,pools,card_art— References to other objects you define below. Always prefix references to objects in your own JSON with@.
Specific interesting fields set on NotHornBreak.
-
"class": "ClassBanished"— Here we specify ClassBanished as it is the internal name of the Banished clan. Since this is something defined in the base game you should not prefix it with an @. -
"pools": ["MegaPool"]— This adds the card to the MegaPool, which is the card pool used for all regular draftable (non–banner unit) cards. Again this is a CardPool defined in the base game so do not prefix it with an @. -
"targetless": false— Setting this to false gives the arrow indicator when you play the card.
In the traits section we define a list of CardTraits that the card uses.
"traits": [
{
"id": "PiercingTrait",
"name": "CardTraitIgnoreArmor"
}
]-
id— The internal ID for this card trait. No @ prefix is needed since we’re defining it here. -
name— This required property specifies the class name that defines how this CardTrait behaves. In this case, the Piercing keyword is handled by the classCardTraitIgnoreArmor.
In the effects section we define a list of CardEffects that the card uses.
"effects": [
{
"id": "Deal5Damage",
"name": "CardEffectDamage",
"target_mode": "drop_target_character",
"target_team": "both",
"param_int": 5
}
]-
name— Just like in the traits section, this required property defines which CardEffect class to use — the code that runs when the effect is applied. Here we useCardEffectDamagewhich is the CardEffect that deals damage to the units targeted. -
target_mode— This specifies what the card effect targets. Here we specifydrop_target_characterwhich means the character that was selected when the card was played. -
target_team— This property filters the list of possible targets fromtarget_mode. Here we want to be able to target both Heroes (enemy units) and Monsters (friendly units) so we use the value "both". -
param_int— The integer parameter used by the CardEffect. Its meaning depends on the effect type. Since we’re using CardEffectDamage, this value sets the base amount of damage dealt. In the card’s description, you can reference this with[effectX.power], where X is the index of the effect in the card’s list.
Lastly we specify the card_art which refers to a CardArt GameObject the framework builds for us. We give it a sprite to use for the artwork. This is done through the game_object and sprites list in JSON.
This setup will look almost identical for every card you make — just swap out the sprite path for your own art.
"game_objects": [
{
"id": "NotHornBreakCardArt",
"type": "card_art",
"extensions": {
"card_art": {
"sprite": "@NotHornBreakSprite"
}
}
}
],
"sprites": [
{
"id": "NotHornBreakSprite",
"path": "textures/NotHornBreak.png"
}
]Once your JSON is saved, rebuild your project and copy to the Bepinex/plugins folder.
If your card doesn’t appear, double-check:
- All
@references match defined IDs - There are no trailing commas in your JSON
- The sprite path is correct
Make sure that the BepinEx logs don't produce any warning or error text, If you have the console embled simply checking for red/yellow text.
Next, we’ll create a spell card that applies a status effect and learn more about different target_mode values.
This spell will be called “Give Everyone Armor.”
It has a unique effect — it grants Armor 2 to every unit on the current floor, both friendly and enemy.
Here’s the full JSON definition for the card.
We’ll go over the new parts afterward.
"cards": [
{
"id": "GiveEveryoneArmor",
"cost": 1,
"names": {
"english": "Give Everyone Armor",
},
"descriptions": {
"english": "Apply [armor] [effect0.status0.power] to all units."
},
"rarity": "uncommon",
"targets_room": true,
"targetless": true,
"class": "ClassBanished",
"card_art": "@GiveEveryoneArmorCardArt",
"pools": ["MegaPool"],
"effects": ["@GiveAllUnitsArmor"]
}
],
"effects": [
{
"id": "GiveAllUnitsArmor",
"name": "CardEffectAddStatusEffect",
"target_mode": "room",
"target_team": "both",
"param_status_effects": [
{
"status": "armor",
"count": 2
}
]
}
],
"game_objects": [
{
"id": "GiveEveryoneArmorCardArt",
"type": "card_art",
"extensions": {
"card_art": {
"sprite": "@GiveEveryoneArmorSprite"
}
}
}
],
"sprites": [
{
"id": "GiveEveryoneArmorSprite",
"path": "textures/GiveEveryoneArmor.png"
}
]You’ll notice that this card looks very similar to Not Horn Break.
Let’s focus only on the important differences.
-
descriptions—[armor]is a special placeholder that is automatically replaced with the localization for Armor.
[effect0.status0.power]references the number of stacks of the first status effect in the first effect.
Important
Never hardcode numbers in your descriptions! In this case Status stacks can be modified dynamically by upgrades such as Doublestack or Dualism, and hardcoding values will make your text inaccurate when these are applied to the card.
The way this card targets units is different.
-
targetless: true— This card does not target a specific character. Instead, it applies its effect to every valid target in the room automatically. -
target_mode: "room"— Because we want the effect to apply to all units on the floor, we use"room"as the target mode. This tells the game to find all characters in that room. -
target_team: "both"— Ensures the effect applies to both friendly and enemy units.
The full effect definition for reference:
"effects": [
{
"id": "GiveAllUnitsArmor",
"name": "CardEffectAddStatusEffect",
"target_mode": "room",
"target_team": "both",
"param_status_effects": [
{
"status": "armor",
"count": 2
}
]
}
]-
name— This determines the effect type. In this case, we useCardEffectAddStatusEffect, a very general-purpose effect used by almost all cards that apply status effects. -
param_status_effects— This works likeparam_intbut for status effects. It defines what effect to apply and how many stacks to give."param_status_effects": [ { "status": "armor", "count": 2 } ]
-
status— The internal ID of the status effect to apply. -
count— How many stacks of that status effect to give.
-
Warning
Only include one status effect in this list. If you provide multiple, CardEffectAddStatusEffect will pick one at random when the effect plays.
-
param_int(optional) — For this CardEffect,param_intis interpreted as a chance (0–100) to apply the status effect.
If omitted, the chance defaults to 100%.
Add Give Everyone Armor to your starter deck, then start a run to verify that:
-
It appears with the right name and cost.
-
Its description shows “Apply Armor 2 to all units.”
-
When played, all characters in the selected room gain 2 Armor.
If everything works, congratulations — you’ve just created your first status effect card!
Next, we’ll create a card that applies a CardUpgrade to a unit, introducing the basics of the game’s upgrade system.
We’ll recreate a classic Awoken card — Razorsharp Edge. If you’re unfamiliar, it gives a unit +10 attack and –2 max HP.
Our version, Not Razorsharp Edge, does exactly that.
"cards": [
{
"id": "NotRazorsharpEdge",
"cost": 1,
"names": {
"english": "Not Razorsharp Edge",
},
"descriptions": {
"english": "Apply +[effect0.upgrade.bonusdamage] and [effect0.upgrade.bonushp] to a unit."
},
"rarity": "uncommon",
"targets_room": true,
"targetless": false,
"class": "ClassBanished",
"card_art": "@NotRazorsharpEdgeCardArt",
"pools": ["MegaPool"],
"effects": ["@ApplyUpgrade"]
}
],
"effects": [
{
"id": "ApplyUpgrade",
"name": "CardEffectAddCardUpgradeToUnits",
"target_mode": "drop_target_character",
"target_team": "both",
"param_upgrade": "@TenAttackMinusTwoHealth",
"param_int_3": 0
}
],
"upgrades": [
{
"id": "TenAttackMinusTwoHealth",
"bonus_damage": 10,
"bonus_hp": -2
}
],
"game_objects": [
{
"id": "NotRazorsharpEdgeCardArt",
"type": "card_art",
"extensions": {
"card_art": {
"sprite": "@NotRazorsharpEdgeSprite"
}
}
}
],
"sprites": [
{
"id": "NotRazorsharpEdgeSprite",
"path": "textures/NotRazorsharpEdge.png"
}
]We’re seeing a few new sections and concepts for the first time here — especially the "upgrades" list.
-
descriptions:effect0.upgraderefers to theparam_upgradein the first CardEffect (index 0). You can then access any field from that upgrade, such asbonusdamageorbonushp. This keeps your description automatically in sync with the upgrade’s actual values — no need to hardcode numbers!
The effect is a little different than what we've seen before, so we'll cover the differences.
"effects": [
{
"id": "ApplyUpgrade",
"name": "CardEffectAddCardUpgradeToUnits",
"target_mode": "drop_target_character",
"target_team": "both",
"param_upgrade": "@TenAttackMinusTwoHealth",
"param_int_3": 0
}
],-
name: This uses the effectCardEffectAddCardUpgradeToUnits, which handles applying upgrades to units.There are three upgrade lifetimes this effect can apply:
-
Transient (1) — lasts until the unit dies,
-
Temporary (0) — lasts until the end of the battle (default).
-
Permanently (2) — lasts for the entire run.
-
Tip
In most cases, stat-only upgrades should use Temporary (0), following the game’s convention.
-
param_upgrade: This is the upgrade we’ll apply — in this case, our"TenAttackMinusTwoHealth"upgrade which we define below. -
param_int_3: Controls upgrade lifetime.
0= temporary (until battle ends),1= transient,2= permanent.
If omitted, defaults to 0.
"upgrades": [
{
"id": "TenAttackMinusTwoHealth",
"bonus_damage": 10,
"bonus_hp": -2
}
],This new "upgrades" section defines reusable stat or ability modifiers.
-
bonus_damage— increases (or decreases) a unit’s attack -
bonus_hp— increases (or decreases) a unit’s max health
You can also add card traits, triggers, status effects, a unit ability, or other modifiers here — this system supports everything from simple stat boosts to complex effect layers.
When a CardUpgrade is applied:
-
If the upgrade is temporary or permanent, it’s applied to both the unit and its card.
-
If the upgrade is transient, it affects only the unit, not the card.
This means:
-
Temporary / Permanent upgrades persist when viewing the card in your deck.
-
Transient upgrades disappear when the unit dies and don’t change the card’s stats.
That’s why stat-only buffs like Razorsharp Edge typically use temporary upgrades — they should last for the battle but not modify the card forever.
-
Load the mod and start a battle.
-
Play Not Razorsharp Edge on a friendly unit.
-
Watch its stats:
-
Attack should increase by +10
-
Max HP should decrease by 2
-
-
Open your Deck during battle — the modified card should reflect these stat changes.
-
Change
"param_int_3": 1and test again — now the upgrade is transient, so only the in-battle unit changes.
Now that we’ve explored basic effects and upgrades, let’s take a look at scaling — effects that grow based on tracked in-game statistics.
In this lesson, we’ll create Play Other Cards.
For the low cost of 2 Ember, this spell gives one of your units +2 Attack per card played this battle.
This introduces a key concept: CardTraits that modify upgrades dynamically at runtime.
"cards": [
{
"id": "PlayOthercards",
"cost": 2,
"names": {
"english": "Play Other Cards",
},
"descriptions": {
"english": "Apply +[trait0.power][attack] per card played this battle."
},
"rarity": "rare",
"targets_room": true,
"targetless": false,
"class": "ClassBanished",
"card_art": "@PlayOtherCardsCardArt",
"pools": ["MegaPool"],
"traits": ["@PlayOtherCardsScalingTrait"],
"effects": ["@ApplyScaledUpgrade"]
}
],
"effects": [
{
"id": "ApplyScaledUpgrade",
"name": "CardEffectAddTempCardUpgradeToUnits",
"target_mode": "drop_target_character",
"target_team": "both",
"param_upgrade": "@PlayOtherCardsUpgrade"
}
],
"traits": [
{
"id": "PlayOtherCardsScalingTrait",
"name": "CardTraitScalingUpgradeUnitAttack"
"track_type": "any_card_played",
"entry_duration": "this_battle",
"param_int": 2,
}
]
"upgrades": [
{
"id": "PlayOtherCardsUpgrade",
"bonus_damage": 0,
}
],
"game_objects": [
{
"id": "PlayOtherCardsCardArt",
"type": "card_art",
"extensions": {
"card_art": {
"sprite": "@PlayOtherCards"
}
}
}
],
"sprites": [
{
"id": "PlayOtherCardsSprite",
"path": "textures/PlayOtherCardsSprite.png"
}
]This card introduces a new mechanic — scaling based on gameplay statistics — which uses CardTraits to modify a CardEffect’s upgrade dynamically.
-
description: We fetch theparam_intvalue from the first CardTrait (index 0) with the replacement tag[trait0.power]. In our case, that’s the2from"param_int": 2in the CardTrait defined below.[attack]is another special replacement tag and will be replaced by the Attack icon.
You'll see that this is quite similar to Not Razorsharp Edge.
"effects": [
{
"id": "ApplyScaledUpgrade",
"name": "CardEffectAddCardUpgradeToUnits",
"target_mode": "drop_target_character",
"target_team": "both",
"param_upgrade": "@PlayOtherCardsUpgrade"
}
],-
name:
We useCardEffectAddTempCardUpgradeToUnits— the same base effect that applies upgrades to units, but here it works in conjunction with a scaling trait. -
param_upgrade:
Points to ourPlayOtherCardsUpgrade, which starts withbonus_damage = 0.
On its own, the effect does nothing — the magic happens in the trait.
"upgrades": [
{
"id": "PlayOtherCardsUpgrade",
"bonus_damage": 0,
"upgrade_will_be_scaled_by_non_magic_power_trait": true
}
],This upgrade serves as a template that the trait modifies when applied.
-
bonus_damage:
Defaults to0. The trait will increase this based on how many cards have been played. -
upgrade_will_be_scaled_by_non_magic_power_trait:
Must be set totruewhenever a non-magic CardTrait (likeCardTraitScalingUpgradeUnitAttack) modifies an upgrade.
Without this, the upgrade might be incorrectly reset by effects like Reform.
"traits": [
{
"id": "PlayOtherCardsScalingTrait",
"name": "CardTraitScalingUpgradeUnitAttack"
"param_tracked_value": "any_card_played",
"param_entry_duration": "this_battle",
"param_int": 2,
}
]-
name:
CardTraitScalingUpgradeUnitAttackis a CardTrait that intercepts all upgrades that will be applied to a unit from the effects and then adjusts their attack values dynamically. Here it will intercept the upgrade that is applied from the effect idApplyScaledUpgrade -
param_tracked_value:
The statistic this trait monitors.
"any_card_played"increments every time the player plays a card — any card. -
param_entry_duration:
The time window to track the statistic:-
"this_turn"— only the current turn -
"previous_turn"— only the last turn -
"this_battle"— cumulative count for the current battle (used here)
-
-
param_int:
The per-event scaling value.
For each card played this battle, +2 attack is added to the upgrade’sbonus_damage.
Important
Scaling CardTraits are only designed for Spells.
Applying them to non-Spell cards can lead to undefined behavior or bugs.
-
Add Play Other Cards to your starting deck or initial hand.
-
Start a battle and play several cards.
-
Notice how each card play increases the power of Play Other Cards — the description dynamically updates.
-
When you finally cast it on a unit, that unit gains +2 Attack per card played this battle.
-
Verify that the applied upgrade appears correctly in the deck.
You may notice that Play Other Cards doesn’t gain an automatic keyword (unlike, say, Piercing from CardTraitIgnoreArmor in Not Horn Break).
That’s because not all CardTraits are represented with keywords —
CardTraitScalingUpgradeUnitAttack is a internal trait that operates behind the scenes.
So far, all of our cards have done something when played.
Now we’ll explore another core concept: Card Triggers — effects that activate automatically when certain in-game conditions are met.
A CardTriggerEffect combines a trigger type (when it activates) with one or more CardEffects (what it does).
In this lesson, we’ll create Pyre Sharpener, an unplayable card that has a Reserve trigger.
At the end of each turn — if this card remains in your hand — it will grant your Pyre +3 Attack.
"cards": [
{
"id": "PyreSharpener",
"cost": 0,
"cost_type": "unplayable",
"names": {
"english": "Pyre Sharpener",
},
"rarity": "common",
"targets_room": "false",
"targetless": false,
"class": "ClassBanished",
"card_art": "@PyreSharpenerCardArt",
"traits": ["@PyreSharpenerUnplayable"],
"triggers": ["@BuffThePyreOnUnplayed"],
"pools": ["MegaPool"],
}
],
"traits": [
{
"id": "PyreSharpenerUnplayable",
"name": "CardTraitUnplayable"
}
],
"effects": [
{
"id": "BuffThePyre",
"name": "CardEffectBuffDamage",
"target_mode": "pyre",
"param_int": 3
}
],
"card_triggers": [
{
"id": "BuffThePyreOnUnplayed",
"trigger": "on_onplayed_positive",
"descriptions": {
"english": "Apply +[effect0.power][attack] to your Pyre."
},
"effects": ["@BuffThePyre"]
}
],
"game_objects": [
{
"id": "PyreSharpenerCardArt",
"type": "card_art",
"extensions": {
"card_art": {
"sprite": "@PyreSharpenerSprite"
}
}
}
],
"sprites": [
{
"id": "PyreSharpenerSprite",
"path": "textures/PyreSharpener.png"
}
]This card introduces two new pieces of data: card triggers and the unplayable cost type.
- Setting
"cost_type": "unplayable"hides the Ember icon from the card’s UI.
However, this alone does not actually make the card unplayable — it’s purely cosmetic. - You may also notice the card has no
descriptionsproperty. This is intended as CardTriggers add themselves to the card text.
To truly make a card unusable by the player, you’ll need to give it a CardTrait.
"traits": [
{
"id": "Unplayable",
"name": "CardTraitUnplayable"
}
],-
CardTraitUnplayableprevents the player from playing the card at all.
This is what actually enforces the “unplayable” behavior. -
Many traits modify a card’s visuals or rules like this;
CardTraitUnplayableis one of the most straightforward examples.
"effects": [
{
"id": "BuffThePyre",
"name": "CardEffectBuffDamage",
"target_mode": "pyre",
"param_int": 3
}
],-
name:
CardEffectBuffDamagetemporarily increases the attack value of a unit or pyre.
It’s great for heroes or the Pyre, but shouldn’t be used for standard units (their stat buffs should generally persist). -
target_mode:
"pyre"directs the effect specifically to the Pyre.
CardTriggers get their own top-level section — just like effects, traits, and upgrades.
"card_triggers": [
{
"id": "BuffThePyreOnUnplayed",
"trigger": "on_onplayed_positive",
"descriptions": {
"english": "Apply +[effect0.power][attack] to your Pyre."
},
"effects": ["@BuffThePyre"]
}
],-
trigger
Defines when the trigger activates.
For Reserve-style effects, use either:-
"on_unplayed_positive"– green (positive) Reserve border -
"on_unplayed_negative"– red (negative) Reserve border
This card uses the positive variant to indicate a beneficial trigger.
-
-
descriptions
Unlike earlier cards, we don’t set a description directly in the"cards"section.
Triggers automatically append their description to the card’s text area.
This makes it easy to show multiple triggers cleanly.We also use substitution text here:
-
[effect0.power]– refers to theparam_int(3) of the first effect in the CardTrigger -
[attack]– shows the attack icon.
-
-
effects
A list of effects that execute when the trigger condition is met.
Here, that’s our single effect:@BuffThePyre.effects: Here we specify we want to run the effect referenced by "BuffThePyre"
-
Add Pyre Sharpener to your starting deck.
-
Notice that:
-
The card has no Ember icon (due to
cost_type: unplayable). -
The card’s frame and border glow differently — a visual cue from the
CardTraitUnplayableand Reserve trigger.
-
-
End a few turns without playing it.
Each time, your Pyre’s Attack should increase by +3.
The next card in our tutorial introduces a new concept — conditional effects and card pools.
Up to this point, our cards have performed a set of effects that always succeeded when played. In this lesson, we’ll explore how to test whether an effect can occur and how to cancel subsequent effects if that test fails. We’ll also learn how to define custom CardPools, which allow us to randomly pick from a group of cards.
Let’s make Hymnist Recruit, a card that adds one random Hymnist card to your hand, and if you have 50 gold it is spent to add a second one!
"cards": [
{
"id": "HymnistRecruit",
"cost": 1,
"names": {
"english": "Hymnist Recruit",
},
"descriptions": {
"english": "Add an <b>Hymnist</b> to your hand. Spend 50[coin] and add another."
},
"rarity": "common",
"targets_room": true,
"targetless": false,
"class": "ClassBanished",
"card_art": "@HymnistRecruitCardArt",
"pools": ["MegaPool"],
"effects": [
"@RecruitAHymnist",
"@Spend50GoldIfPossible",
"@RecruitAHymnist"
]
}
],
"effects": [
{
"id": "RecruitAHymnist",
"name": "CardAddBattleCard",
"param_int": 3,
"param_int_2": 1,
"param_bool": true,
"param_card_pool": ["@HymnistRecruitPool"]
},
{
"id": "Spend50GoldIfPossible",
"name": "CardEffectRewardGold",
"param_int": -50,
"param_bool": true,
"should_cancel_subsequent_effects_if_test_fails": true
}
],
"card_pools": [
{
"id": "HymnistRecruitPool",
"cards": [
"DeathmetalHymnist",
"LeadSongbird",
"PunkrockReveler",
"UpbeatWarbler"
]
}
],
"game_objects": [
{
"id": "HymnistRecruitCardArt",
"type": "card_art",
"extensions": {
"card_art": {
"sprite": "@HymnistRecruitSprite"
}
}
}
],
"sprites": [
{
"id": "HymnistRecruitSprite",
"path": "textures/HymnistRecruit.png"
}
]
This card demonstrates how multiple effects can interact with one another in sequence — with the possibility that some of them may not execute depending on certain conditions.
Let’s break down the key parts.
descriptions: If you want bolded text in your card descriptions just surround the text you want bolded in the HTML tag for bold <b></b>. Several other HTML tags may work in the descriptions too! And yes gold has its own special replacement tag [coin].
effects: You can have more than 1 effect in a card and you can reuse effects. Listing the same id in the list will just grab the same card effect reference when the card is built, so you don't have to copy and paste the effects.
There are two effect definitions here: one to add a Hymnist card and another to spend gold.
{
"id": "RecruitAHymnist",
"name": "CardAddBattleCard",
"param_int": 3,
"param_int_2": 1,
"param_bool": true,
"param_card_pool": ["@HymnistRecruitPool"]
}
This effect adds a random card to a pile — in this case, your hand.
-
name: The effect used isCardAddBattleCard, which adds one or more cards to a specific pile and can optionally upgrade them with a CardUpgrade. The card is always added to the deck temporarily until the end of battle (for permanent addition of cards to the deck useCardEffectAddRunCard) -
param_int: Controls which card pile to place the new card into. This corresponds to theCardPileenum:Value CardPile Name Description 0 DeckPile Bottom of the deck 1 DiscardPile Discard pile 2 ExhaustedPile Consume pile 3 HandPile Hand (what we use here) 4 KeepInStandBy Invalid for this effect 5 DeckPileTop Top of the deck 6 None Invalid for this effect 7 DeckPileRandom Random deck position 8 EatenPile Eaten Pile -
param_int_2: The number of cards to add (1 in this case). -
param_bool: Requires that there be space in the player’s hand. If the hand is full, the effect won’t add the card. -
param_card_pool: Specifies which pool to draw the card(s) from — here,HymnistRecruitPoolwhich we will define below. Again it is a reference so don't forget the @ when refering to the pool from other locations.
{
"id": "Spend50GoldIfPossible",
"name": "CardEffectRewardGold",
"param_int": -50,
"param_bool": true,
"should_cancel_subsequent_effects_if_test_fails": true
}Despite its name, CardEffectRewardGold can also remove gold by providing a negative value in param_int.
-
param_int: -50 means this effect costs 50 gold. -
param_bool: If set totrue, this means the player must have at least 50 gold for the effect to succeed. -
should_cancel_subsequent_effects_if_test_fails: This introduces the effect test system.Every
CardEffectin the game performs a test before applying. If the test fails, the effect doesn’t run.
Here, if the player doesn’t have enough gold, not only does the gold deduction fail — the following effect (RecruitAHymnist) is also skipped entirely.In the case of
RecruitHymnistthe test forCardEffectAddBattleCardis set to fail if the players hand is full.
If you instead wanted the card to be unplayable when the player can’t afford the cost, you could use in place of should_cancel_subsequent_effects_if_test_fails
"should_fail_to_cast_if_test_fails": trueAnd additionally in some cases it may be preferable to skip testing altogether, this may be necessary if you are making cards that have effects that target units and cards.
"should_test": falseBy default, should_test is true. Setting it to false is uncommon, if you aren't sure leave it set to true. A good rule of thumb to look up how a base game card that is similar to the card you are designing and see what effects have it set to false.
As mentioned, Hymnist Recruit draws from a pool of Hymnist cards.
"card_pools": [
{
"id": "HymnistRecruitPool",
"cards": [
"DeathmetalHymnist",
"LeadSongbird",
"PunkrockReveler",
"UpbeatWarbler"
]
}
],
Notice that when referencing base game cards, you must use their internal asset names, not their in-game English names. Punk Shredder is an outliner here in that its internal asset name is PunkrockReveler.
Also each card listed here is equally likely to be picked by CardEffectAddBattleCard as they are all listed once. However, you can control probability by listing cards multiple times.
By listing 10 cards with cards appearing multiple times I can get the following probabilities:
| Entries | Card | Result Probability |
|---|---|---|
| 1 | Deathmetal Hymnist | 10% |
| 3 | Lead Songbird | 30% |
| 4 | Upbeat Warbler | 40% |
| 2 | Punk Shredder (PunkrockReveler) | 20% |
And the associated Json definition to make the above setup.
{
"id": "HymnistRecruitPoolAdjusted",
"cards": [
"DeathmetalHymnist",
"LeadSongbird", "LeadSongbird", "LeadSongbird",
"PunkrockReveler", "PunkrockReveler",
"UpbeatWarbler", "UpbeatWarbler", "UpbeatWarbler", "UpbeatWarbler"
]
}When you play Hymnist Recruit:
-
The first
RecruitAHymnistadds one random Hymnist card to your hand. -
The game then attempts to spend 50 gold.
-
If you have at least 50 gold, the effect succeeds.
-
If you don’t, the test fails and the next effect is canceled.
-
-
If you had enough gold, the final
RecruitAHymnistruns — giving you a second Hymnist card!
Add Hymnist Recruit to your deck and start a run.
-
Play the card with at least 50 gold.
→ You should receive two Hymnist cards in your hand. -
Play it with less than 50 gold.
→ You should receive only one Hymnist card, and no gold will be deducted. -
Try filling your hand to the limit and see that the effect won’t add more than allowed.
-
You can chain multiple effects together on a card.
-
should_cancel_subsequent_effects_if_test_failslets you control conditional behavior by cancelling effects if a test fails. -
CardEffectRewardGoldhandles both giving and spending gold. -
CardPools let you randomly select from a set of cards, with optional weighting.