Soulash 2

Soulash 2

Not enough ratings
Soulash 2 - Modding a new race
By Draken
So this guide will be about modding a brand new race from the scratch.
The race will be available without a civilisation (because that complicates things, and it can be added later!) - playable races without civilisation can start in ANY of the cities of other races.
3
   
Award
Favorite
Favorited
Unfavorite
Introduction to Modding
Modding in Soulash 2 is relatively simple and limited - it doesn't require you to know how to 'code', but would be nice to have a bit of know-how with graphics editing and text editor of your choice.

Required for this excerise are:
- any graphics editor, especially one that handles layering and grid. The preferred one in Soulash is Aseprite, as it can exports layers in the format that soulash can read by default.
- any text editor, like notepad++ or visual studio code. Text editors that got build-in json validators are best here, otherwise if you're not familiar with json format you'd need to use external json validators to find any errors in your files.

OTHER MODDING TOPICS:
If you're interested, there is also a entire playlist related to modding different things on game's Author youtube channel, available here:
https://www.youtube.com/playlist?list=PL0hKsMGYqPsWG1vvEGlFPbcl1AOd7-qfm

WARNING:
The guide is text-heavy, as it also tries to explain why we are doing certain actions.
On positive side; its text-heavy, so you can copy a lot of stuff directly and don't need to write it down and fear the typos, or missing brackets.
Creating our mod folder and required files
So lets start with making a new folder for our mod - navigate to:
\steamapps\common\Soulash 2\data\mods

And create a new folder for your mod.
Once there, create a file and name it:
mod.json

Paste this content into it:
{ "author": "You", "description": "Adds a new race <insert race name> to the game.", "game_required": "1.0.0", "icon": "icon.png", "mods_required": [ "core_2" ], "name": "Your race", "thumbnail": "thumbnail.png", "version": "1.0.0" }

Be sure to change author, description and name fields to however you like them to be.
Also, icon field references the icon that is showing in the mods list in-game (usually something small, like 20x20 pixels will do).

This single mod.json file will be responsible for allowing us to select our mod within the game.
Creating our 'custom' race
So, lets add our 'custom' race.
This is done in character.json file, so lets create one now.

Post the content there:
{ "base_abilities":[ ], "base_recipes":[ ], "races":[ { "ages": { "adult": 50, "child": 14, "elder": 70 }, "description": "Your race description goes here.\n[color=244,247,118,255]Endurance +1, Willpower +1[/color]", "id": "your_race_id", "image": 10, "items": [], "name": "your_race_name", "names": { "female": "npc/names/human_female.txt", "male": "npc/names/human_male.txt" }, "orphan_surname": "from your imagination", "passives": [], "playable": true, "recipes": [], "statistics": { "willpower": 1, "endurance": 1 }, "tags": [ "1", ] }, ] }

I'll explain the more important elements:
  • ages - the ages (in years) for the member of the race to be considered as an adult, child or elder.
  • id - very important, its how the game will internally call your race, and its also used in all other places to reference to your race! Should be in readable format, good examples would be something like: "your_nick_race_name" which will make it unique, for example I use: "draken_undead" in my undead race mod!
  • name - how the race will be referenced in UI or in dialogues.
  • description - the description showing during player creation. You can use \n to move your text to new line, and html-like tags like [color=244,247,118,255] some text [/color] to color the text in between to whatever you want (the numbers refer to Red/Green/Blue values, and the last is alpha which should always be 255).
  • statistics - which statistics the race gets bonus to, available are all main stats (strength, dexterity, intelligence, willpower, endurance) and hearing, sneaking, magic_power, health_regen and health_regen_tick.
  • passives - If the race should have any extra passive available to them, vanilla example is Reptiles which can fish without a fishing road. The vanilla passives are located within the mods/core_2/passives.json file, and we can use it by just referencing its id for example: "passives": [ "core_2_parry_mastery" ] would grant our race the passive 'Parry Mastery' that gives it +2 parry if it uses a sword.
  • names list - the path to your racial names. You can copy them directly from core_2/npc/names and just edit, or write one from the scratch. These names are picked when we hit 'random name' button in character creation.

At this point, we can just load our mod and should be able to play our race within the game - we need to enable our mod inside the game 'mods':


And our race shows up in game - able to be picked:


Obviously, we didn't add any graphics for it yet so it looks kind of blank.
Let's fix that in next part!
Layers, Groups, Restrictions and how it all works
How Character Portraits Are Rendered (Sprites System Overview)
Portraits in the game are built from multiple sprites, stacked in a specific order - just like layers in Photoshop.

Layer Groups"
The game organizes these sprites into layer groups.
Each group represents a category (like eyes, mouth, or clothes), and only one sprite from each group can be active at a time.

Sprites are drawn in a fixed order, from 0_Background to 28_Hair_ADD.

The number is the draw order (from 0 to 28):
  • 0_Background
  • 1_Hair_BACK_TIED
  • 2_Hair_BACK_SHORT
  • 3_Hair_BACK_MEDIUM
  • 4_Hair_BACK_LONG
  • 5_Base_body
  • 6_Base_face
  • 7_Hair_base_SHORT
  • 8_Hair_base_RANDOM
  • 9_Ears
  • 11_Eyes_background
  • 13_Nose
  • 15_Eyes
  • 16_Brows
  • 17_Mouths
  • 19_Clothes_beneath
  • 20_Accessories
  • 21_Clothes_top
  • 22_Beard
  • 23_Mustache
  • 24_Hair_TOP_SHORT
  • 25_Hair_TOP_LONG
  • 26_Hair_TOP_fringe
  • 27_Hair_TOP_mohawk
  • 28_Hair_ADD

Layers:
Each sprite inside a layer group is called a layer.
For example: If there are 4 different short hairstyles in group 24 (Hair_TOP_SHORT), each one is a separate layer.

Restrictions:
Restrictions control when and how layers appear. They can:
  • Limit layers to specific characters (e.g., enemies or player portraits)
  • Apply tints or color changes
  • Be restricted by gender, race, or other character traits
Adding our first portrait
The portraits in game comes in 52x52 pixels format.
So lets create a new folder within our mod folder - we'll call it 'assets'.

So lets add a fitting portrait first for our race, I've picked my recently favorite avatar and decided to make it an in-game race:


I've called the file portrait_parts_new.png and put that in our assets folder.

Next, we need to create an assets.json file within our mod main folder, and paste this code inside:
{ "graphics": { "tilesheets": [ { "name": "portraits", "tiles": [1, 1], "file": "assets/portraits_parts_new.png" } ] } }

The file should reference your .png file path relative from the mod main folder.
This single entry would make the game load our portrait parts file for our mod purposes.

Next we need to create a folder called 'portraits'.
Our folder structure should look like this:


Next we would need to create a 'portrait_parts.json' file in our portraits folder.

If you're using Aseprite to draw your sprites, you can alternatively directly export it:
  • Select all the layers (shift+click, they need to be highlated)
  • File -> Export -> Export sprite sheet
  • In the layout section - "Sheet Type": Packed, Constraints: None.
  • In the Sprite section choose Source: Sprite, Layers: Selected layers, Frames: All frames. Leave the Split layers checked.
  • In the borders section, leave everything at 0 and do not select anything.
  • In the Output section, Output File and JSON Data should be selected.

The inside of files should look like this:
{ "frames": { "portrait_parts_new (body).png": { "frame": { "x": 0, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 } }, "meta": { "app": "https://www.aseprite.org/", "version": "1.3.8.1-x64", "image": "portrait_parts_new-sheet.png", "format": "RGBA8888", "size": { "w": 52, "h": 52 }, "scale": "1", "frameTags": [ ], "layers": [ { "name": "body", "opacity": 255, "blendMode": "normal" } ], "slices": [ ] } }

It may look scary at first, but there are just two important things here:
  • frames - especially x and y, depicts the position in pixels of given image. The counting starts from bottom left, and goes to top right.
  • layers - this is important, as it basically assigns your image to specific in-game layer (more of that later on) and assign its a unique name.

For our single graphics, we don't need to bother too much.
Let's just change the layers to this:
"layers": [ { "name": "5_Base_body" }, { "name": "body", "group": "5_Base_body", "opacity": 255, "blendMode": "normal" } ],

What does this do:
This will assign our first portrat parts, starting at pixel position of x:0 y:0 and ending at pixel x:52 and y:52 - to layer named "body", and will asign our layer to layer group "5_Base_body".

Now, if you start the game, select your mod to be used in mods, and just go to map/character generation, it should automatically generate a 'restrictions.json' file within your portraits.

It will look like this:
{ "colors": null, "restrictions": { "body": [], } }

Lets change it to something like this:
{ "restrictions": { "body": [ { "races": [ "your_race_id" ] } ], } }

If you changed your race id in previous step to something else, be sure to put it instead of "your_race_id" in the example!

This will mean that our image (called "body") will be available to races that has specific id.
No it doesn't work
And the result is not what we anticipated, right?

Chances are you'll see something like this, or black screen:


The game doesn't really anticipate the PC portrait to be a single static image, so lets add a few more layers belonging to different layer groups:


Here is our new portrait parts json file:
{ "frames": { "portrait_parts_new (background_1).ase": { "frame": { "x": 0, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (body_1).ase": { "frame": { "x": 52, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (eyes_2_female).ase": { "frame": { "x": 104, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (eyes_1).ase": { "frame": { "x": 0, "y": 52, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (clothes_top_1).ase": { "frame": { "x": 52, "y": 52, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 } }, "meta": { "app": "https://www.aseprite.org/", "version": "1.3.8.1-x64", "image": "portrait_parts_new-sheet.png", "format": "RGBA8888", "size": { "w": 156, "h": 104 }, "scale": "1", "frameTags": [ ], "layers": [ { "name": "0_Background" }, { "name": "background_1", "group": "0_Background", "opacity": 255, "blendMode": "normal" }, { "name": "5_Base_body" }, { "name": "body_1", "group": "5_Base_body", "opacity": 255, "blendMode": "normal" }, { "name": "15_Eyes" }, { "name": "eyes_2_female", "group": "15_Eyes", "opacity": 255, "blendMode": "normal" }, { "name": "eyes_1", "group": "15_Eyes", "opacity": 255, "blendMode": "normal" }, { "name": "21_Clothes_top" }, { "name": "clothes_top_1", "group": "21_Clothes_top", "opacity": 255, "blendMode": "normal" } ], "slices": [ ] } }

Lo and behold, finally our character looks quite decent:


But, without restrictions to our race, humans also picked this extremely stylish tie to wear:


Indeed, our restrictions doesn't apply to only our race - an entry without any restrictions will be available to ALL races:
{ "colors": null, "restrictions": { "background_1": [], "body_1": [ { "races": [ "your_race_id" ] } ], "clothes_top_1": [], "eyes_1": [], "eyes_2_female": [] } }

Let's change it, so only our awesome custom race has all of our goodies:
So now our restrictions look like this - lets restrict one eyes to female only by adding "sex": 2 to it.
(0 is unisex, 1 is male only, 2 is female only)
{ "colors": null, "restrictions": { "background_1": [{ "races": ["your_race_id"] }], "body_1": [{ "races": ["your_race_id"] }], "clothes_top_1": [{ "races": ["your_race_id"] }], "eyes_1": [{ "races": ["your_race_id"] }], "eyes_2_female": [{ "races": ["your_race_id"], "sex": 2 }] } }

It's done, and now only our awesome race can wear awesome tie.
But for some reason, eyes are not showing up as viable choice!

Fixing Eyes
So, eyes to render actually needs eyes background (with a piece assigned), and probably also a face group.

We also would like to add some a 'empty' pieces, for stuff like eyes and clothing, so we could actually not end up with every member of our race wearing the super duper cool tie.

So here is the new parts file - you can see it contains a bunch of empty spaces:


Here is our updated portrait parts json (layer file):
{ "frames": { "portrait_parts_new (background_2).ase": { "frame": { "x": 0, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (background_1).ase": { "frame": { "x": 52, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (body_1).ase": { "frame": { "x": 104, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (face_wrinkled).ase": { "frame": { "x": 156, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (face_empty).ase": { "frame": { "x": 208, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (eyes_bg_empty).ase": { "frame": { "x": 260, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (eyes_2_female).ase": { "frame": { "x": 312, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (eyes_1).ase": { "frame": { "x": 364, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (clothes_top_1).ase": { "frame": { "x": 416, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 }, "portrait_parts_new (clothes_empty).ase": { "frame": { "x": 468, "y": 0, "w": 52, "h": 52 }, "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 52, "h": 52 }, "sourceSize": { "w": 52, "h": 52 }, "duration": 100 } }, "meta": { "app": "https://www.aseprite.org/", "version": "1.3.8.1-x64", "image": "portrait_parts_new-sheet.png", "format": "RGBA8888", "size": { "w": 520, "h": 52 }, "scale": "1", "frameTags": [ ], "layers": [ { "name": "0_Background" }, { "name": "background_2", "group": "0_Background", "opacity": 255, "blendMode": "normal" }, { "name": "background_1", "group": "0_Background", "opacity": 255, "blendMode": "normal" }, { "name": "5_Base_body" }, { "name": "body_1", "group": "5_Base_body", "opacity": 255, "blendMode": "normal" }, { "name": "6_Base_face" }, { "name": "face_wrinkled", "group": "6_Base_face", "opacity": 92, "blendMode": "normal" }, { "name": "face_empty", "group": "6_Base_face", "opacity": 255, "blendMode": "normal" }, { "name": "11_Eyes_background" }, { "name": "eyes_bg_empty", "group": "11_Eyes_background", "opacity": 255, "blendMode": "normal" }, { "name": "15_Eyes" }, { "name": "eyes_2_female", "group": "15_Eyes", "opacity": 255, "blendMode": "normal" }, { "name": "eyes_1", "group": "15_Eyes", "opacity": 255, "blendMode": "normal" }, { "name": "21_Clothes_top" }, { "name": "clothes_top_1", "group": "21_Clothes_top", "opacity": 255, "blendMode": "normal" }, { "name": "clothes_empty", "group": "21_Clothes_top", "opacity": 255, "blendMode": "normal" } ], "slices": [ ] } }

And updated restrictions file:
{ "colors": null, "restrictions": { "background_1": [ { "races": [ "your_race_id" ] } ], "background_2": [], "body_1": [ { "races": [ "your_race_id" ] } ], "clothes_empty": [ { "races": [ "your_race_id" ] } ], "clothes_top_1": [ { "races": [ "your_race_id" ] } ], "eyes_1": [ { "races": [ "your_race_id" ] } ], "eyes_2_female": [ { "races": [ "your_race_id" ], "sex": 2 } ], "eyes_bg_empty": [ { "races": [ "your_race_id" ] } ], "face_empty": [ { "races": [ "your_race_id" ] } ], "face_wrinkled": [ { "races": [ "your_race_id" ] } ] } }

And the end result - looks pretty charming if I say so myself:


Few just pieces set with proper layers allow to have different combinations of visuals for our race, like this.


Imagine what would happend if our race actually had like 3-4 different faces, eyes, clothing and backgrounds and an author had actual drawing skills?
Colors/Tinting
Restrictions can also define colors lists, which keeps RGB values for specific colors, like this:
"beholder_skin_color": [ [245,66,66 ], [245,230,66 ], [99,245,66 ], [66,245,188 ], [66,117,245 ], [188,66,245 ], [245,66,108 ] ], }

Colors can be applied to some of the layers, to enable tinting them - this is also the reason why a lot of vanilla pieces are 'whitewashed' - so the color tint looks better.

This is usually restricted to clothing, eyes, hair and body colors though, and may not work good (or at all), or be pickable for other groups.

You can assign color to specific piece like this within restrictions.json file:
[...] "body_1": [ { "colors": "beholder_skin_color", "races": [ "your_custom_race" ] } ], [...]

End result:


Of course, if our 'eyes' were separate layer and not part of 5_Base_body layer group, or the base body was greyscaled instead of being green, the final effect will be way better.
Game freezes on world map after we resign
So, there is just one problem with our custom race.

And this is that once we resign our custom race people, without a default racial entities, it will break the save when our next character goes to the world map.

This is because the game tries to find race entity based on our custom race age, and if it cannot find one, it just stucks.

So, we need to add one more thing to our character.json file within our race definition:
"base_entities": { "adult": "694", "child": "2000", "leader": "2203", "recruit": "691" },

This will make it that once our character retires, it will pick up the occupation from entity 694 - Human Adult. Meaning the dialogue options, etc. will be as that of human.

You can use different races occupations (all the vanilla races are defined just like ours in core_2 mods folder, within character.json) if you want to choose different ones dialogues.

As for custom dialogues/occupations, it's out of scope of this basic guide, but it would require to:
- Use modding tools to create a new occupation for your race (you can create new entity, copy human adult, and just tweak values)
- Writing custom dialogues in conversations.json for our new entity id.

Ending Words
And that's all.

The race is there, it's playable, can slay princesses and rescue dragons - a race without a civilisation can start within any starting towns.

The part 2 guide will be about making a civilisation out of our little Arturkins.
It's endevour that is both more complicated, and labour intensive then simply adding a race.

Thanks for reading!
13 Comments
Draken  [author] 16 May @ 1:53am 
@shadow-Fox Hard to say without looking at the mod files.
If you're on discord, you can tag me in modding channel and I can try to help.
Shadow-Fox 7 Mar @ 9:26am 
"This will make it that once our character retires, it will pick up the occupation from entity 694 - Human Adult. Meaning the dialogue options, etc. will be as that of human."

It causes the NPC to be non-interactable! What have I done wrong?
Fediani4 20 Nov, 2024 @ 4:24am 
An amazing guide. I am sort of spriter, but nowhere near coding bits. This will help me plenty to get somewhere modding-wise.
The layer part is the only thing that confused me, but Guess bashing on it would lead to something.
Big Boss 14 Sep, 2024 @ 4:13pm 
I see, I'll give that safter route a shot then. Many thanks for the advice, Draken. You have a great day.
Draken  [author] 14 Sep, 2024 @ 5:10am 
Best (from the perspective of safety) way would be to just copy the existing human portrait parts, make new layers (with new names) out of them and assign them to your new race (or sub-race, you can look at Rasimi in core game files how it's set up for subraces).

People were brute forcing stuff like that by copying portraits, portraits.json and restrictions.json from core game, and adding custom race to existing restrictions - this approach breaks game upon any portraits update in core2 though.

Optimal solution would be to only override restrictions.json file with existing layers, but currently it's not possible - restrictions.json in mod requires also layers defined in portrat_parts, and that requires a .png file.
Big Boss 14 Sep, 2024 @ 4:46am 
This is an awesome guide. I wanted to ask, is it possible to use portraits already existing in game for a new race? Say if I wanted to make a variant of human that looks like any other humans, how would I make that happen?
corruptorv1 5 Sep, 2024 @ 1:56pm 
Wow this is great. You are always making great stuff. I give you a like.
Devox 3 Sep, 2024 @ 11:16am 
Thank you very much, Draken.
Draken  [author] 3 Sep, 2024 @ 11:00am 
For multiple passives, just put in the multiple passive ids one after another, like this:
"passives": [
"core_2_infravision", "core_2_some_other_id"
],
Draken  [author] 3 Sep, 2024 @ 10:59am 
Hey!
To add passives you would need to edit the character.json, and add passive id to the race passives list.
For example, this is how it looks currently for dwarves:
"passives": [
"core_2_infravision"
],

All the passive ids are located in:
\Steam\steamapps\common\Soulash 2\data\mods\core_2\passives.json