Tutorial 1: Making a Basic Mod (Gameplay Modifier) - rspforhp/WildfrostModdingDocumentation GitHub Wiki

By Michael Coopman

Overview

This tutorial will show how to make a mod, test it, and publish it. The example mod will be a modifier system. You may ask, "Why not start with a mod for a unit, item, or a charm?" Well, that is because:

(1) modifier systems are easier to code.

(2) it may not be obvious that these is easier to code.

(3) the effects of these systems are more apparent than a single unit, item, or mod can be.

In effect, I want this tutorial to show how much power your mod has over the gameplay experience. If you just want the example code, the complete tutorial mod code will be available here. Well, let's get started.

From the basic project setup, your code should look like this, with minor variations:

public Tutorial1(string modDirectory) : base(modDirectory)
{
}

public override string GUID => "mhcdc9.wildfrost.tutorial"; //[creator name].[game name].[mod name] is standard convention. LOWERCASE!

public override string[] Depends => new string[0]; //The GUID of other mods that your mod requires. This tutorial has none of that.

public override string Title => "The 1st Tutorial of Many (or a Few)"; //See the 1st in-game image

public override string Description => "The goal of this tutorial is to create a modifier system (think daily voyage bell) and make it a mod."; //See the 1st in-game image

Card Data Editing

First, let's see if we can do something simple: can we set Booshu's hp and attack to 99? There's an easy way to do this with Get<CardData>(string name), but this will change the master copy of Booshu which is bad practice due to unloading, compatibility, etc. Instead, we will try a delicate and more general approach. The master copy of any card is never used directly; only its clones are used. So, whenever a clone of Booshu is created, we set the clone's hp and attack to 99. How do we do this? Cue the Events class.

Events

As mentioned in Tutorial 0, the Events class holds all the general events that are tracked during the game. Any class or object may attach their own event handler method to this event. There are 174 events, and some of them will be mentioned at the end. The event for this tutorial is OnCardDataCreated, which is invoked whenever a master copy card is cloned (convenient, isn't it?). We want to hook our method on Load() and unhook on Unload(). Add the following lines to the class:

protected override void Load() //If you already publicized the assembly, replace "protected" with "public"
{
    base.Load();                           //Where most of the loading actually happens
    Events.OnCardDataCreated += BigBooshu; //Whenever anything invokes OnCardDataCreated, the method BigBooshu will be called.
                                           //We will define BigBooshu soon. 
}

protected override void Unload() //If you already publicized the assembly, replace "protected" with "public"
{
    base.Unload();
    Events.OnCardDataCreated -= BigBooshu; //Unhooking the BigBooshu method from OnCardDataCreated.
}

Now, to make the method BigBooshu. Usually, I hover over the word BigBooshu, see the potential fixes, and choose the first option. This gets the return type and parameters types without needing to look it up. The proper way would be to go to the Events class, find the OnCardDataCreated event and record the parameter and return type. In this case, the return type is void and the parameter type is CardData. The cloned object is sent as the parameter, so we just check if its name is "BerryPet" (this is Booshu's internal name) and change its stats accordingly. See the References or use the Unity Explorer mod for internal names.

private void BigBooshu(CardData cardData) //cardData is the CardData that was created/duplicated
{
    Debug.Log("[Tutorial1] New CardData Created: " + cardData.name); //If the method is unrecognized, try UnityEngine.Debug.Log instead.
    if (cardData.name == "BerryPet")     //Booshu's internal name is BerryPet 
    {
        cardData.hp = 99;                //Setting hp
        cardData.damage = 99;            //Setting damage
        cardData.value = 99 * 36;        //Setting gold dropped when killing enemy BigBooshu to 99
        Debug.Log("[Tutorial1] Booshu!");
        //Alternatively, WriteLine("Booshu!"); works too. See below.
    }
}

Debug.Log / WriteLine

You may notice there are two Debug.Log lines that do not seem necessary to the method. It will only be seen if you have the Console mod, so players of your mod will typically not see it. Debug.Log, Debug.LogWarning, and Debug.LogError will print lines to the console window with increasing levels of importance. It is important to put a unique [tag] in the front to easily find your debug lines (using the Colored Console Mod makes this even easier). Note that if there are streamlined methods for logging with your GUID already: WriteLine, WriteWarn, and WriteError. As I prefer the title/keyword over the entire GUID, the tutorials will use Debug.Log, but code blocks will typically write the alternate way in a nearby comment.

Building/Testing the Mod

Now, it's time to see if it works. Press Ctrl+B to build the project. This creates a .dll in [wherever the code is stored]/obj/debug. The output window in Visual Studio should show the file location. Make a new folder in [GameRoot]/Modded/Wildfrost_Data/StreamingAssets/Mods and place the .dll file there. To find [GameRoot], right-click Wildfrost on steam, go to properties, and browse installed files. Additionally, you can also place an "icon.png" in the newly made folder to give your mod an icon. Now, let's run the game and see.


image

Tutorial1-4 BooshuMod

Caution

"[Error] Couldn't extract exception string from exception of type TypeLoadException (another exception of class 'TypeLoadException' was thrown while processing the stack trace)"
If an empty Error screen shows up ingame, or you get the above message in the console (from Miya/Kopie's Console Mod), then you've used the wrong project settings. Go back to the Basic Project Setup for the correct setup. Make sure to use Class Library (.NET Framework)!


Yay! Let's turn it on and see if Booshu is big. If you go to the Pet House, we see its stats are unchanged. Looking at the log (see Console mod in Tutorial 0), this is because the OnCardDataCreated was never called, otherwise you would see your debug statements. These card copies are never used in the game, so no one bothered to invoke OnCardDataCreated here.

If you try to start a new run, you can see the result:


Note

Troubleshooting & Possible Errors (Click Here) Booshu does not have the desired stats.

Look through the console and find the debug statements from the code (starts with [Debug][Tutorial1]). If none of those are showing up anywhere, BigBooshu was not hooked up properly. The problem is likely in your Load() code.

If some debug statements are there but not the "Booshu!" one, check the spelling of "BerryPet" in your BigBooshu code.

If all debug statmenets are showing up properly and there is still an error, verify the body of the if block in BigBooshu.


Tutorial1-5 BigBooshu

Tutorial 1-6 BooshuDebug

(Using WriteLine would have outputted [mhcdc9.wildfrost.tutorial] instead of [Tutorial1].)


Give All Enemies "Apply Haze"

Well, you can choose BigBooshu and win a run, but maybe we should make the enemies harder first. Let's make them act like Bursters! Back to Visual Studio, we will hook on a ScaryEnemies method to OnCardDataCreated just like we did with BigBooshu. It will look something like this:

//Don't forget to hook this method onto OnCardDataCreated in the Load and Unload methods. 
//This is done in a similar way 
private void ScaryEnemies(CardData cardData)
{
    switch (cardData.cardType.name) //We want all enemies to be harder. There are 4 cardTypes that are enemies, shown below.
    {
        case "Miniboss":
        case "Boss":
        case "BossSmall":
        case "Enemy":
            //Since enemies already have their own set of attack effects, we are simply just adding haze to that list.
            cardData.attackEffects = CardData.StatusEffectStacks.Stack(cardData.attackEffects, new CardData.StatusEffectStacks[]
            {
                new CardData.StatusEffectStacks( Get<StatusEffectData>("Haze"), 1) //This is 1 stack of the haze effect.
            });
            //Reward the player by dropping twice as much gold
            cardData.value *= 2;
        break;
    }
}

Run the game to see the nightmare we have created.


Note

Troubleshooting & Possible Errors (Click Here)

The enemies are unchanged.
If you loaded into the middle of a battle, the enemies of that battle will not be updated. Otherwise, ScaryEnemies is not hooked on properly (or the code was not written). Check the Load function.

The game crashes when entering a battle or when an enemy attacks. The error is a null reference.
Every time Get is used and the string is incorrectly typed, the function returns null and it will not throw an error until later (when it matters). Check if "Haze" is misspelled.

None of these suggestions helped.
Ask in the #mod-development channel on the Wildfrost discord.


Tutorial1-8ThisIsFine


Publishing your mod

On your mod in the mod page, there is a Steam button to the right, that button will publish the game (after a confirmation). Do that only when you're ready. It probably won't be this mod. Clicking the button also allows you to update your mod. Important: It is highly recommended that you subscribe and load the Mod Uploader first. This would allow you to place tags on your mod.

The Steam workshop uses GUID to distinguish mods. The difference between publishing a new mod and updating an old one is whether the GUID has stored in the workshop. So, once you publish, do not change the GUID.

Other Notable Stuff

Some Other Events

OnCardDataCreated: invoked when the master copy of a card is cloned. The parameter is the cloned CardData. This is called before the Card or Entity are created. Most vanilla modifier systems use this.

OnEntityCreated: invoked when an entity of a CardData is created. The parameter is the Entity.

OnSceneLoaded: invoked when a scene is loaded and its parameter is the scene. The parameter is the loaded scene. This is a useful indication of whether you are in Snowdwell, a battle, or the campaign map.

OnEntityOffered: invoked when a card is presented to you as a choice (treasure, companion ice).

OnEntityChosen: invoked when a card is chosen among the choices offered.

OnBattleEnd: invoked when the battle is over and the sun bear thing is greeting you.

PostBattle: invoked when you return to the campaign map after a battle.

OnCampaignEnd: invoked when the run is over, however that may occur.

Vanilla ModifierSystem Classes

Simply search "ModifierSystem" in the object browser to find them. Notable ones are:

AddFrenzyToBossesModifierSystem: Daily Voyage Bell

BombskullClunkersModifierSystem: Daily Voyage Bell

BoostAllEffectsModifierSystem: Daily Voyage Bell

BoostArea2EnemyDamageModifierSystem: Old Design

DeadweightAfterBossModifierSystem: Gunk Fruit

DoubleGoblingGoldModifierSystem: Daily Voyage

PermadeathModifierSystem: Daily Voyage Bell (Fun)

MoreCardRewardsModifierSystem: ???

RecallChargeRedrawBellModifierSystem: Bell of Recall

RecallBellStartChargedModifierSystem: Bell of Recharge

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