-
Notifications
You must be signed in to change notification settings - Fork 3
Tutorial 2: Making Cards and Status Effects
By Michael Coopman
Click here to toggle
Hello modders! In this tutorial, we will finally make a card with effects. Actually, we will make two (shown below). The first card is a Companion that summons a card on deploy, and the second card that triggers whenever its summoner attacks. Note that the two status effects do not exist in the game, so we will have to improvise. Don't worry, we will build up to it.
Like last time, the complete code can be found here. So, let's get started.
See also: Basic Project Setup
In this tutorial, I start with a clean project similar to the end of the basic project setup, instead of continuing from the previous tutorial. In addition, modify your constructor to this:
public Tutorial2(string modDirectory) : base(modDirectory) //Instead of Tutorial2, you should write the name of your class instead.
{
Instance = this;
}then add the following lines:
public static Tutorial2 Instance; //Again, replace Tutorial2 with the name of your class.
public static List<object> assets = new List<object>(); //The list of builders that will build your CardData/StatusEffectData
private bool preLoaded = false; //Used to prevent redundantly reconstructing our data. Not truly necessary.
private void CreateModAssets()
{
//Code for status effects
//Code for cards
preLoaded = true;
}
protected override void Load() //If you already publicized the assembly, replace "protected" with "public"
{
if(!preLoaded) { CreateModAssets(); } //preLoaded makes sure that the builders are not made again on the 2nd load.
base.Load(); //Actual loading and adding assets.
}
protected override void Unload() //If you already publicized the assembly, replace "protected" with "public"
{
base.Unload();
}
//Credits to Hopeful for this AddAssets code.
public override List<T> AddAssets<T, Y>() //AddAssets is called somewhere inside base.Load(). It is called multiple times for each DataFile type, with T being the DataFileBuilder of Y
{
var tAssets = assets.OfType<T>(); //Find asset builders of the corresponding type.
if (tAssets.Any()) //Checks if assets has any builders of the corresponding type.
Debug.LogWarning($"[{Title}] adding {typeof(Y).Name}s: {tAssets.Count()}"); //Debug statement
return tAssets.ToList(); //Return the correct builders.
}These lines will not change from mod to mod, so it's a good template.
We will only have one line in the constructor: Instance = this. It won't do anything in this tutorial, but it will be important when we need to use our mod's methods in other classes, like in some of the helper methods we'll define (and the WriteLine method).
Tip
Only important code in the constructor!
You may ask why I don't put any of the code of CreateModAssets() into the constructor. Well, an instance of the class is constructed when the game boots up, regardless of whether the mod is turned on or not. It would be rather rude to initialize a large amount of data that goes unused for the current session. So only code that must always run should be in the constructor, such as telling our mod an instance of itself exists.
Anyway, we will use the CardDataBuilder and the StatusEffectBuilder in this tutorial. The Builder page contains info on them if you want to familiarize yourself with their methods.
See also: CardDataBuilder
For now, we want to add some cards into the game without worrying about status effects. The code for this will look like:
//Code for cards
//Card 0: Shade Snake
assets.Add(
new CardDataBuilder(this).CreateUnit("shadeSerpent", "Shade Serpent") //Internally the card's name will be "[GUID].shadeSerpent". In-game, it will be "Shade Serpent".
.SetSprites("ShadeSerpent.png", "ShadeSerpent BG.png") //See below.
.SetStats(8,1,3) //Shade Serpent will have 8 health, 1 attack, and a 3-counter.
.WithCardType("Friendly") //All companions are "Friendly". Also, this line is not necessary since CreateUnit already sets the cardType to "Friendly".
.WithFlavour("I don't have an ability yet :/")
.WithValue(50) //Fix future bling issues
.AddPool("MagicUnitPool") //This puts Shade Serpent in the Shademancer pools. Other choices were "GeneralUnitPool", "SnowUnitPool", "BasicUnitPool", and "ClunkUnitPool".
);
assets.Add(
new CardDataBuilder(this).CreateUnit("shadeSnake", "Shade Snake")
.SetSprites("ShadeSnake.png", "ShadeSnake BG.png")
.SetStats(4, 3, 0) //Shade Snake has 4 health, 3 attack, and no timer.
.WithCardType("Summoned") //All summons are "Summoned". This line is necessary.
.WithValue(50) //Fix future bling issues
.WithFlavour("Hissssssssss")
);Every card has a background image and a main sprite in front of that. For quick results, the dimension used is 740x1100 for both sprites. All other sprites on the card (eg frames) is determined by the CardType, so do not worry about that. (Worrying about it will cause you many headaches).
The game's artist Gaziter (@Gaziter on Twitter and Bluesky) provided us with a card template (CSP and PSD files) on the Discord server (
). Here's a modified version of the PSD used in another tutorial: Link (Manually press "Download raw file"). You can use Photopea (https://www.photopea.com) to open this for free in a web browser, or alternatively save the guide PNG at the top of this page in the Overview
.
After positioning your sprites in the template, export all images into an "Images" subfolder located in the same folder as your .dll file, like so:

Let's see if both cards work as is. Build your project, place the .dll and the images (check spelling!) in the right places, and run the game. With your mod loaded, you should see an undiscovered Shade Serpent at the bottom of your journal. (At the time of writing this, shades do not appear in the journal. Poor Shade Snake.) Go into a battle and use the command prompt (another console mod) to gain your cards immediately to see them.
Note
Troubleshooting & Possible Errors (Click Here)
The game crashes on load or whenever I load the mod. It is a null reference error
If the error message mentions CreateModAssets(), check your spelling for the arguments in WithCardType(string cardType) and AddPool(string poolName).
The sprite for the cards do not show up
Check your spelling for the arguments in SetSprites(string mainSprite, string backgroundSprite) and that the strings include the images' file extension.
Also make sure that your sprites are located in the correct folder (refer to the Sprite Stuff section above).
The cards do not appear on the console command auto-complete
Your cards are not loaded. Check to see if you added your cards to the list of assets defined in an earlier code block.

Notice that the GUID has been prefixed on both names. So, internally Shade Serpent is mhcdc9.wildfrost.tutorial.shadeSerpent instead of shadeSerpent. Get/TryGet (see helper methods below) will auto-prepend the GUID for you, but still be careful. Not remembering the GUID in other places (such as how we made Booshu big in Tutorial 1) may cause errors down the line.
Once you had your fun, close the game and return to Visual Studio. It's time to make status effects.
See also: StatusEffectDataBuilder
Status effects can be placed on cards in two ways:
-
attackEffects: the status effect will be applied onto the card's target; OR -
startWithEffects: the status effect starts applied on the card, and may apply another effect to some specified targets.
Most classes of StatusEffects derive from one of three main classes: StatusEffectApplyX, StatusEffectInstant, and StatusEffectData. (Less used are StatusEffectOngoing for temporary stat changes, StatusEffectReaction for conditional triggers, StatusEffectWhileActiveX for temporary effects)
When these effects are applied on a card, and some condition is met, it will apply an effect to another card. If your effect sounds like "When X happens, this card applies Y to Z", it'll usually derive from StatusEffectApplyX.
99% of the time, these should be used as startWithEffects
Example: Blunky uses the startWithEffect When Deployed Apply Block To Self, which has the class StatusEffectApplyXWhenDeployed
Tip
Effects that happen when a card attacks are still startWithEffects (and are usually StatusEffectApplyXOnCardPlayed)
Unless they're meant to apply to the whatever it attacks. In that case directly use the effectToApply as an attackEffect.
When these effects are applied on a card, it will immediately do something to the card, then remove itself. These are useful for when you run specific code once instead of over multiple turns.
100% of the time, these should be used as attackEffects, OR the effectToApply of a StatusEffectApplyX
Example 1: Flamewater uses the attackEffect Increase Attack, which has the class StatusEffectInstantIncreaseAttack.
Example 2: Lil' Berry uses the startWithEffect When Healed Apply Attack To Self, which when healed applies the Increase Attack effect from Example 1.
Tip
Even though Flamewater (and most items) has no attack, the act of playing them against a card (target) is enough to apply their attackEffect on the target.
When these effects are applied on a card, it will change the card's behaviour in some way as long as it stays on the card. This the parent class of statuses like Snow, Spice, Shroom, or non-statuses like Noomlin's StatusEffectFreeAction. If your effect can read like "Apply X", then use this as an attackEffect. But usually if you want these to do something on your card, you'd set them as a startWithEffect.
For units, the overwhelming majority of unit effects are startWithEffects, even ones that clearly apply status effects to targets (e.g. Snobble uses a startWithEffect). attackEffects are commonly used for item cards or for applying simple statuses like snow, shroom, increase attack, etc. For our purposes, both of our cards clearly have a startWithEffect, so that's what we will be focusing on. The act of creating status effects changes based on what is available in the vanilla game.
Before we make the effects, we should make useful helper methods. The following lines are useful for shortening the code and making it look nice.
-
TryGet<T>is a modified version ofGet<T>, a method from the modding API that lets you pull cards, status effect, etc.
Note
There are two major distinctions from Get<T>:
- Any StatusEffectData class can used as
Tand it will automatically cast the status effect to that class.
- If you misspelt the name in
TryGet<T>, the game would throw an error that blocks the screen.
This is great for debugging, and in almost all cases, better than simply returning null (there will not be a case in these tutorials whereGetwould be better).
The other two methods are easier to explain.
-
SStackmakes aStatusEffectStackusing significantly less characters (You should do the same withTStackandTraitDataeventually). - The
StatusCopymethod does all the set-up for copying an existing effect and making theStatusEffectBuilderof an existing class. This comes at the cost of accidentally copying properties that don't apply to your status anymore, possibly causing bugs.
Warning
Even though we introduce the StatusCopy method here, you should avoid using it in favour of copying (a functionally accurate recreation of) its databuilder the databuilder command. This way, you see all the properties that can be changed. Otherwise in most cases you're very likely to blindly copy properties from the old status that can cause visual or game-breaking errors.
public T TryGet<T>(string name) where T : DataFile
{
T data;
if (typeof(StatusEffectData).IsAssignableFrom(typeof(T)))
data = base.Get<StatusEffectData>(name) as T;
else if (typeof(KeywordData).IsAssignableFrom(typeof(T)))
data = (AddressableLoader.Get<KeywordData>("KeywordData", Extensions.PrefixGUID(name, this).ToLower())
?? base.Get<KeywordData>(name.ToLower())) as T;
else
data = base.Get<T>(name);
if (data == null)
throw new Exception($"TryGet Error: Could not find a [{typeof(T).Name}] with the name [{name}] or [{Extensions.PrefixGUID(name, this)}]");
return data;
}
public CardData.StatusEffectStacks SStack(string name, int amount) => new CardData.StatusEffectStacks(TryGet<StatusEffectData>(name), amount);
//See above
//Note: you need to add the reference DeadExtensions.dll in order to use InstantiateKeepName().
public StatusEffectDataBuilder StatusCopy(string oldName, string newName)
{
StatusEffectData data = TryGet<StatusEffectData>(oldName).InstantiateKeepName();
data.name = GUID + "." + newName;
data.targetConstraints = new TargetConstraint[0];
StatusEffectDataBuilder builder = data.Edit<StatusEffectData, StatusEffectDataBuilder>();
builder.Mod = this;
return builder;
}For now let's temporarily add Mimik's effect to Shade Snake. Looking at the Unity explorer/references, we see that the effect is named "Trigger When Ally In Row Attacks":
new CardDataBuilder(this).CreateUnit("shadeSnake", "Shade Snake")
.SetSprites("ShadeSnake.png", "ShadeSnake BG.png")
.SetStats(4, 3, 0)
.WithCardType("Summoned")
.WithValue(50)
.WithFlavour("Hissssssssss") //The new effect adds text, so this won't show up anymore... (unless durian'd)
.SubscribeToAfterAllBuildEvent(data => //New lines
{
data.startWithEffects = new CardData.StatusEffectStacks[]
{
SStack("Trigger When Ally In Row Attacks", 1) //Mimik's effect (temporary)
};
})You may be tempted to use .SetStartWithEffect to avoid the lengthier SubscribeToAfterAllBuildEvent. However SetStartWithEffect won't work once we've made our modded effect, so this saves us the possible future headache.
Don't reinvent the wheel. If there is a similar enough effect, we can copy and modify it. Our Shade Serpent's effect is very similar to a Shademancer leader's: "When deployed, summon Fallow". Looking at the Unity explorer/references again (you will be doing this a lot), we see that the effect is named "When Deployed Summon Fallow", and it has the internal effect "Instant Summon Fallow". This internal effect then has an internal effect called "Summon Fallow". Sigh. So, we are actually modifying three status effects here. The good news is we only have to change one variable for each of these effects.
Note
When revisiting this tutorial, you might want to summon items to hand. In that case, copy the effect used by the Trash trait: "On Card Played Add Junk To Hand". The process will be similar, but copying the wrong one will lead to bugs 🪲🪲
Starting with "Summon Fallow", we create "Summon Shade Snake":
//Code for status effects
//Status 0: Summon Shade Snake
assets.Add(
StatusCopy("Summon Fallow", "Summon Shade Snake") //Makes a blind copy of the Summon Fallow effect. Copy this directly from the databuilder command if possible
.SubscribeToAfterAllBuildEvent<StatusEffectSummon>(data => //Changes the summoned card to Shade Snake, but not immediately. Once Shade Snake is properly loaded, the delegate is called.
{
data.summonCard = TryGet<CardData>("shadeSnake"); //Alternatively, I could've put TryGet<CardData>("mhcdc9.wildfrost.tutorial.shadeSnake") or TryGet<CardData>(Extensions.PrefixGUID("shadeSnake",this)) or the Get variants too
//This is because TryGet will try to prefix the name with your GUID.
//If that fails, then it uses no GUID-prefixing.
})
);
Debug.Log("[Tutorial] Summon Shade Snake Added."); //Debug
//Alternatively, WriteLine("Summon Shade Snake Added");Interestingly, the two cards and four effects each reference each other, which makes fully defining cards/effects one at a time difficult. The SubscribeToAfterAllBuildEvent is for situations like this. The event occurs after all of your mod assets are loaded in, so it would be the best time to perform references of other modded pieces. We do the same with the next two effects.
Note
This above code is not the best example to showcase how to copy and modify status effects as most effects are not summons. In most cases, your effect > would be an StatusEffectApplyX class. For this, you would modify the variables effectToApply and applyToFlags. See the drop-down for more details.
Example with effectToApply and applyToFlags (Click Here)
Below is an example of how it would look like coding the "On Kill Apply Attack To Self" effect.
assets.Add( new StatusEffectDataBuilder(this)
.Create<StatusEffectApplyXOnKill>("On Kill Apply Attack To Self")
.WithText("Gain {0} on kill")
.WithTextInsert("<+{a}><keyword=attack>") //TextInsert will replace the {0} in the line above. <keyword=blah> gives a keyword pop-up on hover. {a} is the current number of stacks.
.WithCanBeBoosted(true)
.WithType("")
.SubscribeToAfterAllBuildEvent<StatusEffectApplyXOnKill>(
data =>
{
data.effectToApply = TryGet<StatusEffectData>("Increase Attack");
data.applyToFlags = StatusEffectApplyX.ApplyToFlags.Self;
})
);The effect is an instance of the class StatusEffectApplyXOnKill. The class itself knows that it must apply something on kill, but it does not know "what" effect and to "whom". The variable effectToApply tells what effect to apply (Increase Attack). The variable applyToFlags tells the target for the effect (Self). Usually, Visual Studio will give you options on what the flag in StatusEffectApplyX.ApplyToFlags.[flag] can be. In addition, multiple flags can be used by separating them with a vertical bar "|".
//Status 1: Instant Summon Shade Snake
//This status effect is an instant effect (inherits from StatusEffectInstant). These effects are placed then immediately removed.
assets.Add(
StatusCopy("Instant Summon Fallow", "Instant Summon Shade Snake") //Copying Instant Summon Fallow and changing the name.
.SubscribeToAfterAllBuildEvent<StatusEffectInstantSummon>(data => //Replacing the targetSummon with our StatusEffectSummon, once the time is right.
{
data.targetSummon = TryGet<StatusEffectSummon>("Summon Shade Snake");
})
);
Debug.Log("[Tutorial] Instant Summon Shade Snake Added."); //Debug
//WriteLine("Instant Summon Shade Snake Added.");
//Status 2: Summon Snake On Deploy
//This status effect is an StatusEffectApplyXWhenDeployed. It applies its effectToApply when something (typically itself) is deployed.
assets.Add(
StatusCopy("When Deployed Summon Wowee", "When Deployed Summon Shade Snake") //Another copy
.WithText("When deployed, summon {0}") //Since this effect is on Shade Serpent, we modify the description shown.
.WithTextInsert("<card=mhcdc9.wildfrost.tutorial.shadeSnake>") //Any {0} in the line above is replaced with the text insert. The html tag must be of the form <card=[GUID name].[card name]>. No spaces around the equal sign. This creates the card pop-up.
.SubscribeToAfterAllBuildEvent<StatusEffectApplyXWhenDeployed>(data => //Another eventual replacement
{
data.effectToApply = TryGet<StatusEffectData>("Instant Summon Shade Snake");
})
);
Debug.Log("[Tutorial] Summon Shade Snake When Deployed Added."); //Debug
//WriteLine("Summon Shade Snake When Deployed Added.");Finally, let's update Shade Serpent with its effect. This is effectively replacing WithFlavor with the SubscribeToAfterAllBuildEvent method. Afterwards, rebuild and run to see what happens. By this point, the Shade Serpent should be fully implemented and you can test its effects in-game (if you are currently in a battle with a Shade Serpent, the change will not affect the current card. So either gain a new serpent or start a new run).
new CardDataBuilder(this).CreateUnit("shadeSerpent", "Shade Serpent")
.SetSprites("ShadeSerpent.png", "ShadeSerpent BG.png")
.SetStats(8,1,3)
.WithCardType("Friendly")
.WithValue(50)
.AddPool("MagicUnitPool")
.SubscribeToAfterAllBuildEvent(data => //New lines (replaces flavor text)
{
data.startWithEffects = new CardData.StatusEffectStacks[] //Manually set Shade Serpent's effects to the desired effect... when the time is right.
{
SStack("When Deployed Summon Shade Snake", 1) //The effect we just made.
};
})It is important that you spell the names of these status effects correctly, both yours and the base game's. A helpful tip is to follow a convention, especially if the base game does it. The base game uses spaces and each word starts capitalized. It is recommended you do the same for status effects. Alternatively, for your own effects, you can make string variables that hold the status effect name and use that variable instead. This way, your IDE will correct you if you misspell your variable name.
Note
Troubleshooting & Possible Errors (Click Here)
Null Reference Error
We used quite a bit of Get's since our last build, and a misspelling in any of them will cause a null reference error. The error is likely a misspelling. Use Debug.Log() and the Unity Explorer from Tutorial 0 for debugging. If the error does not happen on load, use the methods listed in the error message to help narrow down the possible culprits.
If you get a TryGet error, check the spelling in the string.
Hovering over Shade Serpent does not give a pop-up of Shade Snake. Instead, the html tag is shown on the card description
Check the spelling. <card=[GUID name].[card name]> with no spaces around the equal sign.
Sometimes, it is necessary to make a new StatusEffect class. Even in this case, it is useful to know what came before. No doubt you have seen the similarity between Mimik's effect and Shade Snake's. The only differences is that we want our card to ignore the row condition but require a specific ally instead. We should look at what makes Mimik's effect work then.
Through the references, Mimik's effect is an instance of the StatusEffectTriggerWhenAllyAttacks class. Peeking at the code in the object browser gives us some understanding of it. Whenever a hit (RunHitEvent) is performed, the effect checks if the hit represents an ally attacking. Then, the target (Mimik) triggers during the OnCardPlayed event. The bool allyInRow adds the condition that the attacking ally is in the row. The bool againstTarget makes it so the target attacks whatever the ally attacks (note that Puffball also uses this class). If we make a subclass of this one, all we need to do is change allyInRow to false and override HasRunEvent. Unfortunately, allyInRow is a private variable, so, we cannot change it from the outside without doing some assembly stripping (see the advanced part of Basic Project Setup). Though we could make a carbon copy of the entire class but set that variable to public, that generally sounds like bad practice. In order to keep the tutorial basic, Shade Snake to only trigger when a Shade Serpent attacks in the row. The code then becomes rather simple:
public class StatusEffectTriggerWhenCertainAllyAttacks : StatusEffectTriggerWhenAllyAttacks
{
//Cannot change allyInRow or againstTarget without some publicizing. Shade Snake is sad :(
//If you have done the assembly stripping part, feel free to change those variables so that ShadeSnake can rise to its true potential.
public CardData ally; //Declared when we make the instance of the class.
public override bool RunHitEvent(Hit hit)
{
Debug.Log($"[Tutorial] {hit.attacker?.name}"); //Debug
if (hit.attacker?.name == ally.name) //Checks if the ally attacker is Shade Serpent.
{
return base.RunHitEvent(hit); //Most of the actual logic is done through the StatusEffectTriggerWhenAllyAttacks class, which is called here.
}
return false; //Otherwise, don't attack.
}
}The Hit class is used to describe most interactions between units (e.g. healing is a hit) or communication from the game to a unit (e.g. counting down is a hit). So, it is possible for hit to have null as an attacker. (Tangent: the variables of a Hit are public and free to change, which is what Demonize, Shell, and Block all do). Finally, we make create the last StatusEffectData, and attach it to Shade Snake:
//Status 3: Trigger When Shade Serpent In Row Attacks
assets.Add(
new StatusEffectDataBuilder(this)
.Create<StatusEffectTriggerWhenCertainAllyAttacks>("Trigger When Shade Serpent In Row Attacks")
.WithCanBeBoosted(false) //Not a Lumin Ring/Vase target
.WithStackable(false) //Allow multiples of this effect with different stacks on a single card
.WithText("Trigger when {0} in row attacks") //Changing the text description.
.WithTextInsert("<card=mhcdc9.wildfrost.tutorial.shadeSerpent>") //You must put the GUID in some way here. $"<card={Extensions.PrefixGUID("shadeSerpent",this)}>" works as well here.
.WithType("") //Type is typically used for SFX/VFX when applying the status effect. Not necessary as we are not applying this effect during battle, unless you use the "add effect" command.
.FreeModify(data => //FreeModify edits the variables immediately. These variables correspond to any Trigger effect
{
data.isReaction = true; // Gives the reaction symbol at the bottom of the card, and lets Ooba Charm (card starts snowed) work
data.descColorHex = "F99C61"; // Colors the entire text brown
data.affectedBySnow = true; // "Trigger" effects should be snow-able
})
.SubscribeToAfterAllBuildEvent<StatusEffectTriggerWhenCertainAllyAttacks>( //Finally, declare the ally to be Shade Serpent.
data =>
{
data.ally = TryGet<CardData>("shadeSerpent");
})
);
Debug.Log("[Tutorial] Trigger When Shade Serpent In Row Added."); //Debug
//WriteLine("Trigger When Shade Serpent In Row Added.");
//Code for cards
//Card 0: Shade Snake
assets.Add(
new CardDataBuilder(this).CreateUnit("shadeSnake", "Shade Snake")
.SetSprites("ShadeSnake.png", "ShadeSnake BG.png")
.SetStats(4, 3, 0)
.WithCardType("Summoned")
.WithValue(50)
.WithFlavour("Hissssssssss") //Should not show up anymore.
.SubscribeToAfterAllBuildEvent(data => //New Line start here (replace the Mimic effect)
{
data.startWithEffects = new CardData.StatusEffectStacks[]
{
SStack("Trigger When Shade Serpent In Row Attacks",1)
};
})
);Rebuilding and playing should get us the working cards. Errors encountered are typically null references, so double-check spelling and verify that all useful variables are defined. Have fun!
Note
Troubleshooting (possible errors)
Null Reference Error
Check the spelling for every Get method added since the last build.
Use Debug.Log() and the Unity Explorer from Tutorial 0 for debugging. If the error does not happen on load, use the methods listed in the error message to help narrow down the possible culprits.
TryGet Error
Check your spelling and possibly the class name.
Hovering over Shade Snake does not give a pop-up of Shade Serpent. Instead, the html tag is shown on the card description
Check the spelling. <card=[GUID name].[card name]> with no spaces around the equal sign.
Shade Snake does not attack when a Shade Serpent in row attacks
Use the unity explorer to make sure that the ally in Shade Snake's effect is set to Shade Serpent.

Now that the cards work as intended, there is still an issue remaining: unloading the mod may crash the game when starting a new run if you do not exit the application in between. This is because unloading removes cards from the reward pools, but there is still a gap in the pools of where they once were. To fix this, call this method in your Unload method.
//Call this method in Unload.
public void UnloadFromClasses()
{
List<ClassData> tribes = AddressableLoader.GetGroup<ClassData>("ClassData");
foreach(ClassData tribe in tribes)
{
if (tribe == null || tribe.rewardPools == null) { continue; } //This isn't even a tribe; skip it.
foreach(RewardPool pool in tribe.rewardPools)
{
if (pool == null) { continue; }; //This isn't even a reward pool; skip it.
pool.list.RemoveAllWhere((item) => item == null || item.ModAdded == this); //Find and remove everything that needs to be removed.
}
}
}To ensure it worked, build the solution, open the game. Then use the command Inspect ClassData Magic to view the Shademancer class. Check its reward pools and find the Shade Serpent. Now, unload the mod, and click the green "Update displayed values" button to update that list. The Shade Serpent should be gone and there should be no null in the list. If that is correct, good job! You have successfully unloaded your mod. (Be sure to remember that changing vanilla status effects/cards may need to be reverted when unloading).

This tutorial misses a key aspect of modding and coding in general: the joy torture of debugging code. When working on your own mod, you will inevitably encounter errors. They will usually be null reference errors, and they will be as mysterious as possible. Don't fret, look closely at your code, and use the unity explorer if applicable. In a lot of cases, the errors are caused by the dumbest of slip-ups. That's ok, it happens to everyone (for this tutorial, most errors were caused by spelling mistakes or forgetting to define a variable. rather than a logical error).
If you want to contribute or need support, join the Wildfrost Discord
