Custom Card Effects - brandonandzeus/Trainworks2 GitHub Wiki
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.
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.
- To get the Parameters from the
CardEffectData
we have been creating all throughout this tutorial. We can get that information fromCardEffectState
. Here whenever we want to get the ParamInt we have set we go throughCardEffectState
. - To get information about our Cards in play, we go through
CardManager
. We can get this Manager throughCardEffectParams
.GetHand
gets our current hand, andGetDrawPile
you guessed it gets the cards we haven't drawn yet. - Another note, if you recall, we have been creating
CardData
instances for all of our cards. Each Card in the deck is represented by aCardState
recall earlier in the tutorial thatCardData
is a template, andCardState
is an instance. There's only 1CardData
and manyCardStates
for each one in your deck. CardStates hold the Effects it plays a list ofCardEffectState
, and the traits it has throughCardTraitState
. There's also the trigger it has, which is aCardTriggerEffectState
. 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 aCardTraitTrinity
; if it does, set it ParamInt to the value we want.
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.
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