Custom Card Effects - brandonandzeus/Trainworks2 GitHub Wiki

Custom Card Effects

Overview

So now we dive into our first custom card effect. All CardEffects are subclasses of CardEffectBase. CardEffects can be a bit tricky to implement as you now have to worry about the Combat State, whether or not the CardEffect is being played as a preview or for real. The preview system is rather finicky, and you don't want to do anything to break it. Otherwise, that makes for a poorer user experience playing your mod!

I find that the base game's CardEffects cover just about 99.99% of all use cases, but if you are doing something out of the park, then Custom CardEffects become necessary. Again this should be a last resort if none of the existing card effects work for your use case.

if you hadn't already, take a look at Custom Card Traits, as this tutorial builds off that one.

CardEffectFillTrinity

Building off the Trinity effect, we should make a card that automatically activates all cards' Trinity effect and all Trinity cards in the draw pile. There's no card effect that can apply an upgrade in this fashion, so we are left to make it ourselves.

Here's the class implementing such an effect. Again comments are inline.

    class CardEffectFillTrinity : CardEffectBase
    {
        public override bool CanPlayAfterBossDead => false;
        // Important to set to false! In this case we don't want to apply this effect in preview mode.
        // Otherwise, when the card with this effect is drawn, the cards are immediately upgraded...
        public override bool CanApplyInPreviewMode => false;

        // This determines if you can play the card. You normally check for things such as if there's a valid target.
        // Here, the card is always playable.
        public override bool TestEffect(CardEffectState cardEffectState, CardEffectParams cardEffectParams)
        {
            return true;
        }

        public override IEnumerator ApplyEffect(CardEffectState cardEffectState, CardEffectParams cardEffectParams)
        {
            // Get your current hand.
            List<CardState> hand = cardEffectParams.cardManager.GetHand();
            foreach (CardState card in hand) 
            { 
                foreach (CardTraitState trait in card.GetTraitStates())
                {
                    if (trait is CardTraitTrinity)
                    {
                        // Set trait to activate Trinity.
                        trait.SetParamInt(cardEffectState.GetParamInt());
                        // Kick the card the update its text.
                        card.UpdateCardBodyText();
                        // Also refresh the card.
                        cardEffectParams.cardManager?.RefreshCardInHand(card, cleanupTweens: false);
                        // And show the Enhance effect on the card. This is a method call on the corresponding CardUI object.
                        cardEffectParams.cardManager.GetCardInHand(card)?.ShowEnhanceFX();
                        break;
                    }
                }
            }

            // Also do this for all cards in the draw pile.
            foreach (CardState card in cardEffectParams.cardManager.GetDrawPile())
            {
                foreach (var trait in card.GetTraitStates())
                {
                    if (trait is CardTraitTrinity)
                    {
                        trait.SetParamInt(cardEffectState.GetParamInt());
                        // No need to refresh these cards as they are in the draw pile.
                        break;
                    }
                }
            }

            // No other IEnumerator methods were called, so we yield break.
            yield break;
        }
    }

A few things to note.

  1. To get the Parameters from the CardEffectData we have been creating all throughout this tutorial. We can get that information from CardEffectState. Here whenever we want to get the ParamInt we have set we go through CardEffectState.
  2. To get information about our Cards in play, we go through CardManager. We can get this Manager through CardEffectParams. GetHand gets our current hand, and GetDrawPile you guessed it gets the cards we haven't drawn yet.
  3. Another note, if you recall, we have been creating CardData instances for all of our cards. Each Card in the deck is represented by a CardState recall earlier in the tutorial that CardData is a template, and CardState is an instance. There's only 1 CardData and many CardStates for each one in your deck. CardStates hold the Effects it plays a list of CardEffectState, and the traits it has through CardTraitState. There's also the trigger it has, which is a CardTriggerEffectState. There are getters that will return a list of all of these objects if you need them. Here we need to check if the card has a CardTraitTrinity; if it does, set it ParamInt to the value we want.

How to handle Previews.

This is very important to reiterate.

CanApplyInPreviewMode If your effect is doing anything other than changing the CharacterState, then you can set it to false. If, for some reason, you wanted to both mutate a Card and a Character, then in ApplyEffect, you can check if this is being called for a Preview by checking if cardEffectParams.saveManager.PreviewMode is true. For a good example, See CardEffectAddTempCardUpgradeToUnits for that CardEffect specifically, you want to apply the CardUpgrade to any units, but you don't want to apply the upgrade to the card in preview mode.

Why? You see, when Preview Mode is set, each Character (Heroes and Monsters) has a PreviewState setup; this is automatically done by CombatManager when Combat is run for preview results. Any modification to CharacterStates will change its Preview State. Now, when you play a card or hit the end turn button, Combat is run for keeps, this time against the actual state. Only CharacterState has previews set up and not Cards, so if you applied an upgrade to a card, the effect stays on.

If you really want to see what I mean by this at the end of this tutorial, set CanApplyInPreviewMode to true. And watch as you draw this card. The cards are immediately upgraded multiple times since previews will run anytime the CombatState has changed.

Trinity Charge.

Here's the CardData again. Nothing too noteworthy. However, I will use this opportunity to do something a little different. Lets have a tooltip popup showing what Trinity is since it is affecting Trinity cards.

   class TrinityCharge
    {
        public static readonly string ID = TestPlugin.CLANID + "_TrinityCharge";

        public static void BuildAndRegister()
        {
            // Use the new CardEffect
            var effect = new CardEffectDataBuilder
            {
                EffectStateType = typeof(CardEffectFillTrinity),
                // Param to set Trinity Traits to.
                ParamInt = 2,
                // Popup a nice tooltip explaining the Trinity keyword.
                AdditionalTooltips =
                {
                    new AdditionalTooltipData
                    {
                        // This must be a localized text key
                        titleKey = "CardTraitTrinity_CardTooltipTitle",
                        descriptionKey = "CardTraitTrinity_CardTooltipText",
                        // This is the default.
                        // style = TooltipDesigner.TooltipDesignType.Keyword
                        isStatusTooltip = false,
                        isTriggerTooltip = false,
                        isTipTooltip = false,
                    }
                }
            };

            // Simple localization for one language only.
            BuilderUtils.ImportStandardLocalization("CardTraitTrinity_CardTooltipTitle", "Trinity");
            BuilderUtils.ImportStandardLocalization("CardTraitTrinity_CardTooltipText", "Every 3rd time a Trinity card is played, the card will cost 0 ember and have an additional 50 Magic Power");

            new CardDataBuilder
            {
                CardID = ID,
                Name = "Trinity Charge",
                Description = "Fully charge <b>Trinity</b> cards in hand and in the draw pile",
                Cost = 1,
                Rarity = CollectableRarity.Common,
                TargetsRoom = true,
                Targetless = true,
                ClanID = Clan.ID,
                AssetPath = "assets/trinitycharge.png",
                CardPoolIDs = { VanillaCardPoolIDs.MegaPool },
                EffectBuilders =
                {
                    effect
                },
                TraitBuilders =
                {
                    new CardTraitDataBuilder
                    {
                        TraitStateType = typeof(CardTraitExhaustState),
                    }
                },
                
            }.BuildAndRegister();
        }
    }
  • AdditionalTooltips: This nifty parameter makes a tooltip popup when the player is mousing over the card. All you need is a title and text, but these aren't the actual text but a localization key. Below I show how to create a simple localization text mapping the key to the translation. This is only if you wanted to support one language and shouldn't be used if you intend to translate your mod. Again the Custom Localization Tutorial should be used in that case.

Again nothing new. ParamInt is set to 2, the indicator to CardTraitTrinity that activates the Trinity effect this time.

And there you have it, your own Custom Card Effect, this was a bit more complicated than a CardTraits, but this will really make your cards stand out from the base game.

Next: Custom Room Modifiers

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