Skip to content

Modifying an Existing Card

brandonandzeus edited this page May 19, 2024 · 9 revisions

These wiki pages are deprecated and are outdated. Please find the newer tutorials here

Modifying an Existing Card

Surprisingly, modifying existing content is actually more difficult than adding new content, but understanding how it works will make it easier to add your own cards later on.

First, a quick primer: Monster Train has CardData and CardState. Think of Data as the template for the card and State as the instance. There can be many States (you can have five Frozen Lances in your deck), but each card will only ever have one Data. We want to modify the Data for Frozen Lance so that we can apply only one change, and all Frozen Lance cards that are ever created will parrot it.

Create a new method named ModifyFrozenLance(), and call it from your plugin's Initialize() method.

private void ModifyFrozenLance()
{
}

Getting the CardData

The first thing we need to do is find the FrozenLance CardData. Add a new using statement for TrainworksModdingTools.Managers. Instead of using Harmony to fetch it, we'll make the framework do it.

First we need to get Frozen Lance's ID. You could find this in game or via AssetStudio, but the framework defines a number of constants for us, so we'll use those instead. Add another using statement for TrainworksModdingTools.Constants.

To get the ID: string frozenLanceID = VanillaCardIDs.FrozenLance;

Then to get the card data: var frozenLanceData = CustomCardManager.GetCardDataByID(frozenLanceID);

Now that we have Frozen Lance's CardData, let's make some adjustments.

By far the most annoying thing about modifying existing cards is how just about every field is private. Ordinarily this means you'd need to use Reflection to access them, but Harmony provides two much more convenient ways: AccessTools and Traverse. AccessTools is (slightly) faster and Traverse is easier, so use Traverse when speed isn't a concern. In this example, this code is run exactly once on startup, so we don't mind the (very) minor performance hit.

With that out of the way, let's talk goals. We want to do three things:

  • Add Piercing to Frozen Lance
  • Change its damage to 12
  • Make it apply 327 Frostbite on hit

Let's get started.

Adding Piercing

Piercing is what's known as a CardTrait. Note that traits are distinct from effects, which we'll get to later. Piercing in particular's a very easy one to add since it has no parameters. All we need to do is create a new trait, set its trait type, and add it to Frozen Lance's traits list.

var piercingTrait = new CardTraitData();
string piercingTraitName = VanillaCardTraitTypes.CardTraitIgnoreArmor.AssemblyQualifiedName;
piercingTrait.Setup(piercingTraitName);
frozenLanceData.GetTraits().Add(piercingTrait);

Piercing is nice because it doesn't require reflection to make. Other traits can be a bit tougher, but once we cover the basics of using Harmony to set private fields, they will be plenty doable.

Modifying damage

Modifying damage is a bit more involved. First, we need to reach into Frozen Lance and get its damage effect. This requires us to know how Frozen Lance is set up. To help with this, we're going to look up Frozen Lance's data in AssetStudio.

Sometimes the game assets don't match the card's actual name. We need to know Frozen Lance's asset name. Go back to where we got frozenLanceID and hover over VanillaCardIDs.FrozenLance. It'll tell us that the asset name is, in fact, StarterIceSpear. Remember this that you can either mouse over the constant to find the Asset Name. Now, let's search for StarterIceSpear in AssetStudio.

Click it. Make sure to select the Managed folder at the prompt, and you should see Frozen Lance's card data on the right side. We're particularly interested in the effects. We see that effects[0] is the damage effect, and the amount of damage dealt is determined by the paramInt field. Now we can write the rest.

First get the effect:

var frozenLanceDamageEffect = frozenLanceData.GetEffects()[0];

Unfortunately, paramInt is a private field, like just about everything in Monster Train. It has a getter but no setter, so to change it we need to use some sort of reflection. As discussed before, we'll be using Harmony's Traverse.

Traverse.Create(frozenLanceDamageEffect).Field("paramInt").SetValue(12);

Now Frozen Lance deals 12 damage. You'll note that the card text has already been updated to match, as it did with Piercing.

Adding Frostbite

Adding status effects to existing cards is hard, so this is going to get quite involved. Buckle in.

First we need to create the effect that'll add the Frostbite. You can find what this effect is by searching up another Frostbite card in AssetStudio, but I'll spare you the trouble and tell you that it's a CardEffectAddStatusEffect effect. Effects that cards have are expressed with a CardEffectData object that specifies the effect and its parameters.

To save some trouble with using reflection to create the data we want, we have various Builder classes for each GameData object present in the game. CardEffectData can be created with a CardEffectDataBuilder. These Builder classes are designed to create the GameData object easily and ensure that all required fields are set to prevent errors.

Note that the above example with adding Piercing to Frozen Lance could have also been done with a Builder, specifically CardTraitDataBuilder.

CardEffectData frostbiteEffect = new CardEffectDataBuilder
{
    EffectStateName = "CardEffectAddStatusEffect",
    TargetMode = TargetMode.LastTargetedCharacters,
    TargetTeamType = Team.Type.Heroes,
    ParamStatusEffects = new StatusEffectStackData[]
    {
        new StatusEffectStackData {statusId = VanillaStatusEffectIDs.Frostbite, count = 327 },
    }
}.Build();

This is a lot to take in, so an explanation of what's going on.

  1. EffectStateName specifies the CardEffect class that gets instantiated. In this case, the effect we want is CardEffectAddStatusEffect.
  2. TargetMode specifies what the CardEffect is going to target. Again, you can find this in AssetStudio. We want to target the last targeted character.
  3. TargetTeamType specifies which team to target. Heroes, Monsters, or Both. We want to target Heroes. This, combined with TargetMode specify the last targeted hero. Note that Heroes here means the Enemies and Monsters mean our units.
  4. ParamStatusEffects specifies status effect parameters to the card effect. In this case, CardEffectAddStatusEffect determines what status effects to add through ParamStatusEffects. ParamStatusEffects wants an array of StatusEffectStackData, so we create an array with one item specifying Frostbite and 327 stacks of it.

Last but not least, we need to add this entire effect to the Frozen Lance CardData's effects list:

frozenLanceData.GetEffects().Add(frostbiteEffect);

If you boot up the game, you'll notice that Frozen Lance pierces, deals 12 damage, and inflicts 327 frostbite. But, strangely enough, the frostbite isn't in the description. Descriptions are explained more in the custom localization tutorial, but the short version:

  • Traits add themselves to card text.
  • Effects do not; they are displayed as part of the card's description, which is stored in the game assets.
  • To modify the card's description, we would have to either write a Harmony patch or replace it entirely. In the custom localization tutorial, we will replace it entirely.

Next: Custom Spell Cards

Clone this wiki locally