Building a Class Mod - BG3-Community-Library-Team/BG3-Community-Library GitHub Wiki
Class Mods are an exciting way to add more playstyles to the game. Rather than being limited to the existing classes, you can completely reinvent gameplay.
- Getting Started
- Class Descriptions
- Ability Distribution Presets
- Progressions
- Localization
- Abilities, Passives, and Spells
- Passive and Spell Lists
- Custom Equipment
- Custom Icons
- Skill Icons
- Handling Subclasses
There's a lot that goes into creating a custom class, but overall it's one of the easier things to do. This guide will go with the assumption that you're building this with the Community Library in mind. If you want to create a class, but don't want to include any parts of it in the community library, this guide will still be applicable, but you may need to make some modifications. You'll also want to skip the rest of this step and head straight to File Layout.
Building a mod for Community Library involves adding the base components into the Community Library, and building a separate Implementation Mod. To get started, make sure you've followed the steps outlined in our How to Contribute and Setting up a Development Environment pages, and have pulled down your own copy of the Community Library.
- VSCode, or other preferred text editor/IDE
- Git
- A local fork of Community Library
- LSLib
- Modder's Multitool
Now, before we start, let's just make sure we have the latest code.
- Open Gitbash in your Community Library folder.
- Input
get checkout main
to navigate to the main branch. - Input
git fetch && git pull
to make sure you have the latest version of the project - Finally, Input
git checkout -b BRANCHNAME
, substituting BRANCHNAME with a descriptive name of your branch.
For our Implementation Mod, we'll need to set up a file structure, outside of our Community Library folder. Let's assume something like this, and add and remove as we go:
ModName/ (Folder which will contain your mod's folder, zip files, and any other files that won't be converted into a .pak file)
ModName/
/Localization/English/ModName.xml
/Mod/ModName/meta.lsx
/Public/ModName/...
Of course, sub English with whichever language you want to release your mod in. For the sake of this tutorial, we'll assume English.
We're going to start with the most unintuitively named part of the whole Class workflow. Class Descriptions have nothing to do with Localization - these nodes are, in fact, where we define our Class. This is done in Community Library. Class Descriptions are stored in Public/ModName/ClassDescriptions/ClassDescriptions.lsx
. The nodes aren't too lengthy, so let's take a quick look at an example provided by MailMe's Death Knight, graciously donated to the Community Library:
<node id="ClassDescription">
<attribute id="BaseHp" type="int32" value="12"/>
<attribute id="CanLearnSpells" type="bool" value="false"/>
<attribute id="CharacterCreationPose" type="guid" value="0f07ec6e-4ef0-434e-9a51-1353260ccff8"/>
<attribute id="ClassEquipment" type="FixedString" value="EQP_CC_DeathKnight"/>
<attribute id="Description" type="TranslatedString" handle="h8f688ae8g8104g4542g94cfge4d0b1d5318c" version="1"/>
<attribute id="DisplayName" type="TranslatedString" handle="h0ac042b8g9c9eg40f3g88dag83496ad4b8a8" version="1"/>
<attribute id="HpPerLevel" type="int32" value="7"/>
<attribute id="LearningStrategy" type="uint8" value="1"/>
<attribute id="MustPrepareSpells" type="bool" value="false"/>
<attribute id="Name" type="FixedString" value="DeathKnight"/>
<attribute id="PrimaryAbility" type="uint8" value="3"/>
<attribute id="ProgressionTableUUID" type="guid" value="29b323b1-d1fa-4882-a8f3-d4cb0ad6c158"/>
<attribute id="SoundClassType" type="FixedString" value="Fighter"/>
<attribute id="SpellCastingAbility" type="uint8" value="5"/>
<attribute id="UUID" type="guid" value="52296c38-fbb5-4e3a-a728-0420884ed152"/>
<children>
<node id="Tags">
<attribute id="Object" type="guid" value="c7bdcdf2-15e7-456e-adf1-21dda3172e18"/>
</node>
<node id="Tags">
<attribute id="Object" type="guid" value="1ae7017c-4884-4a43-bc4a-742fa0d201c0"/>
</node>
</children>
</node>
Looking at the above example, we can see there are a good number of attributes. Let's go over what we're seeing:
- BaseHp - This is the Base HP value of the class(what you'd expect the class to have at level 1).
- CanLearnSpells - This refers to the Wizard's ability to learn spells from scrolls.
- CharacterCreationPose - This references the pose that the class should have during Character Creation. This is typically the same value for all playable classes.
- ClassEquipment - This matches the ID(Name) of an equipment entry, something we'll go over later on in the guide.
- Description - This is the localization handle for our Class' description.
- DisplayName - This is the localization handle for our Class' displayed name.
- HpPerLevel - The base amount of HP gained per level.
- LearningStrategy - Going to level with you, I'm not totally sure what this actually translates to, but the value for most classes is 1. For Archfey Warlock, it's 2.
- MustPrepareSpells - A boolean value on whether or not the class must prepare their spells, if they have any.
- Name - The internal reference to this Class.
- PrimaryAbility - Primary attribute for the class. Maps to the attributes, 1 = STR, 2 = Dexterity, 3 = Constitution, 4 = Intelligence, 5 = Wisdom, 6 = Charisma
- ProgressionTableUUID - Maps to the TableUUID value of the Class' Progressions .
- SoundClassType - Refers to the Sounds this class makes.
- SpellCastingAbility - Similar to
PrimaryAbility
, but identifies the Class' Spellcasting Modifier.
We can also add tags to the class, as you can see.
So what do we do with this? Let's start by copy/pasting it as a new Race node, and making our changes.
- Using Modder's Multitool, tick "Handle", and generate a new UUID.
- Copy the UUID(click on it's display), and paste it into Description's handle value.
- Do the same for DisplayName.
- Write an internal name, prefixed with CL_ to help ensure there aren't any collisions with other mods
- Set the ClassSoundSwitch based on another similar class.
- Untick "Handle" in Modder's Multitool, and generate a new UUID, pasting it into the UUID attribute's handle field.
- Set your Class' base HP value.
- Set your Class' Primary Attribute and Spellcasting Modifier Attribute.
- Decide if you want your class to learn spells from scrolls, or if you class must prepare spells to use them.
Leave the Progression UUID field blank for now. We'll come back to that in the Progressions section. Same goes for Class Equipment. Once you've finished making your class' ClassDescription node, we'll move on to Ability Distribution.
The Ability Distribution Presets define the presets for a class' Attributes in Character Creation. You'll want to set it up in your implementation mod, at this location:Public/ModName/AbilityDistributionPresets/AbilityDistributionPresets.lsx
. Overall a fairly simple piece, an AbilityDistributionPreset
node looks something like this:
<node id="AbilityDistributionPreset">
<attribute id="ClassUUID" type="guid" value="52296c38-fbb5-4e3a-a728-0420884ed152"/>
<attribute id="Charisma" type="int32" value="8"/>
<attribute id="Constitution" type="int32" value="15"/>
<attribute id="Dexterity" type="int32" value="8"/>
<attribute id="Intelligence" type="int32" value="8"/>
<attribute id="Strength" type="int32" value="15"/>
<attribute id="Wisdom" type="int32" value="15"/>
<attribute id="UUID" type="guid" value="5cd2d546-d19c-4271-9f11-8577b25d51f2"/>
</node>
It's fairly self-descriptive, so I won't break down what each piece means. They key point here is that you need to reference your class, generate an ID for the preset, and assign base stat values for each of the listed attributes. Note that while this file looks general enough to be placed in Community Library, because it's in the CharacterCreationPresets
folder, we recommend keeping it in your Implementation mod for now.
One issue you'll run into if you've attempted to test the mod already: The class, though it's defined, doesn't show up in Character Creation. This is because it lacks a Level 1 Progression. As Progressions are what define our class as implemented, this is something we'll do in our Implementation Mod. Make a Progressions
folder in Public/ModName/
, and create a file called Progressions.lsx
, with the following boilerplate:
<?xml version="1.0" encoding="UTF-8"?>
<save>
<version major="4" minor="0" revision="9" build="330"/>
<region id="Progressions">
<node id="root">
<children>
</children>
</node>
</region>
</save>
Everything we add to this file will be between the two children
tags under the root node.
Now that we have our basis for a Progressions file, let's take a look at what a Progression Node looks like:
<node id="Progression">
<attribute id="AllowImprovement" type="bool" value="false"/>
<attribute id="Boosts" type="LSString" value="ProficiencyBonus(SavingThrow,Wisdom);ProficiencyBonus(SavingThrow,Constitution);Proficiency(Greatswords);Proficiency(HeavyArmor);Proficiency(MediumArmor)"/>
<attribute id="Level" type="uint8" value="1"/>
<attribute id="Name" type="LSString" value="DeathKnight"/>
<attribute id="ProgressionType" type="uint8" value="0"/>
<attribute id="Selectors" type="LSString" value="SelectSkills(6a4f8598-3a17-42d1-beac-d7ced582bafa,2);AddSpells(f8799582-ef4c-49b7-88eb-2120aecab569);SelectPassives(da3203d8-750a-4de1-b8eb-1eccfccddf46,1,FightingStyle);SelectAbilityBonus(b9149c8e-52c8-46e5-9cb6-fc39301c05fe,AbilityBonus,2,1)"/>
<attribute id="TableUUID" type="guid" value="29b323b1-d1fa-4882-a8f3-d4cb0ad6c158"/>
<attribute id="UUID" type="guid" value="9406edff-5e57-4eb6-ab0d-a2cc7d5d8b8a"/>
</node>
Pretty straightforward, right? Still, let's break it down:
- AllowImprovement
- Boosts - Any benefits your Class receives. Typically
Proficiency()
andResistance()
function calls, but pretty much anything that can work in a Status Boost field should work here. - Level - The level at which this progression is granted.
- Name - The internal identifier, typically the same as your Class'
Name
value. - ProgressionType - This is typically 0, 1, or 2. For the purposes class progressions, the value will usually be 0. 0 = Class, 1 = Subclass, and 2 = Race.
- Selectors - A semi-colon separated list of functions that allow you to have the user receive or select passives, spells, skills, ability bonuses, and more. We'll touch on this more when we get to the Passive & Spell Lists section.
- TableUUID -The Identifier for tis series of progressions - more in a bit.
- UUID - The Identifier for this specific Progression.
Having the above is great, but say you want to enable multiclassing for this class. It's fairly simple, but here's an example:
<node id="Progression">
<attribute id="AllowImprovement" type="bool" value="false"/>
<attribute id="Boosts" type="LSString" value="Proficiency(Greatswords);Proficiency(MediumArmor)"/>
<attribute id="IsMulticlass" type="bool" value="true"/>
<attribute id="Level" type="uint8" value="1"/>
<attribute id="Name" type="LSString" value="DeathKnight"/>
<attribute id="ProgressionType" type="uint8" value="0"/>
<attribute id="Selectors" type="LSString" value="AddSpells(f8799582-ef4c-49b7-88eb-2120aecab569);SelectPassives(da3203d8-750a-4de1-b8eb-1eccfccddf46,1,FightingStyle)"/>
<attribute id="TableUUID" type="guid" value="29b323b1-d1fa-4882-a8f3-d4cb0ad6c158"/>
<attribute id="UUID" type="guid" value="ae89fb12-155e-4ad9-bbf7-bfe67774d8e3"/>
</node>
We can see that this Progression node looks very similar to the above. We have a few less values in the Selectors
attribute, but otherwise, we just have a new attribute, IsMulticlass
. This being set to true flags to the game that "Hey, this class needs to be available as a selection when opting to multiclass."
Of course, any class is going to need more than just their level 1 progression. You'll want to have progression nodes at any level where the player should be getting a special bonus or mechanical change, like a new Feat, Action, or Spell.
So, the difference between TableUUID
and UUID
is important. Generally, when you're working with Classes, Races, and anything else with a progression, you want to reference the TableUUID
rather than the regular UUID. This is because the game reads each Node with an identical TableUUID
as part of a series, and as you can guess, a single Class or Race can have a lot of Progression Nodes related to them.
Once you've set up your Progressions the way you like, copy your TableUUID
field, and paste it into your ClassDescription
node's ProgressionTableUUID
attribute. Now you're ready to test.
- Pack Community Library and your Implementation Mod, then load it into your game.
- Start a new game
- Go to the Class Selection UI
You should now see your class at the bottom of the list of Classes. You may notice an issue though - the text says "Not Found." Let's fix that in the next section.
If you've read the guide to setting up a custom race, you likely already know what to do, so feel free to skip this section if so. Otherwise, let's get into it:
In your Implementation Mod, create a file, Localization/English/ModName.xml
. Paste in this boilerplate:
<contentList date="8/21/2023 5:00">
<content contentuid="" version="1"></content>
<content contentuid="" version="1"></content>
</contentList>
contentui
is the handle that the game reads to identify which line goes where. The text within each Content element is what will display on the screen. If nothing is defined for a given handle, you'll see "Not Found" in-game.
If you recall, way back in the beginning, when we were setting up our ClassDescriptions.lsx
entries, we generated a handle for DisplayName
and Description
. We're going to use those to make sure our text is readable:
- In
ClassDescriptions.lsx
, copy your Class'DisplayName
field's handle - In that first content element we made in the boilerplate, pasting in your
DisplayName
field's handle into thecontentuid
. - In between the opening and closing tags for your
content
element, input the name of your class. - Repeat the above steps for the
Description
field to the secondcontent
element, inputting the description for your class.
You're almost ready to test your Race again! Just one thing first: We need to convert our ModName.xml
into a .loca
file.
- Open LSLib and navigate to the Localization Tab
- Under
Input file path
, copy and paste the location and filename of yourModName.xml
file - Under
Output file path
, do the same, but replace.xml
with .loca` - Hit Convert
Note that this can all be done in the Community Library if you wish. You'll want to use CL_Classes.xml
if it exists.
Now you just need to package your Implementation Mod, and test again. You should see the text you'd expect to see!
We have our class, and we can see what's going on, but what makes it stand out? What powers and resistances does it have? We need to set those up, so let's switch back to our Community Library branch. Navigate to Public/CommunityLibrary/Stats/Generated/Data
. If you've checked out our guide to building a Race mod, you know this already, feel free to skip this section. The rest of you, let's take a look at the entry 1CL_MarchDead
in CL_Passives.txt
:
new entry "CL_MarchDead"
type "PassiveData"
data "DisplayName" "h613431bfg3cd7g4978g8c67g9d667817f3e4;4"
data "Description" "hc8b9a207gbb68g452cgaf74gbfb32c50c3b3;4"
data "Icon" "PassiveFeature_LandsStride_DifficultTerrain"
data "Properties" "Highlighted"
data "Boosts" "StatusImmunity(SG_DifficultTerrain)"
This is a fairly simple passive for the Death Knight class. We have localization handles for DisplayName and Description(we'll touch on that later), a defined Icon, properties set to Highlight the passive, and an effect granting Status Immunity to Difficult Terrain. Let's take a look at a slightly more complex one:
new entry "CL_UnholyWpn"
type "PassiveData"
data "DisplayName" "haecff2fbgf630g478cga6d5g0fe1e73b03e1;1"
data "Description" "hd22bba2egdc74g4118ga101g1176e4e6a351;1"
data "DescriptionParams" "DealDamage(WisdomModifier,Necrotic)"
data "Icon" "Mailem_DK_Unholyweapon"
data "Properties" "Highlighted"
data "StatsFunctorContext" "OnAttack"
data "Conditions" "not IsMiss() and IsMeleeWeaponAttack() "
data "StatsFunctors" "DealDamage(WisdomModifier,Necrotic,Magical)"
We can see a few interesting things going on here:
-
DescriptionParams
, this is information that is referenced in theDescription
Localization. Here we see that Necrotic Damage should be dealt based on the Wisdom Modifier. Sending the value and damage type to the localization string, where it is displayed in[1]
. Additional information can be added inDescriptionParams
with;
between each value. The order of how these are referenced is[1]
,[2]
,[3]
and so on. -
We have some lines titles
StatsFunctorContext
,Conditions
, andStatsFunctors
. Let's get into those a bit.
These lines identify the situations in which the Passive will trigger its StatsFunctors
value. However, they both look at very different things. StatsFunctorContext
identifies the specific event upon which the game should review the Conditions
to determine if StatsFunctors
fires. To put it plainly, StatsFunctorContext
is the event, Conditions
is the detail that must be true.
StatsFunctors
are effectively a function call. This can be used to Apply a status, Deal Damage, or any number of other things. It will always rely on a StatsFunctorContext
, and a Conditions
line can help flesh it out further, but is optional.
Soon we'll look at applying these to our class, but for now, we'll move on to Spells and Abilities.
If your class is a spellcasting class, you may want to define a few new spells. Or you may want some custom actions. If you only want to use already-existing spells and actions, feel free to skip ahead, otherwise, let's take a look at a spell:
new entry "CL_Target_WeaknessToFrost"
type "SpellData"
using "CL_Target_Weakness_BASE"
data "Level" "2"
data "Icon" "Status_Chilled"
data "DisplayName" "hf2534213g45afg49afg97d8g8a4c69077734"
data "Description" "hc98ee227g3b69g4792ga95eg8cc501160443"
data "SpellProperties" "ApplyStatus(CL_STATUS_WEAKNESS_TO_FROST, 100, 9)"
data "TooltipStatusApply" "ApplyStatus(CL_STATUS_WEAKNESS_TO_FROST, 100, 9)"
This is a fairly simple spell. From a quick read, we can tell quite a few things:
- It uses the data from a spell titled
CL_Target_Weakness_BASE
- It's a level 2 spell
- It applies a status of
CL_STATUS_WEAKNESS_TO_FROST
.
While we're here, let's take a look at adding new items. Maybe we want to go back and add a brand new item to the Class' equipment. Let's take a look at CL_Armor.txt
:
new entry "DK_StarterHelmet"
type "Armor"
using "_Head_Magic_Metal"
data "RootTemplate" "8888628e-4eda-4eb5-bfb4-eb828782b051"
data "ValueUUID" "a229f048-70b0-4b0c-88cb-29b5c6bdb2d0"
data "Rarity" "Uncommon"
data "Boosts" "RollBonus(SavingThrow, 1, Constitution)"
We can see here that we're defining the type of item, using the data of a pre-existing item, _Head_Magic_Metal as a base, defining a Root Template, a Value UUID, a level of Rarity, and special bonuses for having the item equipped.
This piece is held in the Community Library. Let's open up the Public/CommunityLibrary/Lists/
folder. Here there are two files: PassiveLists.lsx
and SpellLists.lsx
. Functionally, these lists are nearly identical. SpellList
nodes have an optional, but useful, Comment
attribute. Besides that, it works the same.
SpellList
Nodes have the following attributes:
-
Comment
- An optional field where you can place a description of what the node is for. -
Spells
- A semi-colon-separated list of Spells. -
UUID
- A UUID, which you'll have to generate when making new lists.
PassiveList
Nodes have the following attributes:
-
Passives
- A semi-colon separated list of Passives. -
UUID
- A UUID, which you'll have to generate when making new lists.
If you recall, in our Progressions section, we saw the Selectors
attribute. This is where the Spell and Passive Lists come in to play. The UUIDs being referenced in those attributes map to these lists' UUID
field. Once you've defined a Spell or Passive List, you'll want to go back to Progressions, and add these selectors via the AddSpells()
, SelectSpells()
, or SelectPassives()
function calls.
You may notice we don't specify an AddPassives()
function call - Progressions have an attribute you may remember, PassivesAdded
- here you'll want to copy any passives you wish to grant for a given progression in a semi-colon separated list.
// TODO: Go into detail on extra parameters useable in Selectors.
Once you've set up your Class Progression's Selectors
with your new Passive and/or Spell List information, package up your Community Library branch and your Implementation Mod, and test to see how your spells and passives are being granted.
Another Community Library Piece, the Custom Equipment file can be found near the Spells, Passives, and Statuses we went over earlier. Check in Public/CommunityLibrary/Stats/Generated/CL_Equipment.lsx
. This is a DataItem, a type of file commonly used for Spells, Passives, Items, and Statuses. Equipment is a different type of object. Let's take a look at an Equipment entry:
new equipment "EQP_CC_DeathKnight"
add initialweaponset "Melee"
add equipmentgroup
add equipment entry "WPN_Necrosword"
add equipmentgroup
add equipment entry "OBJ_Potion_Healing"
add equipmentgroup
add equipment entry "OBJ_Potion_Healing"
add equipmentgroup
add equipment entry "ARM_Boots_Leather"
add equipmentgroup
add equipment entry "ARM_ChainMail_Body"
add equipmentgroup
add equipment entry "DK_StarterHelmet"
add equipmentgroup
add equipment entry "OBJ_Scroll_Revivify"
add equipmentgroup
add equipment entry "ARM_Camp_Body"
add equipmentgroup
add equipment entry "ARM_Camp_Shoes"
add equipmentgroup
add equipment entry "OBJ_Keychain"
add equipmentgroup
add equipment entry "OBJ_Bag_AlchemyPouch"
add equipmentgroup
add equipment entry "OBJ_Backpack_CampSupplies"
Overall, fairly simple stuff. It defines the initial loadout for the player, including the base weapon type, armor, scrolls, containers, and potions. Once you've defined your equipment, make sure to go back to your ClassDescription
node and fill in the ClassEquipment
attribute with your new equipment's entry name.
By this point, you should have just about everything you need for your custom class. The next three sections are window-dressing - great to have, but not entirely needed for a 1.0 release. Still, let's keep on going.
Note: The ImprovedUI Mod has recently updated and simplified the process of adding Custom icons. I'll update this guide with a summary of the link, however follow this link for details!
// TODO
If you're trying to make a Custom Class with Subclasses, I've got some bad news for you: You'll need to repeat the above stages for your Subclass. You may already have guessed this, but it'll be a bit of effort. Regardless, whether you've finished that, or are just trying to add a Subclass to an existing Class, it's extremely easy to get things rolling.
You'll need to go to your Main Class' progression for the level at which your class will grant subclass options. If it doesn't already exist, add the following beneath the <attribute>
tags:
<children>
<node id="SubClasses">
<children>
</children>
</node>
</children>
Now inside the interior <children>
tags, paste the following:
<node id="SubClass">
<attribue id="Object" type="guid" value="your-subclass-class-description-uuid" />
</node>
Paste the value as your Subclasses UUID as defined in ClassDescriptions.lsx
.
This will be more of an issue if you're trying to add a subclass to an existing class, but good to be mindful of: Any mod that alters the same progression will conflict. Hard stop. There are ways around this:
- The Subclass Compatibility Framework - A project utilizing the Script Extender to insert any detected subclass mods at runtime.
- The BG3 Compatibility Patcher - A Python application that allows a user to patch together multiple mods affecting progressions. This doesn't always work as expected, but is a great option for those in a hurry.
If you'd like to rely on the patcher, you're good to go - compatibility will be up to users to set up. Otherwise, using the Compatibility Framework is a good approach, and the project maintains a guide on adding support for your mod.