Skip to content
Brandon edited this page Jun 14, 2026 · 1 revision

This tutorial will be a bit more involved than the others. And require a few more steps and code to get working.

Overview

Steps to create a CharacterTrigger

  1. Define a character_trigger_type enum from within JSON.

  2. Fetch the enum's value from the CharacterTriggerRegister, and store it.

  3. Fire the trigger at the appropriate time.

Additionally an icon sized 50x50 must be provided.

In this tutorial we will be creating a trigger called Furnish. It triggers when a Room or Equipment card is played on the floor.

A good place to find icons is flaticon.com. An image you can use (but should resize to 50x50) is this one chandelier Save it as textures/trigger_furnish.png

Defining A CharacterTrigger

Here is the json to define a character trigger:

"character_trigger_types": [ 
  {
    "id": "Furnish",
    "names": {
      "english": "Furnish"
    },
    "descriptions": {
      "english": "Triggers when a Room or Equipment is played on this floor.",
    },
    "sprite": "@trigger_furnish"
  }
],
"sprites": [
  {
    "id": "trigger_furnish",
    "path": "textures/trigger_furnish.png"
  }
],
"atlas_icons": [
  {
    "id": "trigger_furnish",
    "path": "textures/trigger_furnish.png"
  }
]

A new section character_trigger_types for defining CharacterTriggerData.Trigger enum values. For a full reference for all of the fields character_trigger_types.json which documents all of the fields.

We use atlas_icons with the same image as this registers the image to be available to be used in tooltips. Unlike with status effects we can use the same image as it displays with full color.

When your mod is loaded, Trainworks Reloaded will allocate a value for our new CharacterTriggerData.Trigger enum. It is important to go through Trainworks Reloaded because if folks randomly picked integers for enums there is a possibility of a collision, or even worse, if the game updates and the devs made new enum values that they could collide with ones produced by mods.

Trainworks enum allocation prevents both of these issues, it computes the maximum value of the enum at runtime and assigns enums such that there is no chance of a collision. That said do not depend on the value of this enum staying the same value (i.e do not print the enum and copy the value). The value depends on what mods users have installed and will vary. In the next section we will get the enum which we will use in code.

Fetching the CharacterTrigger

It is not enough to simply define an Trigger enum. On its own it does not do anything and the trigger will never fire if you give the trigger to a character. The very first thing though is that we need the value of the enum first so that we can fire the trigger.

Trainworks Register Objects

Each object we have been creating throughout the tutorial. Trainworks stores them all in Register objects there is CardDataRegister, CharacterDataRegister, etc. It is easier to refer to them via the interface they implement so IRegister<CardData> and IRegister<CharacterData>.

So after we provide our json files to Trainworks, Trainworks will load and process them. We then can query for IRegister<CharacterTriggerData.Trigger> and we can get our Trigger enum value from it.

Railhead and Railend

An aside, you may have noticed this boilerplate that handles loading our json files

            var builder = Railhead.GetBuilder();
            builder.Configure(
                MyPluginInfo.PLUGIN_GUID,
                c =>
                {
                    c.AddMergedJsonFile(
                        "json/global.json",
                        "json/plugin.json"
                    );
                }
            );

This part handles the construction of our mod. Likewise there is the Railend class which handles mods after they have been loaded.

Railend provides two useful static functions.

  • Railend.ConfigurePreAction(Action<Container> action) - Can be called to add additional processing steps prior to the framework reading and processing all JSON files.

  • Railend.ConfigurePostAction(Action<Container> action) - Can be called to add additional processing steps after JSON files are processed.

Writing a PostAction

With that we want to write a post action.

  1. We will get the IRegister<CharacterTriggerData.Trigger> which should contain all registered custom Triggers.

  2. We query the register for our trigger, which requires a fully qualified id.

Here's the code to fetch our Furnish trigger.

            Railend.ConfigurePostAction(
                c =>
                {
                    var manager = c.GetInstance<IRegister<CharacterTriggerData.Trigger>>();
                    string id = MyPluginInfo.PLUGIN_GUID.GetId(TemplateConstants.CharacterTriggerEnum, "Furnish");
                    var trigger = manager.GetValueOrDefault(id);
                }
            );

There is a few things going on. As you saw from the signature the method takes in an Action<Container>. From C# a method that takes in a Container object and does something with it. So we define a lambda for it.

  1. With a Container we can get objects that were previously configured within it with a call to GetInstance We ask for the IRegister<CharacterTriggerData.Trigger> instance which gets our register.

  2. We get the fully qualified id. Trainworks internally when we create objects forms an id based off that, using our Plugin's GUID along with the data type of the object to form a unique string so that if different modders use the same id for something they don't collide.

  3. We call GetId on our Plugin's GUID string.

  4. We pass the data type identifier TemplateConstants.CharacterTriggerEnum

  5. We also pass the trigger id we want. Furnish.

  6. IRegister objects are also just C# Dictionaries so any method accessing a value from a Dictionary can be used here.

Configuring / Firing the trigger

At this point you have the trigger, actually causing the trigger to fire is the hardest part. Normally, you'd to write a Harmony Patch in the appropriate place in the game's code, and fire the trigger that way via a call to CombatManager.QueueTrigger and doing it that way is beyond the scope of this tutorial.

Conductor's Trigger Interfaces

Fortunately, Conductor has functionality to configure when a trigger should fire.

Currently the supported events are as follows:

  • When a status effect is added to a character

  • When a status effect is removed from a character

  • When a card is played

  • When a card is discarded

  • When a character is spawned

  • When a character is damaged

  • When the pyre is damaged.

Since Furnish triggers when a Room or Equipment card is played on the floor we will show how to use Conductor's trigger interface to wire the trigger to fire when a Room or Equipment card is played.

Writing an Trigger Event Handler

The function we are interested in is

public static CharacterTriggerData.Trigger SetToTriggerOnCardPlayed(this CharacterTriggerData.Trigger trigger, TriggerOnCardPlayedDelegate func)

This is an extension function, it requires a TriggerOnCardPlayedDelegate which is a method with this signature.

public delegate bool TriggerOnCardPlayedDelegate(TriggerOnCardPlayedParams data, out QueueTriggerParams? outParam);

So we need to make a function that returns bool with this signature that handles the event. The return for this function is if the trigger should fire. The out parameter controls parameters forwarded to CombatManager.QueueTrigger which to reiterate is the function call to fire a trigger.

With that here is our handler which we define as a static function in our Plugin class:

        static bool OnPlayedRoomOrEquipment(TriggerOnCardPlayedParams data, out QueueTriggerParams? triggerQueueData)
        {
            var cardType = data.Card.GetCardType();
            if (cardType == CardType.TrainRoomAttachment || cardType == CardType.Equipment)
            {
                triggerQueueData = new QueueTriggerParams
                {
                    fireTriggersData = new FireTriggersData
                    {
                        paramInt = cardType == CardType.Equipment ? 0 : 1,
                    }
                };
                return true;
            }
            triggerQueueData = null;
            return false;
        }

Each event handler function will take in a Params object which has various fields we can use to determine whether or not to allow the trigger to fire.

  1. We get the card from the event parameters, and then get its CardType

  2. if the card is a Room (CardType.TrainRoomAttachment) or Equipment (CardType.Equipment)

  3. We create a QueueTriggerParams for the out Parameter and return true.

  4. Otherwise set the output parameter to null and return false.

QueueTriggerParams

As mentioned the fields set here are directly passed to the CombatManager.QueueTrigger call which is called by Conductor to fire our trigger. A brief overview of the parameters for this.

  • dyingCharacter - This gets forwarded to CardEffectParams.dyingCharacter which we can then use in any Card Effects. (Defaults to null)

  • canAttackOrHeal - This controls whether the unit can attack or heal. (Defaults to true)

  • canFireTriggers - If this is set to false then the trigger won't fire. (Defaults to true)

  • fireTriggersData - This sets some parameters with our trigger. (Defaults to null)

  • triggerCount - This overrides how many times the trigger fires. (Defaults to 1)

  • exclusiveTrigger - This restricts the trigger from firing unless it matches this specific Trigger on the character. (Defaults to null)

FireTriggersData

This controls specific parameters associated with the fired trigger. An overview of a few of the parameters

  • paramInt - If you recall from Custom Monster Cards when we made a monster with a conditional trigger, this is where the value we compare the trigger_at_threshold to. For a revenge trigger this gets set to the amount of damage.

  • paramInt2 This is an additional integer parameter, it is used by RelicEffects

  • paramString This is a string parameter available by RelicEffects

  • overrideTargetCharacter - This is a parameter where you can assign a character to. Card Effects within the trigger can then target this character specifically.

In our case we set paramInt to 0 if it was an equipment card otherwise it is 1.

Assigning a Trigger Event Handler to a Trigger

To complete the code this is within our Plugin's Initialize function:

            Railend.ConfigurePostAction(
                c =>
                {
                    var manager = c.GetInstance<IRegister<CharacterTriggerData.Trigger>>();
                    string id = MyPluginInfo.PLUGIN_GUID.GetId(TemplateConstants.CharacterTriggerEnum, "Furnish");
                    var trigger = manager.GetValueOrDefault(id);
                    trigger.SetToTriggerOnCardPlayed(OnPlayedRoomOrEquipment);
                }
            );

Testing

To test a CharacterTrigger we must give the trigger to a Character like usual

This JSON adds the trigger to Dreg:

  "characters": [
    {
      "id": "Dreg",
      "override": "replace",
      "triggers": [ "@FurnishDaze" ]
    }
  ],
  "character_triggers": [
    {
      "id": "FurnishDaze",
      "trigger": "@Furnish",
      "descriptions": {
        "english": "Apply [dazed] [effect0.status0.power] to enemy units."
      },
      "effects": ["@Dazed"]
    }
  ],
  "effects": [
    {
      "id": "Dazed",
      "name": "CardEffectAddStatusEffect",
      "target_mode": "room",
      "target_team": "heroes",
      "param_status_effects": [
        {
          "status": "dazed",
          "count": 1
        }
      ]
    }
  ]
  1. Start as Rector Flicker or give yourself the card during battle.

  2. Play Dreg then any room or equipment card after deployment phase.

  3. All enemies should gain Dazed 1 afterward.

Clone this wiki locally