-
Notifications
You must be signed in to change notification settings - Fork 5
Custom Tracked Values
If you recall from Custom Spell Cards, we made a card that scales based on the number of cards played in the entire battle, The game keeps track of several stats about the run and code can access these stats and do stuff with them.
Note that throughout this tutorial (and even in the title) I use the term TrackedValue. The game has two types of TrackedValues. The one I am referring to is CardStatistics.TrackedValueType which is used by CardTraits and RelicEffectConditions mainly. The other type of TrackedValue MetagameSaveData.TrackedValue is for run stats and is not covered by this tutorial.
Here we will make our own tracked value, EphemeralCardsPurged and make it accessible to effects within the game. As the name suggests this will be a count of Ephemeral Cards that have been purged.
Like other enums we must request a value be allocated for us from Trainworks
"tracked_value_types": [
{
"id": "EphemeralCardsPurged"
}
]Trainworks will allocate an unused value from the underlying enum CardStatistics.TrackedValueType. We must then fetch the enum value from Trainworks and store it within our mod. It is important we don't hardcode the underlying integer value within our mod because its not guaranteed our allocated enum will always have the same integer value.
The next steps depends on how the TrackedValue will be used. If the TrackedValue is a statistic tied to a card (like the number of times the specific card is played), then nothing more needs to be done as the native behavior of any TrackedValue is to be tied to a card individually, you can directly use the CardStatistics functions to manage/update the value.
Like in Custom Triggers we can get out TrackedValue once the framework processes our JSON files with a PostAction.
Railend.ConfigurePostAction(
c =>
{
var trackedValueRegister = c.GetInstance<IRegister<CardStatistics.TrackedValueType>>();
EphemeralCardsPurged = trackedValueRegister.GetValueOrDefault(MyPluginInfo.PLUGIN_GUID.GetId(TemplateConstants.TrackedValueTypeEnum, "EphemeralCardsPurged"));
});We store the field as a public static variable in some class. It could be your Plugin class, I generally like to keep a TrackedValues class that has all of the enums I created. Whatever the case, be sure to have a public static member variable defined somewhere as you will need it to pass to the CardStatistics.IncrementValue function to modify the stat.
public static CardStatistics.TrackedValueType EphemeralCardsPurged;To override the default handling that a TrackedValue is tracked per card, we can use a TrackedValueHandler. With Conductor, wiring a TrackedValue to code that handles it is easy through its TrackedValues API.
The API allows you to associate a tracked value with:
-
A class that manages the statistic for you. Along with tracking the statistic for the various EntryDurations (ThisTurn, PreviousTurn, ThisBattle).
-
A user provided function that returns the current value of the statistic when requested.
This also handles interfacing with CardStatistics to return the value of the statistic to any effects that request it
This is an abstract class that can be subclassed to handle a TrackedValue ourselves, the three main functions to implement
public abstract int GetValue(CardStatistics.StatValueData statValueData);
public abstract void IncrementValue(CardState? card, int amount, CardStatistics.EntryDuration entryDuration = CardStatistics.EntryDuration.ThisTurn);
public virtual void Reset();Once associated, CardStatistics will call these functions when necessary,
This is a subclass of AbstractTrackedValueHandlerthat manages a single global variable. It handles tracking its value from the three durations (previous turn, this turn, this battle).
Again once it has been associated, You can update the value through CardStatistics.IncrementValue There's no need to keep a reference to this object.
A function can be associated with a TrackedValue, it must follow this delegate
public delegate int TrackedValueGetter(CardStatistics.StatValueData statValueData, IReadOnlyDictionary<CardState, CardStatsEntry> deckStats, ICoreGameManagers coreGameManagers);-
statValueDatacontains the parameters associated with the TrackedValueHandler, for instance for counting the specific number of a particular cards in the deck, you'd want to use statValueData.cardState which specifies the card to count. -
deckStats- contains the stat dictionary for every card that existed this battle (included purged cards). If counting cards in the active deck be sure not to count any cards that exist that have been purged. -
coreGameManagers- contains all of the games managers if you need to reference any manager to compute the result.
In our PostAction right after we get the TrackedValue from the framework, we can call either aTrackedValue.SetTrackedValueHandler(AbstractTrackedValueHandler handler) or aTrackedValue.SetTrackedValueGetter(TrackedValueGetter handler) depending on if you have a class or a function that handles the TrackedValue.
Our PostAction is now:
Railend.ConfigurePostAction(
c =>
{
var trackedValueRegister = c.GetInstance<IRegister<CardStatistics.TrackedValueType>>();
EphemeralCardsPurged = trackedValueRegister.GetValueOrDefault(MyPluginInfo.PLUGIN_GUID.GetId(TemplateConstants.TrackedValueTypeEnum, "EphemeralCardsPurged"));
EphemeralCardsPurged.SetTrackedValueHandler(new SimpleGlobalTrackedValueHandler());
});As mentioned you don't need to keep a reference to the TrackedValueHandler class, but it does have some helper functions to override its current values bypassing CardStatistics this is useful for debugging, however it is best to only use CardStatistics to get the value.
Now this connects the TrackedValue to the CardStatistics functions and Conductor will route CardStatistics function calls to our handlers when the associated CardStatistics functions are called.
But with all of this when we get our stat we will always get the value 0. Why? We never told CardStatistics to increment our stat value when an interesting event happens (When an ephemeral card is purged). So now we must write code that When an ephemeral card gets purged, we tell CardStatistics to increment our stat.
We need to add some code that when the game purges an Ephemeral card to increment our stat. We can do this with Harmony, which is a library for patching, replacing and decorating .NET and Mono methods during runtime.
In short, Harmony allows us to add code that executes before or after a function in the game executes.
Here we want to add code to execute after a function executes. What function, we'll have to look up the CardTrait that implements Ephemeral for that.
Looking this class up in the Decompiler and it has a function PurgeCard which gets called when the card is played or discarded which is what we want. We note that PurgeCard is a private function, with that we can write our Harmony Patch.
[HarmonyPatch(typeof(CardTraitEphemeral), "PurgeCard")]
internal class EphemeralCardsPurgePatch
{
public static void Postfix(ICoreGameManagers coreGameManagers, CardState cardState, bool cardPlayed)
{
if (!cardPlayed)
{
coreGameManagers.GetCardStatistics().IncrementStat(cardState, Plugin.EphemeralCardsPurged, 1);
}
}
}Writing a HarmonyPatch requires a class, with one or more static functions.
[HarmonyPatch(typeof(CardTraitEphemeral), "PurgeCard")]
This line denotes the following class as a HarmonyPatch, we give it the Type of the class we want to patch, along with the method we want to patch. Since it is private we hardcode it as a string but if the method was public it is preferred to use nameof(CardTraitEphemeral.PurgeCard) instead.
The class itself can be named anything EphemeralCardsPurgePatch.
We want to inject some code after the function executes, so we define a Postfix function. The function arguments can be any of the ones the original defines, but note that the argument names must match exactly with the game's function.
As far as the code we are injecting. we only want to count cards that weren't played. The function PurgeCard may actually get called twice the first time it is called is if the Ephemeral card gets played. The second time is when the card is discarded (when cards are played they are also discarded), hence the need for the if statement, we would doublecount Ephemeral cards that are played.
The most important code
coreGameManagers.GetCardStatistics().IncrementStat(cardState, Plugin.EphemeralCardsPurged, 1);We get the CardStatistics instance through the coreGameManagers object. We then Increment our TrackedValue by passing our enum and how much to increment the stat by.
Afterwards Conductor intercepts the call and calls the SimpleGlobalTrackedValueHandler to increment the internal tracking for the stat appropriately incrementing the stats value for the current turn, and the current battle.
We can test new TrackedValues one of two ways. With RuntimeUnityEditor or simply making a card that utilizes the TrackedValue.
Once installed and setup properly, by pressing F12 you can bring up the Runtime Unity Editor. Make sure the Object Browser is open.
If you stored your SimpleGlobalTrackedValueHandler in a static variable you can use Runtime Unity Editor. In the Object Browse type <Namespace>.Plugin where Namespace is the namespace your Plugin class is under. Afterwards hit the Statics button which will bring up the Inspector window which should list your Plugin class if found.
If you do not have an instance of your tracked value handler in your Plugin class then instead TrackedValueTypeExtensions and hit Statics. Here you will find all of the TrackedValues registered you will need to find the integer ID associated with your tracked value which you can find in your plugin class if you have multiple mods installed. Afterwards click TrackedValueHandlers, the index of your tracked value that matches your ID, then value
Afterwards you should see all of the static and instance variables defined in your Plugin class
currentStats contains the stats per EntryDuration - CurrentTurn, PreviousTurn, ThisBattle in that order.
Keep playing Ephemeral cards (give cards Spellchain or use Stardust Invocation) and watch the value for the current turn change. On the next turn the current value of the stat becomes the previous turns value.
If you still have the JSON for PlayOtherCards you can modify the CardTrait it uses to instead refer to the new TrackedValue you created.
{
"id": "PlayOtherCardsScalingTrait",
"name": "CardTraitScalingUpgradeUnitAttack",
"param_tracked_value": "@EphemeralCardsPurged",
"param_entry_duration": "this_battle",
"param_int": 2
}And update the card's description.
"descriptions": {
"english": "Apply +[trait0.power][attack] per [ephemeral] card purged this battle."
},Play a card that generates Ephemeral cards like Stardust Invocation and play them.
Give yourself Play Other Cards, and see the amount of attack it grants increases each time an Ephemeral card is purged.
For the last part lets examine a TrackedValueGetter that Conductor gives modders for free. It is a TrackedValue for getting the number of Blights and Scourges in deck.
internal static int CountBlightsAndScourgesInDeck(CardStatistics.StatValueData _, IReadOnlyDictionary<CardState, CardStatsEntry> deckStats, ICoreGameManagers coreGameManagers)
{
int count = 0;
var cardManager = coreGameManagers.GetCardManager();
foreach (CardState card in deckStats.Keys)
{
if (cardManager != null && (cardManager.GetExhaustedPile().Contains(card) || cardManager.GetEatenPile().Contains(card) || cardManager.GetPurgedPile().Contains(card)))
{
continue;
}
if (card.GetCardType() == CardType.Blight || card.GetCardType() == CardType.Junk)
{
count++;
}
}
return count;
}I'll hightlight key parts of the function.
CardStatistics.StatValueData _ from the method declaration. We can ignore parameters, we ignore this parameter because this TrackedValue doesn't actually use any of the parameters.
We iterate through each card the game knows about this battle. We ensure not to count any cards in that exist in the Consume Pile (ExhaustedPile), Eaten Pile, or have been Purged from the deck. Even though cards have been purged they are still tracked and we don't want to include them in our calculation.
You need not include this code in your project to use it (it is discouraged). Conductor defines a TrackedValue BlightsAndScourgesInDeck that can be used in any object that wants a TrackedValue.