Custom Triggers - brandonandzeus/Trainworks2 GitHub Wiki
Custom Triggers
Writing a Custom Trigger may prove to be a difficult exercise. The Monster Train Codebase offers no support here, and you will need to implement your hooks to get your trigger to work with Harmony Patches in most cases. Sometimes if your trigger is simple enough, it can be expressed in terms of other GameData types. For example, if you had a trigger that only activates when an imp is summoned on the same floor. That can be done without a Patch using concepts presented in earlier pages.
Steps to creating a Custom Trigger
- Create an instance of the
CharacterTrigger
and/orCardTrigger
class and keep a reference to the created object. It's constructor requires a localization key which will be used as the base for the text and description of the trigger. It is customary to name the base localization of the formTrigger_OnXXX
where XXX is the name of your trigger. - [Optional] If you must create both a CharacterTrigger and CardTrigger of the same type call
CustomTriggerManager.AssociateTriggers
to bind them together. Doing so will fire the CardTrigger if the Character's spawner card has one when the CharacterTrigger is fired. Overgorger's Gorge is an example of this binding. The Gorge trigger on Overgorger is the CardTrigger version of Gorge since it is an effect that permanently changes the card. - Add translations for the localization keys your new Trigger the following localization keys must be set: a. Trigger_OnXXX_CardText Text when the trigger is displayed on the card. b. Trigger_OnXXX_CharacterText (Only necessary for CharacterTriggers) Text when the trigger is displayed on a character. c. Trigger_OnXXX_TooltipText The description of the trigger as displayed on tooltips.
- [Optional] adding an custom icon for your CharacterTrigger.
Call the function
CustomCharacterManager.AddCustomTriggerIcon
passing in the CharacterTrigger object, and two icons. The first used in the trigger icon below the character (an image with white and transparency), the second used in tooltips (black and white image) both being sized 24x24.
Firing a Custom Trigger.
Simple, most places where you would need to fire a trigger will have a reference to CombatManager
.
CombatManager
has a few functions to handle firing triggers:
- The most common is
CombatManager.QueueAndRunTrigger(CharacterState, CharacterTriggerData.Trigger)
which fires a Characters CharacterTrigger effect. You may pass yourCharacterTrigger
object you created in Step 1 above as it will be implicitly converted to the properCharacterTriggerData.Trigger
enum value. - Need to fire the associated trigger for all characters in a room?
CombatManager.ApplyCharacterEffectsForRoom(CharacterTriggerData.Trigger, int)
- Need to fire a CardTrigger?
CombatManager.FireCardTriggers(CardTriggerType triggerType, CardState playedCard, int roomIndex, bool ignoreDeadInTargeting, CharacterState triggeredCharacter, int fireCount, Action cardTriggerFiredCallback = null)
Again you can just pass your CardTrigger object as it will be converted to the proper CardTriggerType enum value.
Setting up your new Custom Trigger in a Card or Character effect.
For both CharacterTriggerDataBuilder
and CardTriggerEffectDataBuilder
the Trigger
property can be set with the CharacterTrigger
or CardTrigger
objects directly. They will be implicitly converted into the proper enum value representing the trigger.
Imp Counter.
This is a trigger I previously made for a mod. This trigger activates when an imp is damaged, allowing another unit to do something in return.
Here's the code. It is not included in the Template, but if you want to see it in action, see this mod. If you want to see the usage of the trigger. That is here
// This is just here to hold constants.
public static class OnOtherMonsterHit
{
public static CharacterTrigger MonsterTrigger = CharacterTrigger("Trigger_OnOtherMonsterHit");
public static CharacterTrigger ImpTrigger = CharacterTrigger("Trigger_OnOtherImpHit");
}
// Method in Character State that actually reduces the HP of a unit.
[HarmonyPatch(typeof(CharacterState), nameof(CharacterState.ApplyDamage))]
class OnOtherMonsterHitPatch
{
// Just to track HP/Armor before and after ApplyDamage is called.
private struct CharacterHpState
{
public int hp;
public int armor;
}
private static CharacterHpState hpState = new CharacterHpState();
public static List<CharacterState> outCharacters = new List<CharacterState>();
public static void Prefix(CharacterState __instance)
{
// cache the hp/armor of the unit.
hpState.hp = __instance.GetHP();
hpState.armor = __instance.GetStatusEffectStacks(VanillaStatusEffectIDs.Armor);
}
public static IEnumerator Postfix(IEnumerator enumerator, CharacterState __instance, CombatManager ___combatManager)
{
// An actual use of the yield return here. The method is already called and the enumerator is filled with data.
// Reyield the data to the caller. Otherwise things will break.
while (enumerator.MoveNext())
yield return enumerator.Current;
if (__instance == null)
yield break;
if (__instance.GetTeamType() == Team.Type.Heroes)
yield break;
var lastAttackerCharacter = __instance.GetLastAttackerCharacter();
if (lastAttackerCharacter == null || lastAttackerCharacter.IsDead || lastAttackerCharacter.IsDestroyed)
yield break;
// Test if the character was hit. If the HP or Armor changes then it should trigger OnHit
if (__instance.GetHP() == hpState.hp && __instance.GetStatusEffectStacks(VanillaStatusEffectIDs.Armor) == hpState.armor)
{
yield break;
}
outCharacters.Clear();
RoomState room = __instance.GetSpawnPoint().GetRoomOwner();
room.AddCharactersToList(outCharacters, Team.Type.Monsters);
foreach (CharacterState character in outCharacters)
{
if (character == __instance)
{
continue;
}
// Again how to actually queue a trigger.
yield return ___combatManager.QueueAndRunTrigger(character, OnOtherMonsterHit.MonsterTrigger, fireTriggersData: new CharacterState.FireTriggersData
{
// This hack is used to get the LastAttackerCharacter in CardEffects.
// Set TargetMode.LastAttackedCharacter, unintuitive, but that's how the code for overrideTargetCharacter was written.
overrideTargetCharacter = __instance.GetLastAttackerCharacter(),
});
if (__instance.GetHasSubtype(SubtypeManager.GetSubtypeData(VanillaSubtypeIDs.Imp)))
{
yield return ___combatManager.QueueAndRunTrigger(character, OnOtherMonsterHit.ImpTrigger, fireTriggersData: new CharacterState.FireTriggersData
{
overrideTargetCharacter = __instance.GetLastAttackerCharacter(),
});
}
}
}
}