Custom Status Effects - brandonandzeus/Trainworks2 GitHub Wiki

Custom Status Effects

Overview

Past this point, you are entering the wild woods. Here's where you will need knowledge of C#, your C# decompiler, and possibly Harmony. AssetStudio becomes much less useful now as that's for looking at the Game Data. Now, we will make our custom subclasses and use them in Mods. Do you remember all throughout this tutorial where we set EffectStateType, TraitStateType, RoomModifierClassType, and RelicEffectClassType? These values have all been classes within the Monster Train codebase, and you can look them up with your decompiler.

This tutorial will only serve as a tour guide, noting exciting portions of the codebase. We will not cover C# (there are plenty of tutorials for that) and it will not perform as the end-all or answer all of your questions. With research, you should be able to create any effects you want. Please don't hesitate to ask any questions in the Monster Train discord.

Weaken

As a starter status effect we will be creating is called Weaken. Weaken is a defense down. If the target inflicted with it gets hit by a unit, they will take Weaken stacks more damage. This includes the return damage from spikes as well. It is similar to Melee Weakness in that the units take more damage from attacks, but it removes itself when triggered. Weaken will not.

Steps to create a status effect

  1. Create a StatusEffectDataBuilder setting the proper fields that define how the status effect works, and be sure to Build it.
  2. Make a Subclass of StatusEffectState, and override one or more of StatusEffectState's virtual methods. Notable ones are TestTrigger, OnTriggered, and GetEffectMagnitude.

StatusEffectDataBuilder

Here we define the behavior and properties of the status effect. At the very least, we need to tell it:

  1. The internal statusId (must be unique).
  2. StatusEffectStateType is the subclass of StatusEffectState, which handles the actual effects the status effect does.
  3. A name for the status effect that will be displayed to the player.
  4. A description of the status effect for its tooltip.
  5. The DisplayCategory, whether the status is positive, negative, or persistent.
  6. TriggerStage and AdditionalTriggerStages (optional), which determines when our StatusEffectState subclass is called.
  7. An Icon for the status effect.
  8. Any Parameters that should get sent to the StatusEffectState subclass.
namespace MonsterTrainModdingTemplate.StatusEffects
{
    class Weaken
    {
        public static void BuildAndRegister()
        {
            // Register the status effect with StatusEffectManager
            new StatusEffectDataBuilder
            {
                // Status Effect Subclass to use to run the effect.
                StatusEffectStateType = typeof(StatusEffectWeakenState),
                StatusID = StatusEffectWeakenState.StatusId,
                Name = "Weaken",
                Description = "Unit takes additional damage equal to the number of weaken stacks when attacked",
                DisplayCategory = StatusEffectData.DisplayCategory.Negative,
                // Determines when to test to trigger the status effect.
                TriggerStage = StatusEffectData.TriggerStage.OnPreAttacked,
                RemoveAtEndOfTurn = false,
                // This should be a black and white image sized 24x24.
                IconPath = "assets/status_weakness.png",
                ParamInt = 1,
            }.Build();
        }
    }
}

Time for an explanation of what we have so far.

  • StatusEffectStateType: We will get to the custom subclass of StatusEffectType in the next section.
  • StatusID: This should match the StatusId field in the custom subclass, so we use it there.
  • Name / Description: Again, if these are set, it will set the localization across all languages.
  • DisplayCategory: We set it to negative as it is a negative status effect. The red background comes from this property.
  • TriggerStage: We set this to pre-attacked since we want to modify the damage. This is the same as Melee Weakness which I took inspiration from.
  • RemoveAtEndOfTurn: There are several removal options (I suggest looking at StatusEffectData) that all default to false, I just wanted to point this out.
  • IconPath: As the comment states, this should be a black-and-white image. The background is handled by DisplayCategory
  • ParamInt: This parameter is passed to StatusEffectStateType we will use it to set the Effect's Magnitude.

If you ever need help determining what values to set for these various parameters, I suggest taking a look at an existing status and copy the fields it uses. In this case I looked at Melee Weakness when coming up with this status effect.

StatusEffectWeakenState

Here's the code. The comments are inline for the most important stuff.

    public class StatusEffectWeakenState : StatusEffectState
    {
        // Each status effect subclass has this field defined don't forget it.
        public const string StatusId = "weaken";

        // This is called Based on TriggerStages and AdditionalTriggerStages in the corresponding StatusEffectData
        // If True is returned then Trigger will be called.
        // This is only called when a unit has this status effect.
        // In this case the true/false value doesn't matter as the effect handling is done at the very end of TestAttackTrigger.
        public override bool TestTrigger(InputTriggerParams inputTriggerParams, OutputTriggerParams outputTriggerParams)
        {
            // If the unit attacked is alive then do some additional testing.
            if (inputTriggerParams.attacked != null && inputTriggerParams.attacked.IsAlive)
            {
                return TestAttackTrigger(inputTriggerParams, outputTriggerParams);
            }
            else
            {
                return true;
            }
        }

        // 
        private bool TestAttackTrigger(InputTriggerParams inputTriggerParams, OutputTriggerParams outputTriggerParams)
        {
            CharacterState characterState = null;
            // This should always be true given when this is called, but just in case.
            if (inputTriggerParams.attacked != null && inputTriggerParams.attacked.IsAlive)
            {
                characterState = inputTriggerParams.attacked;
            }
            if (characterState == null)
            {
                return false;
            }
            // This catches damage from a Card and not to unit.
            if (inputTriggerParams.attacker == null)
            {
                return false;
            }
            // If the unit has Phased they can't be targeted anyway.
            if (characterState.HasStatusEffect(VanillaStatusEffectIDs.Phased))
            {
                return false;
            }
            // Handler for damage shield since it totally blocks damage.
            if (characterState.GetStatusEffectStacks(VanillaStatusEffectIDs.DamageShield) > 0)
            {
                return true;
            }
            // The effect handling code is here. We get the amount of status the character has.
            // And then add EffectMagnitude stacks to it.
            int statusEffectStacks = characterState.GetStatusEffectStacks(StatusId);
            int damageAdded = GetEffectMagnitude(statusEffectStacks);
            // We adjust the damage here outputTriggerParams is the new amount of damage.
            // and count is the new amount of statusEffectStacks the unit will have.
            outputTriggerParams.damage = inputTriggerParams.damage + damageAdded;
            outputTriggerParams.count = statusEffectStacks;

            return outputTriggerParams.damage != inputTriggerParams.damage;
        }

        public override int GetEffectMagnitude(int stacks = 1)
        {
            // GetParamInt will get the ParamInt from StatusEffectData from the Builder.
            return GetParamInt() * stacks;
        }
    }

Last but not least, let's make this a starter card for our new clan. You know the drill!

This card, called Rustify, applies 10 weaken to the front enemy unit.

    class Rustify
    {
        public static readonly string ID = TestPlugin.GUID + "_Rustify";

        public static void BuildAndRegister()
        {
            new CardDataBuilder
            {
                CardID = ID,
                Name = "Rustify",
                Description = "Apply <nobr><b>Weaken</b> [effect0.status0.power]</nobr> to the front enemy unit.",
                Cost = 1,
                Rarity = CollectableRarity.Starter,
                TargetsRoom = true,
                Targetless = false,
                ClanID = Clan.ID,
                AssetPath = "assets/rustyshield.png",
                CardPoolIDs = { VanillaCardPoolIDs.MegaPool },
                EffectBuilders =
                {
                    new CardEffectDataBuilder
                    {
                        EffectStateType = VanillaCardEffectTypes.CardEffectAddStatusEffect,
                        TargetMode = TargetMode.FrontInRoom,
                        TargetTeamType = Team.Type.Heroes,
                        ParamStatusEffects =
                        {
                            new StatusEffectStackData
                            {
                                statusId = StatusEffectWeakenState.StatusId,
                                count = 10
                            }
                        }
                    }
                }
            }.BuildAndRegister();
        }
    }

The only thing of note is the description. All of the vanilla status effects have a replacement tag. Since this is custom, it does not. Just note that [rage] expands to Rage, so it's no big deal.

Lastly, change the ChampionCardDataBuilder to use Rustify as its starter.

            new ChampionCardDataBuilder()
            {
                Champion = championCharacterBuilder,
                ChampionIconPath = "assets/slimeboy-character.png",
                StarterCardID = Rustify.ID,
                CardID = ID,
                Name = "Slimeboy",
                ClanID = Clan.ID,
                UpgradeTree = FormUpgradeTree(),
                AssetPath = "assets/slimeboy.png"
            }.BuildAndRegister(0);

And there you have it, your first custom status effect! Again this doesn't cover all there is to status effects. I suggest you look up the vanilla status effects to see how they work, most of the code for status effects is self-contained, but as you can see from other statuses like Phased and Damage Shield, the code could be present in more places in the code base.

Next: Custom Card Traits

⚠️ **GitHub.com Fallback** ⚠️