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.
Overview: This is the simplest of spell cards. One that Damages an enemy unit. Along with a CardTrait.
The first spell we're going to make is similar to the classic from Hellhorned Horn Break. It's a card that deals 5 damage and has the Piercing trait. It will also use fully custom (non-animated) art.
That's not the only thing I've prepared for us, though! Going through this line-by-line's a bit hard to follow, so I will drop the whole darn card on you and explain it afterward. Here:
"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": "@NotHornBreak",
"pools": [
"MegaPool"
],
"traits": [
{
"id": "@Piercing"
}
],
"effects": [
{
"id": "@Deal5Damage"
}
]
}
],
"traits": [
{
"id": "Piercing",
"name": "CardTraitIgnoreArmor"
}
],
"effects": [
{
"id": "Deal5Damage",
"name": "CardEffectDamage",
"target_mode": "drop_target_character",
"target_team": "both",
"param_int": 5
}
],
"game_objects": [
{
"id": "NotHornBreak",
"type": "card_art",
"extensions":
{
"card_art":
{
"sprite": "@NotHornBreak"
}
}
}
],
"sprites": [
{
"id": "NotHornBreak",
"path": "textures/NotHornBreak.png"
}
]
This is very long and intimidating, but it's actually extremely simple. Promise. Let's go through one field at a time:
-
id
: This is the internal ID of your card. Each Card, Trait, Effect whatever needs to have a unique ID. -
names
: This specifies the names of the card in various languages. The game supports 10 langauges (english, franch, german, russian, portuguese, chinese, spanish, chinese_traditional, korean, and japanese) the field names are the ones just listed. -
descriptions
: This is the card text, excluding traits like piercing (which add themselves). Note the[effect0.power]
. This is how you get numbers to show up in your card description. Don't hardcode them! otherwise, you will break shop upgrades! Get them from your effects instead. In this case, it'll get the power (param_int) of the effect at index 0 in the card's effects list. -
cost
: The ember cost. Pretty self-explanatory. -
rarity
: The valid rarities are common, uncommon, rare, and champion. -
targets_room
andtargetless
: Values for these aren't always obvious. When in doubt, look up a card with a similar targeting mechanism in MonsterTrainGameData and copy it. -
class
: This is the clan name (ClassBanished, ClassPyreborne, or a Custom one) -
card_art
: This is an ID to a card_art GameObject. The @ makes the framework refer to an object within your own JSON don't forget it when you want to reference an object! -
pools
: The card pools the card can appear in. For almost all spell cards, you'll want to put them into the MegaPool, which is the one used by the game for most things. -
traits
: These are references to CardTraits we want on the card. Like card_art we want to reference something from the traits list so don't forget the @. -
effects
: Same as above.
The Traits list This is where we define CardTraits that the card uses. We give each CardTrait an id so when the card is made it knows which CardTrait to reference.
-
name
- This is the more important parameter, this defines which CardTrait class to use. If you aren't a programmer, this determines what CardTrait to use. Here the Piercing keyword internally is handled by the CardTrait classCardTraitIgnoreArmor
. We specify which class to use vianame
of the CardTrait.
The Effects list This is where we define CardEffects that the card uses. We give each CardEffect an id so when the card is made it knows which CardEffect to reference.
-
name
- Like CardTrait this is the more important parameter. To non programmers this is the command that tells the game what effect to play. To programmers this is the CardEffect class to use. Here we wantCardEffectDamage
which is a CardEffect that does damage of course! -
target_mode
- A basic parameter of the CardEffect we can tell the CardEffect what to target. "drop_target_character" just means the Character selected as a target. -
target_team
- This parameter 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 say both. -
param_int
- Finally this parameter specifies a number. The meaning of this depends on the CardEffect. Since we are specifying CardEffectDamage this specifies the base amount of damage to apply. Tying back intodescriptions
you can fetch this value in a description by [effectX.power] where X is the index of the effect within the card.
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 the JSON file. This is always be the similar for each card we make just with a different sprite.
Overview: This is the spell card that applies a status effect. Also more on TargetModes.
Our next card will be named "Give Everyone Armor." This spell has the wholly unique effect of giving everyone on the current floor 2 Armor. It serves as an introduction to creating cards that apply status effects.
Download the fully custom card art if you haven't already from here.
"cards": [
{
"id": "GiveEveryoneArmor",
"cost": 1,
"names": {
"english": "Give Everyone Armor",
},
"descriptions": {
"english": "Give everyone <nobr>[armor] [effect0.status0.power]</nobr>"
},
"rarity": "uncommon",
"targets_room": true,
"targetless": true,
"class": "ClassBanished",
"card_art": "@GiveEveryoneArmor",
"pools": [
"MegaPool"
],
"effects": [
{
"id": "@GiveAllUnitsArmor"
}
]
}
],
"effects": [
{
"id": "GiveAllUnitsArmor",
"name": "CardEffectAddStatusEffect",
"target_mode": "room",
"target_team": "both",
"param_status_effects": [
{
"status": "armor",
"count": 2
}
]
}
],
"game_objects": [
{
"id": "GiveEveryoneArmor",
"type": "card_art",
"extensions":
{
"card_art":
{
"sprite": "@GiveEveryoneArmor"
}
}
}
],
"sprites": [
{
"id": "GiveEveryoneArmor",
"path": "textures/GiveEveryoneArmor.png"
}
]
You'll note that not much is different from Not Horn Break. Let's discuss the following things that are different:
-
descriptions
: [armor] is a special replacement text that will sub it out with the localized text for armor. To reference the amount of stacks of a status effect from the Card's effects we reference the first effect's (effect0), first status (status0), count (power) hence effect0.status0.power. Again its important not to hardcode the value as things such as Doublestack, Dualism (for characters), will modify the actual amount applied. -
targetless
: This card does not target anything specific, so it is targetless.
Effects list
-
name
: This is no different from when we putCardEffectDamage
, but I'm calling attention to it regardless because I'd like to point out the obvious in how general this effect type is. All status effects use this same type,CardEffectAddStatusEffect
. Again these are classes you can look up in your C# decompiler to see how they work. -
target_mode
: It targets the entire room so we use the value "room" to see the full list of possible values see the TargetMode enum in the C# decompiler. -
param_status_effects
: Like param_int from Not Horn Break, except for status effects. ForCardEffectAddStatusEffect
, it applies a single status effect so we useparam_status_effects
to specify that. Remember to give it only 1 status effect, otherwise it will apply one status effect from the list at random. This CardEffect also accepts param_int, but its use here is much different than Not Horn Break. If param_int is specified then it acts as a probability to apply the status effect. If we don't specify param_int then it will 100% apply the status effect. Different CardEffects will use these parameters differently.
Add it to your starter deck to make sure it works, then move on to the next card of this tutorial!
Overview: This is a card that applies a CardUpgrade to a unit. Basics of CardUpgrades.
Next we will touch upon another thing cards can do, apply upgrades to characters. Here we will be recreating another classic Razorsharp Edge. This card as you may know gives a unit an upgrade +10 attack damage but -2 health.
"cards": [
{
"id": "NotRazorsharpEdge",
"cost": 1,
"names": {
"english": "Not Razorsharp Edge",
},
"descriptions": {
"english": "Apply +[effect0.upgrade.bonusdamage] and [effect0.upgrade.bonusp] to a unit."
},
"rarity": "uncommon",
"targets_room": true,
"targetless": false,
"class": "ClassBanished",
"card_art": "@NotRazorsharpEdge",
"pools": [
"MegaPool"
],
"effects": [
{
"id": "@ApplyUpgrade"
}
]
}
],
"effects": [
{
"id": "ApplyUpgrade",
"name": "CardEffectAddTempCardUpgradeToUnits",
"target_mode": "drop_target_character",
"target_team": "both",
"param_upgrade": "@TenAttackMinusTwoHealth"
"param_bool": false
}
],
"upgrades": [
{
"id": "TenAttackMinusTwoHealth",
"bonus_damage": 10,
"bonus_hp": -2
}
],
"game_objects": [
{
"id": "NotRazorsharpEdge",
"type": "card_art",
"extensions":
{
"card_art":
{
"sprite": "@NotRazorsharpEdge"
}
}
}
],
"sprites": [
{
"id": "NotRazorsharpEdge",
"path": "textures/NotRazorsharpEdge.png"
}
]
-
descriptions
: More special replacement text syntax. Hereeffect0.upgrade
references param_upgrade of the first CardEffect in the card. We then get the bonus_damage and bonus_hp of the CardUpgrade.
Effects list
-
name
: A new CardEffect type. CardEffectAddTempCardUpgradeToUnits, despite its name. Allows you to apply a CardUpgrade to Units, either transient (until it dies), temporary (until the end of the scenario), or permanently (lasting the rest of the run). Usually with stat upgrades to keep the convention you will want them to apply until the end of the scenario. -
param_upgrade
: The CardUpgrade to apply. This is a reference to the "upgrades" list. Remember to reference an object within your own plugin don't forget the @ before the id. -
param_bool
: I just tossed this in to illustrate the different knobs of this card effect. Because I set this to false the CardEffect will apply the upgrade temporarily. If you want a permanent upgrade then set it to true. -
param_bool2
: If you wanted a transient upgrade set this to true. By default boolean params will be false so leaving it out makes it false. I will explain how CardUpgrades work in the next section.
Upgrades list
-
bonus_damage
/bonus_hp
: self explanatory. These parameters increase or decrease the attack damage or health of a unit here.
So this card effect upgrades a character. How the upgrade process works is first the upgrade is applied to a character changing it's stats (among other things). Then the Upgrade is applied to the characters spawner card if it is a monster.
When a card is upgraded it is applied to a list of upgrades that are either temporary or permanent. The temporary upgrades are cleared at the end of battle.
If a monster card is upgraded the next time you play that card the character gets the upgraded stats from the upgrades previously applied to the card.
So going back to the CardEffectAddTempCardUpgradeToUnits
param_bool2
controls if the card upgrade is applied to the spawner card.
param_bool
controls whether the card upgrade goes to a cards temporary or permanent upgrades.
Overview: This is a card that applies a scaled CardUpgrade to a unit. Scaling CardTraits.
For this tutorial's next card, we shall create the ominously-named-but-otherwise-innocent "Play Other Cards." For the low cost of 2 energy, you can give one of your monsters +2 attack per card played in this battle. It serves as an introduction to scaling effects.
As usual, download the art from here, then gaze upon the code below.
"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": [
{
"id": "@PlayOtherCardsScalingTrait"
}
],
"effects": [
{
"id": "@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": "PlayOtherCards",
"path": "textures/PlayOtherCards.png"
}
]
This sort of scheme is generally how the game handles variable effects. Let's go over the things that changed:
-
description
: To access fields from your traits you just say [trait0.name] here we want param_int so we say power. You know how some cards have sprites in their descriptions, like for, say, the attack symbol? This is how it's done. [attack] will be replaced by the associated sprite.
Effects List
- Nothing interesting here again we use
CardEffectAddTempCardUpgradeToUnits
for this
Upgrades List
-
bonus_damage
: This sets the base amount of damage to gain. We set it to 0 since we want 2 * (number of cards played this battle). The CardTrait does the 2*cards part for us.
Traits List
-
name
: We want to scale an upgrade and only its attack stat. So we useCardTraitScalingUpgradeUnitAttack
. Its important to only use this CardTrait with spell cards. -
track_type
: This is the statistic the trait will use to scale the upgrade. In this case, we use AnyCardPlayed, which you may be shocked to hear increments any time a card is played. -
entry_duration
: This determines the the length of the measurement of the statistic. You can specify This Turn, Previous Turn, or the Entire Battle. Here we want this battle so we use the value this_battle -
param_int
: The purpose of this field, as with effects, varies by trait type. In this case, it's how much attack the unit gains per the tracked statistic. The statistic gets the numgber of cards played this battle, and then it takes that number and multiplies by param_int here.
Add it to your starting deck, hop into a battle, play other cards, and notice how the card gradually ramps up in damage.
A last note, an observant reader might notice that this card did not have a fancy keyword added to it automatically. As mentioned in Anatomy of a Card, Not all CardTraits are keywords. CardTraitScalingUpgradeUnitAttack
is a CardTrait that doesn't add text to the card.
Overview: CardTriggers, Unplayable cards, CardEffect that affects the pyre.
The next card touches on cards with triggers. So far, all the cards have only had effects that play when you play the card in battle. You can add triggers to a card to have different effects play when the trigger's condition is met. Triggers on cards are represented with a CardTriggerEffect
. A CardTriggerEffect
has a Trigger type and a list of CardEffects to play when the Trigger is activated.
With that, let's make a card that has a Reserve trigger that gives your pyre +3 attack. If you recall, Reserve triggers at the end of the turn if the card is in your hand. The card is also unplayable, meaning you can't play this card like other cards. This card's name is called Pyre Sharpener.
"cards": [
{
"id": "PyreSharpener",
"cost": 0,
"cost_type": "unplayable",
"names": {
"english": "Pyre Sharpener",
},
"rarity": "common",
"targets_room": "false",
"targetless": false,
"class": "ClassBanished",
"card_art": "@PyreSharpenerArt",
"traits": [
{
"id": "@Unplayable"
}
],
"triggers": [
{
"id": "@BuffThePyreOnUnplayed"
}
],
"pools": [
"MegaPool"
],
}
],
"traits": [
{
"id": "Unplayable",
"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": [
{
"id": "@BuffThePyre"
}
]
}
],
"game_objects": [
{
"id": "PyreSharpenerArt",
"type": "card_art",
"extensions":
{
"card_art":
{
"sprite": "@PyreSharpener"
}
}
}
],
"sprites": [
{
"id": "PyreSharpener",
"path": "textures/PyreSharpener.png"
}
]
Some interesting things to go over.
-
cost_type
: Little known fact this setting actually doesn't make the card unplayable. It removes the ember icon from the card. -
descriptions
(absensce): You may notice the card has no description. This is intended as CardTriggers add themselves to the card text.
Traits List
-
traits0.name
: If you were wondering how to make a card actually non-playable, it's done through a CardTrait. Here we useCardTraitUnplayable
to make the card actually non-playable.
Effects List
-
effects0.name
:CardEffectBuffDamage
is a card effect that temporarily buffs damage, you should only use it with heroes or the pyre. Otherwise if the monster had endless and it dies the buffs applied via this effect don't carry over. Its okay to use this with the pyre because of course if it dies its game over! -
effects0.target_mode
: "pyre" is how you make a card target the pyre.
Card Triggers List
-
card_triggers0.trigger
: This is the in-game trigger type. Reserve corresponds to either on_unplayed_positive or on_unplayed_negative. -
card_triggers0.description
: Triggers have descriptions! Triggers will add themselves to the Card text, so there's no need to set description within the card this time. It's better to set the description within the trigger as you can properly use the substitution text[effect0.power]
here.
So add this card to your initial deck and give it a whirl. You may notice the card looks different than other cards. CardTraits can also change how a card looks, and CardTriggerEffects can as well!