Custom Monster Cards - brandonandzeus/Trainworks2 GitHub Wiki
For many of you, this is probably the exciting part. In fact, it may be so exciting that you skipped right over the previous tutorial about spell cards. Don't do that. Monster cards are actually just spell cards that summon monsters, and I'm not going to repeat things that were already covered there, so go back and read it over.
Once you've done that, you can move on to the next section, where we'll discuss our first monster card, a true classic.
Download the card art here, then download the monster art here. They're both static images. Animated characters are much more complicated, so they're covered in their own dedicated tutorial.
The first thing we're going to do is create a new subtype. It's very easy. For later convenience, I'm going to put it in its own class.
class SubtypeDragon
{
public static readonly string Key = TestPlugin.GUID + "_Subtype_Dragon";
public static void BuildAndRegister()
{
CustomCharacterManager.RegisterSubtype(Key, "Dragon");
}
}
One line of actual code. Nice and easy. Now let's make the card to go with it!
new CardDataBuilder
{
CardID = TestPlugin.GUID + "_BlueEyesCard",
Name = "Blue-Eyes White Dragon",
Cost = 3,
CardType = CardType.Monster,
Rarity = CollectableRarity.Rare,
TargetsRoom = true,
Targetless = false,
AssetPath = "assets/blueeyes.png",
ClanID = VanillaClanIDs.Stygian,
CardPoolIDs = { VanillaCardPoolIDs.StygianBanner, VanillaCardPoolIDs.UnitsAllBanner },
EffectBuilders =
{
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectSpawnMonster),
TargetMode = TargetMode.DropTargetCharacter,
ParamCharacterDataBuilder = new CharacterDataBuilder
{
CharacterID = TestPlugin.GUID + "_BlueEyesCharacter",
Name = "Blue-Eyes White Dragon",
Size = 5,
Health = 2500,
AttackDamage = 3000,
AssetPath = "assets/blueeyes_character.png",
SubtypeKeys = { SubtypeDragon.Key }
}
}
}
}.BuildAndRegister();
Much of what's here is either the same as it was for spell cards or fairly self-explanatory. Let's go over the parts that changed.
-
CardType
: If you want this to be a monster card, you have to set this toCardType.Monster
. Don't forget! -
CardPoolIDs
: Unlike spell cards which are usually just stuffed into the MegaPool, monsters go into banner pools. Since this is a Stygian card (for now), we'll put it in the Stygian pool. We'll also put it in the UnitsAllBanner pool, which holds all banner units in the game. -
EffectStateType
: Notice how this is CardEffectSpawnMonster. All monster cards will use this CardEffect. -
TargetMode
: Again, all monster cards will use DropTargetCharacter. -
ParamCharacterDataBuilder
: This is the actual character data that the monster card will summon. All unit data monsters and heroes are instances ofCharacterData
. -
CharacterID
: As with CardID, this must be unique. -
Name
: As with the card, setting the Name field will set it for all languages. If you want your mod to support localization, you'll want to use NameKey, which is covered in the custom localization tutorial. -
Size
: The capacity you need in the room you want to summon the monster. -
Health
: Its health stat. -
AttackDamage
: Its attack stat. -
AssetPath
: As with cards, this should be the path relative to your plugin dll. This will be the monster's art (which is distinct from the card's art, which we used up above). Setting it this way will only use a static image; for animated character art, see the dedicated tutorial. -
SubtypeKeys
: A list of keys corresponding to subtypes. This is where we use the Dragon subtype key we created higher up. If you want to use a vanilla subtype, All of the subtypes are documented inTrainworks.Constants.VanillaSubtypeIDs
, or you can find them in AssetStudio.
Add the card to your starting deck and hop in-game to ensure that it works as expected. Congratulations! You've made your first monster card. It's agonizingly boring, so let's make another one with a bit more spice to it.
Ever-efficient, we're going to reuse the subtype we made in the previous step by creating another Dragon card. Download the card art here and the character art here.
This character is a dragon costume. When it takes a hit, it puts on a new costume (graphical effect not included), gaining a damage shield. If you remember from the last tutorial, Cards can have triggers; now we show that Characters can have triggers too!
var dragonCostumeCharacter = new CharacterDataBuilder
{
CharacterID = TestPlugin.GUID + "_DragonCostumeCharacter",
Name = "Dragon Costume",
Size = 5,
Health = 50,
AttackDamage = 5,
AssetPath = "assets/dragoncostume_character.png",
SubtypeKeys = { SubtypeDragon.Key },
TriggerBuilders =
{
new CharacterTriggerDataBuilder
{
TriggerID = TestPlugin.GUID + "_DragonCostumeCharacter",
Trigger = CharacterTriggerData.Trigger.OnHit,
Description = "Gain <nobr>[damageshield] [effect0.status0.power]</nobr>",
EffectBuilders =
{
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectAddStatusEffect),
TargetMode = TargetMode.Self,
TargetTeamType = Team.Type.Monsters,
ParamStatusEffects =
{
new StatusEffectStackData
{
statusId = VanillaStatusEffectIDs.DamageShield
count = 1,
}
}
}
}
}
}
};
-
TriggerID
: Remember to set the unique TriggerID identifier. -
Trigger
: The character trigger to use. Remember: triggers will generate their own text. This example is a Revenge card, but we did not put "Revenge:" in the description; the game will do it for us. -
Description
: Trigger builders also have their own description field. This is appended after the trigger name. Altogether this will create the text "Revenge: Gain Damage Shield 1". -
EffectBuilders
: The CardEffects that fire when the trigger is triggered. We've already seen how this works; it's the same here as it was everywhere else.
It may surprise you to hear that with monster cards, the monster data is the complicated part. The card builder is simple and will look pretty much the same for all monsters within a clan. For completeness' sake, here it is:
new CardDataBuilder
{
CardID = TestPlugin.GUID + "_DragonCostumeCard",
Name = "Dragon Costume",
Cost = 2,
CardType = CardType.Monster,
Rarity = CollectableRarity.Common,
TargetsRoom = true,
Targetless = false,
AssetPath = "assets/dragoncostume.png",
ClanID = VanillaClanIDs.Stygian,
CardPoolIDs = { VanillaCardPoolIDs.StygianBanner, VanillaCardPoolIDs.UnitsAllBanner },
EffectBuilders =
{
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectSpawnMonster),
TargetMode = TargetMode.DropTargetCharacter,
ParamCharacterDataBuilder = dragonCostumeCharacter
}
}
}.BuildAndRegister();
This time we'll be adding a card of an existing type: a new morsel. Grab the card art here and the character art here.
Apple Morsel is a morsel in the same tier as Rubble Morsel. It's not very good, and it'll show up a lot. Actually, playing with it will grossly skew your morsel pool toward bad cards, so don't try any serious runs. Its effect: It's so disgusting (because it's not a rock) that the eater takes 5 damage and gains Rage 3.
As with the previous example, we'll start with the character data.
var appleMorselCharacter = new CharacterDataBuilder
{
CharacterID = TestPlugin.GUID + "_AppleMorselCharacter",
Name = "Apple Morsel",
Size = 1,
Health = 1,
AttackDamage = 0,
AssetPath = "assets/apple_morsel_character.png",
SubtypeKeys = { VanillaSubtypeIDs.Morsel },
PriorityDraw = false,
TriggerBuilders =
{
new CharacterTriggerDataBuilder
{
TriggerID = TestPlugin.GUID + "_AppleMorselEaten",
Trigger = CharacterTriggerData.Trigger.OnEaten,
Description = "Eater takes [effect1.power] damage and gains <nobr>[rage] [effect0.status0.power]</nobr>",
EffectBuilders =
{
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectAddStatusEffect),
TargetMode = TargetMode.LastFeederCharacter,
TargetTeamType = Team.Type.Monsters,
ParamStatusEffects =
{
new StatusEffectStackData
{
statusId = VanillaStatusEffectIDs.Rage,
count = 3
}
}
},
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectDamage),
TargetMode = TargetMode.LastFeederCharacter,
TargetTeamType = Team.Type.Monsters,
ParamInt = 5
}
}
}
}
};
-
SubtypeKeys
: The framework provides constants for all of the Subtypes in the base game. -
PriorityDraw
: This defaults to true, which is what you'd want it to be for a banner unit since that's how the game knows to put it in one of your first few hands. Morsels aren't a priority draw, so we set it to false here. -
Trigger
: For morsels, it's OnEaten. This is required to make the Morsel edible. Note that just giving a unit the Morsel subtype isn't enough to make it edible. -
TargetMode
: Here, we target the character that ate the Morsel. -
Description
: We're using more than one effect in the description this time.effect0
is the effect at index 0, andeffect1
is the effect at index 1 in the effects list. -
EffectBuilders
: The first effect here adds Rage 3 to the eater, and the second deals 5 damage.
Despite this character being completely different from the last two, there's not a whole lot to say. The differences are few. You can see here how even simple building blocks can create varied designs. Let's move on to the CardDataBuilder:
new CardDataBuilder
{
CardID = TestPlugin.GUID + "_AppleMorselCard",
Name = "Apple Morsel",
Cost = 0,
CardType = CardType.Monster,
Rarity = CollectableRarity.Common,
TargetsRoom = true,
Targetless = false,
AssetPath = "assets/applemorsel.png",
ClanID = VanillaClanIDs.Umbra,
CardPoolIDs =
{
VanillaCardPoolIDs.MorselPool, VanillaCardPoolIDs.MorselPool,
VanillaCardPoolIDs.MorselPool, VanillaCardPoolIDs.MorselPool,
VanillaCardPoolIDs.MorselPool, VanillaCardPoolIDs.MorselPool,
VanillaCardPoolIDs.MorselPool, VanillaCardPoolIDs.MorselPool,
VanillaCardPoolIDs.MorselPool, VanillaCardPoolIDs.MorselPool,
VanillaCardPoolIDs.MorselPoolStarter, VanillaCardPoolIDs.MorselPoolStarter,
VanillaCardPoolIDs.MorselPoolStarter, VanillaCardPoolIDs.MorselPoolStarter
},
EffectBuilders =
{
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectSpawnMonster),
TargetMode = TargetMode.DropTargetCharacter,
ParamCharacterDataBuilder = appleMorselCharacter
}
}
}.BuildAndRegister();
The big difference you'll notice here is in CardPoolIDs
. Why, there are tons of them! This is how the game makes certain morsels more common than others. Every card in the card pool has the same chance of being rolled. How did the devs get around this? By adding the same card multiple times! In the main morsel pool, Rubble Morsel shows up ten times. In the starter card morsel pool, it shows up four times. Apple Morsel copies the ratio. Notably, this makes other morsel cards much less likely to show up.
For the last monster card, we will touch on a unique aspect that Monsters have but cards don't have, Room Modifiers. A RoomModifier is basically a passive effect applied to the room when the Monster is in the room, but it can do so much more. Some great examples of Monsters with RoomModifiers are Deranged Brute (rage damage modification), Mollusc Mage (magic power modification), and Tethy's Conduit upgrade path (spell cost modification).
So let's give an example of this concept; this time, we will make a character that has a RoomModifier that buffs any unit with 10 additional damage while the said unit has regen. Since we need a way to give Regen to a unit, let's add a trigger on Harvest to give all friendly units on floor 2 regen.
Here's the character.
var character = new CharacterDataBuilder
{
CharacterID = TestPlugin.GUID + "_FrostFuryCharacter",
Name = "Frost Fury",
Size = 2,
AttackDamage = 20,
Health = 20,
AssetPath = "assets/FrostFury_Character.png",
PriorityDraw = true,
TriggerBuilders =
{
new CharacterTriggerDataBuilder
{
TriggerID = TestPlugin.GUID + "_FrostFuryHarvest",
Trigger = CharacterTriggerData.Trigger.OnAnyUnitDeathOnFloor,
Description = "Gain <nobr>[regen] [effect0.status0.power]</nobr>",
EffectBuilders =
{
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectAddStatusEffect),
TargetMode = TargetMode.Room,
TargetTeamType = Team.Type.Monsters,
ParamStatusEffects =
{
new StatusEffectStackData {statusId = VanillaStatusEffectIDs.Regen, count = 2},
}
}
}
}
},
RoomModifierBuilders =
{
new RoomModifierDataBuilder
{
RoomModifierID = TestPlugin.GUID + "_FrostFuryRoomModifier",
// Note tooltipTitle in game is tied to the RoomStateModifier class.
RoomModifierClassType = typeof(RoomStateStatusEffectDamageModifier),
// Note the <br> as its used as part of the Card's Description to make it look nicer as
// the trigger text appears on the same line otherwise.
Description = "<br><br>Units with [regen] gain and deal [paramint] additional damage.",
DescriptionInPlay = "Units with [regen] gain and deal [paramint] additional damage.",
ParamInt = 10,
ParamStatusEffects =
{
new StatusEffectStackData {statusId = VanillaStatusEffectIDs.Regen, count = 0},
}
}
}
};
I'll only note the interesting things here; by now, this should be routine :)
-
Trigger
: Harvest internally isCharacterTriggerData.Trigger.AnyUnitDeathOnFloor
-
RoomModifierDataBuilder
: RoomModifiers are represented with aRoomModifierData
, which is part ofCharacterData
-
RoomModifierStateType
: This should be a class type that inherits fromRoomStateModifierBase
here. We use the classRoomStateStatusEffectDamageModifier
, which you can look up the implementation in your C# decompiler. -
RoomModifierID
: Much like with other things, a unique ID is required. Again this generates unique localization keys. -
Description
: RoomModifiers add their description text to the card. If you also have a trigger, the text isn't very nice looking, so include two
s to separate the texts. -
DescriptionInPlay
: This is a separate description when the room modifier is active. It is used in the tooltip for the RoomModifier icon. -
ParamInt
: This is used inRoomStateStatusEffectDamageModifier
to modify the damage amount. The class name is a misnomer; it just gives units with the status effect additional Attack damage. -
ParamStatusEffects
: Only statusID is used here to identify which status effect the room modifier targets.
Here's the card.
new CardDataBuilder
{
CardID = TestPlugin.GUID + "_FrostFury",
Name = "Frost Fury",
Cost = 2,
CardType = CardType.Monster,
Rarity = CollectableRarity.Common,
TargetsRoom = true,
Targetless = false,
AssetPath = "assets/FrostFury.png",
ClanID = Clan.ID,
CardPoolIDs = { VanillaCardPoolIDs.StygianBanner, VanillaCardPoolIDs.UnitsAllBanner },
EffectBuilders =
{
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectSpawnMonster),
TargetMode = TargetMode.DropTargetCharacter,
ParamCharacterDataBuilder = character
}
}
}.BuildAndRegister();
Same as always. Add a few of these to your deck and test the effect.
As part of the TLD upgrade for Monster Train, Monsters can have Essences. These are simply a CardUpgradeData
that gets applied to another unit when the unit is sacrificed at a Divine Temple. Recall from the tutorial on Custom Spell Cards, Icy Boost Specifically.
All that's required is to define a CardUpgradeData
with the upgrades to apply and then to set the UnitSynthesisBuilder
property in your CharacterDataBuilder object. Ensure that the LinkedPactDuplicateRarity
property is set to CollectableRarity.Rare
to ensure the duplication costs at a Hellvent are consistent.
So, to continue with Frost Fury, let's give it an essence, say Increase damage by 10 and give it a Harvest: Gain Regen 1 effect.
var synthesis = new CardUpgradeDataBuilder
{
UpgradeID = TestPlugin.GUID + "_FrostFuryEssence",
UpgradeDescription = "+10[attack] and 'Harvest: Gain [regen] 1'"
BonusDamage = 10,
LinkedPactDuplicateRarity = CollectableRarity.Rare,
TriggerUpgradeBuilders =
{
new CharacterTriggerDataBuilder
{
TriggerID = TestPlugin.GUID + "_FrostFuryEssenceHarvest",
Trigger = CharacterTriggerData.Trigger.OnAnyUnitDeathOnFloor,
Description = "Gain <nobr>[regen] [effect0.status0.power]</nobr>",
EffectBuilders =
{
new CardEffectDataBuilder
{
EffectStateType = typeof(CardEffectAddStatusEffect),
TargetMode = TargetMode.Self,
TargetTeamType = Team.Type.Monsters,
ParamStatusEffects =
{
new StatusEffectStackData {statusId = VanillaStatusEffectIDs.Regen, count = 1},
}
}
}
}
}
}
Quick summary for any new stuff.
-
UpgradeDescription
: CardUpgrades can have names and descriptions. This is usually used for ChampionUpgrades but is also required for Unit Essences. Remember that setting this field sets the localization for all languages. Also, please note that many substitution texts (the [effect0.status0.power] aren't used in essences, and the values are hardcoded). Only in effect itself do you not hardcode the numbers. -
LinkedPactDuplicateRarity
: This field controls the cost in Pact shards if you duplicate the card with the upgrade. Just remember to set it to CollectableRarity.Rare to keep the cost at 15 like other essences.
Congrats on reading until the end. This and the last tutorial should cover the most basic cards you can create. You should now know how to do the following.
- How to make a new Monster subtype.
- How to make a monster card with a static image for the character.
- How to make a monster with a trigger.
- How to make a morsel unit and modify the morsel pools.
- How to make a monster with a RoomModifier.
- How to add an Essence to a monster.
Again with the MonsterTrainGameData repo and some research, there's no limit to what you can make here. Some additional things that Monsters can do that weren't covered here.
- Enchanters. Units that apply a status effect to other units on the floor as soon as they are spawned or moved to the floor. Units such as Wyldenten or ConduitInfiltrator are examples.
- Heroes. Heroes can also be created with CharacterDataBuilder. Making new hero types and bosses is beyond the scope of this tutorial.
Next: Custom Relics