Bloodworks - Modding Guide

Before Starting

This file is kinda big and will get even bigger as time goes on. I splitted things into collapsable articles to organize things a little better. If you want to expand everyhing, you can use this button




Modding in Bloodworks

Modding support is one of the priorities in Bloodworks. Game engine is written in C++ for performance (and me being more familiar) reasons but the game content mostly runs on Lua side. Each moddable game object (Guns, monsters, missions etc) is loaded using a json file (which initializes some static variables related to object).

Lets start with an example:

Simple Example : Pistol

Each game entity template has a json file that defines the template. The first thing game does is searching for all json files under resources folder and interpreting/loading them one by one. Pistol is defined under "resources/guns/pistol/data.json".
{
  "type" : "gun",
  
  "name": "Pistol",
  "icon" : "icon.png",
  
  "bulletTexture" : "bullet.png",
  "bulletSize" : [8.0, 8.0],
  "bulletMeshShift" : [0.0, 0.0],
  "bulletRadius" : 2.0,
  "bulletSpeed" : 850.0,
  "bulletDamage" : [50, 90],
  
  "crosshairDistance" : 350.0,
  "maxAmmo" : 12,
  "reloadTime" : 1.2,
  
  "firingSound" : "pistol.ogg",
  
  "scriptFile" : "pistol.lua",
  "scriptName" : "Pistol"
}

Each json file must has an entry named "type" which represents the type of the entity template that is being loaded. In this case we are loading a gun. All other entries in this example are specific to a gun. For example first two entries (name and icon) is used for displaying the gun on various GUI elements. And after that we have a bunch of stuff that makes this gun different than others.

One of the most important entries here are scriptFile and scriptName. scriptFile is path to lua script that contains the trigers for this gun and scriptName is the name of the lua object that contains trigers for our gun. Once the engine sees the entry "scriptFile", it will load the lua script and it will link the gun on c++ side to scriptTable that has been just loaded. Let's see what is inside pistol.lua.

function Pistol.init(gun)
    ShootTimer.initGun(gun, 0.35)
    
    SpreadHelper.initGun(gun)
    gun.data.minSpread = 0.0
    gun.data.maxSpread = 0.10
    gun.data.spreadDecreaseSpeed = 0.25
    gun.data.spreadIncreasePerShoot = 0.02
end


function Pistol.onTick(gun)
    SpreadHelper.onTick(gun)
    if gun.isTriggered and gun:hasAmmo() then
        if ShootTimer.checkGun(gun) then
            gun:consumeAmmo()
            SpreadHelper.onShoot(gun)
            local bullet = gun:addBullet()
            local particle = bullet:addTrailParticle("BulletTrailParticle", Vec2.new(0.0, 14.0), 15.0, {})
            particle.args.initialScale = 2.0
            particle.args.fadeOutSpeed = 1.2
            particle.args.color = Vec3.new(0.8, 0.8, 0.8)
        end
    end
end

An here we have a bunch of trigers that has been attached to "Pistol" (which is scriptName that is defined in json). We have init and onTick trigers for our gun. Notice that each triger has a parameter named "gun". This is the lua object that the trigers should use to change or get state of the gun.

As the name suggests, init is the triger that is called once (per mission) which is basically initializes the gun. ShootTimer and SpreadHelper are two helper objects that is used in guns (their sources are in resources/guns/helpers.lua). ShootTimer basically sets the minimum interval between each firing of gun and SpreadHelper is used creating a dynamic spread (which gets larger as you fire the gun and lower as it gets cooldown). Here we set a 0.35 second delay between shoots (so ~3 shoots per second) and initialize some variables for our gun's spread.

Notice that the parameters that SpreadHelper are stored inside gun.data. Each gun (and other game entities) has a lua table named "data" in their lua objects. This table can be used for storing any custom variable the gun needs. SpreadHelper has a bunch of variables it initilizes on "data" and we are customizing these variables after the initialization. In this example we are setting min and max spread to 0 and 0.1 (which are angles in radian, so Pistol has 0 degree spread when it is perfectly cooled down and 18 degree spread once you fire your pistol a bunch of times). After setting min and max spread, it sets the spread decrease rate as time goes on (spreadDecreaseSpeed, gun reduces spread by 0.25 per second) and how larger the spread gets after every shoot (spreadIncreasePerShoot, 0.02 radian per shoot)


Next triger in for our gun is onTick. As the name suggests, this one is called on every game tick.

First thing the gun does here is ticking the spread, which is a helper function on SpreadHelper that cooldowns the gun and reduces its spread. After that we have an if statement gun.isTriggered and gun:hasAmmo()

Remember that I said the trigers access the gun's state using the parameter named "gun"? This is an example. Here we are checking if gun is trigered (ie. player wants to shoot it) and we call a function to check if we have enough to fire. If both of those are true, we want to create a bullet for our gun. But wait, we also have a ShootTimer to control our shooting rate of the gun. ShootTimer.checkGun checks if the gun ready and returns true if it is.

If all those conditions are true, we can create a new bullet for our gun. We already defined our bullet details in json file so they are not necessarry to define here (allthough we can override them here if we want.). First, the triger calls gun:consumeAmmo() to remove one bullet from our gun. (We also defined number of bullets per clip and reloading duration, so gun will be reloaded when we use last bullet and gun:hasAmmo() will return false until it is done).

And in our next statement, finally we create a bullet for our gun! All bullet details are given in the json. But here we want to customize our bullet a little by adding a trail particle to it. gun:addBullet() returns a bullet object so we can change the state of the bullet. Here we set this object to local variable named "bullet" and call addTrailParticle function to attach a particle effect to this bullet. "BulletTrailParticle" is name of the particle effect we want to attach and it is defined in some other json file (and controlled by lua & shader files). We pass some arguments to addTrailParticle (first one defines local shift for particle position and second one is distance between each particle that will be created as bullet moves, last one (empty table) is a parameter that is sent to particle's lua script) and tweak the particle that the function returns.

That is all for our simples gun!

Pistol is just one implementation of a gun entity template. Let's see what are the types of entities

Game Entity Types

We made our introduction with a gun. But we have a few other different type of game entities that we can define templates for. "type"s that we can define are:

"mission" : mission template, which basically just points to a lua file that contains script for mission
"gun" : gun templates
"monster" : monster templates
"game_object_template" : game object template. game objects are higher level constructs that is not specialized in anything and can be used for implementing more complex objects
"bonus" : bonuses that randomly spawns around the map
"perk" : perks that you gain after a level up
"particle" : particle template that can be attached to variable in game objects (bullets, monsters, gameobject)
"animation_template" : template for "animations" which are basically a bunch of png files bundled together to create a frame based animation
"json_list" : a list of json elements that represents different game object templates, each element will be loaded seperately as if they are in a different json file
                           

And let's discuss each item seperately in further articles.

Missions

...

Guns

Guns are the weapons that the player use. Each gun has a json object that defines some basic properties of the guns, a script table to hold its trigers and a lua object to control the state of the gun.

Json File Entries

Each json file that defines a gun must have "type" : "gun" defined. Other than that there are many other things that you can define in json file to specialize a gun. Some of them are mandatory and others have default values. Most of these things can be overridden in script file if necessarry. The things that can define on json file are:

    "name" : Name of the gun (mandatory)
    "scriptFile" : Path to lua file that contain the script for the gun (mandatory)
    "scriptName" : Name of the lua object that contains gun's trigers (mandatory)
    "icon" : Icon of the gun that is displayed on gui or on random spawns (mandatory)

    "ultimate" : A gun can be defined as an "ultimate", in which case they are bound to secondary mouse button and have limited ammo
    "hideSpread" : If you don't want to show the circle that represents gun spread, set to true

    "showShootAnimation" : Set to false if you want to hide gun muzzle animation
    "shootParticleColor" : Color of the gun muzzle animation

    "bulletTexture" : Path to a texture that will be used for bullets of this gun 
    "bulletMeshShift" : If your texture is not cented, use this to shift its center relative to bullet position
    "bulletSize" : Size of the bullet texture

    "bulletSpeed" : Speed of the bullets
    "bulletDamage" : Min and max damage of bullets
    "bulletRadius" : Hit radius for bullets

    "maxAmmo" : Ammo count for gun for a full clip (or total ammo count for ultimates)
    "reloadTime" : Time to reload a gun (or time between ultimate shoots)
    "bulletLifeTime" : Life time of bullets, can be set to remove bullets after a duration they are created

    "crosshairDistance" : Maximum crosshair distance for the gun 
    "spawnChance" : Random spawn chance of this gun in mission

    "firingSound" : Path to sound file that will be played on each trigger
    "maxSoundPlayInterval" : An interval between playing sepearte firingSounds of this gun
    "firingSoundContinuous" : Set to true if firingSound is not something trigerred for each bullet but a continous looping sound file that plays as long as the gun is trigerred
    "firingSoundFadein" : Fade in value for continous firing sounds
    "firingSoundFadeout" : Fade out value for continous firing sounds

    "bulletHitSound" : Path to sound file that will be played when the bullet hits something
    "reloadBeginSound" : Can be set to customize reloading sound for guns
    "reloadEndSound" : Can be set to customize the sound that plays at the end of the reload

    "isLaser" : set to true if gun has a laser texture, details of the laser texture can be specified by:
        "laserThickness"
        "laserBeginWidth"
        "laserEndWidth"
        "laserBeginShift"
        "laserEndShift"
        "laserTexture"
        "laserShader"

Avaiable Trigers for Guns

Each gun has a lua script file that contains the triggers for the gun. Possible trigers are:

init : called once for each gun, use it to initialize stuff (mandatory)
onTick : called once every game tick
onBulletHit : called when a bullet of this gun hits something
onReloadStart : called when gun starts reloading
onReloadEnded : called when gun finishes reloading
onPlayerDamaged : called when the player is damaged (guns are able to prevent damage if wanted)
Lua Object For Guns

Each gun have a lua object that contains the data and current state of the gun. This object has some variables about the gun (some of them are readonly, some of them already defined in json but can be altered with trigers) and some methods to do stuff with the gun. These methods and variables are:

"id": (read only) unique integer for gun
"name": (read only) name of the gun

"bulletSpeed": same as the one in json file
"bulletRadius": same as the one in json file
"bulletSpeed": same as the one in json file
"bulletLifeTime": same as the one in json file
"showShootAnimation": same as the one in json file
"shootParticleColor": same as the one in json file
"firingSoundFadein": same as the one in json file
"firingSoundFadeout": same as the one in json file
"firingCurVolume":same as the one in json file
"playFiringSound":same as the one in json file
"crosshairDistance": same as the one in json file

"scriptTable": table that contains trigers for gun 

"data": a lua table that can store anything necessarry for gun to work

"spreadAngle": current spread of the gun in angles, bullet direction will be randomized within this threshold

"isTriggered": is set to true when the gun is trigerred and the player wants to shoot bullet

"getRandomDamage": returns a random damage value for gun

"hasAmmo": returns true if gun currently has ammo
"addAmmo": adds ammo to gun
"consumeAmmo": consumes ammo from the gun
"reload": manually relad gun
"isReloading": returns true if gun is reloading
"getReloadPercentage": percentage of the reloading process
"getMaxAmmo": maximum ammo that the gun can have
"getCurrentAmmo": current ammo the gun have
"reloadSpeedMultiplier": a multiplier for gun's reload speed

"addBullet": adds a bullet to a gun and returns the bullet object
"laser": renderable object of the gun's laser

Monsters

...

Game Objects

...

Bonuses

...

Perks

...

Particles

...

Animations

...

How lua side of Bloodworks... works

Game loop of Bloodworks runs on C++ side. In this part it renders stuff, does player controls, ticks monsters, guns, calls necessarry trigers etc etc. On lua side, we have scriptTables that contains trigers for game entities. For each instance of object (monster/gun etc) we have a lua object with some variables and methods to link it to its c++ counterpart and we have a scriptTable to contain all trigers for this object's template.

Other than these objects and scriptTables, we have a bunch of public methods on lua side to alter game/mission state or get some useful information about our current state. For example "addCustomBullet" adds a bullet that is not linked to any gun. "getMonsterCount" returns number of monsters. There are quite a few function like that and they will be further explained.

Pretty much everything that can be accessible by lua side are defined in BloodworksLuaWorld.cpp. I will try to explain things here but check that file if you want a more complete list

Uploading Mods

Mods can be shared on Steam Works by using mod_helper/upload_mod.exe. Once your mod is ready to publish, copy its folder to mod_helper folder. You will also need 'steam_mod_info.json' and an image file to upload your mod.

Here is what an example json file looks like

{
  "title" : "Nerf Gun",
  "description" : "For hardcore players",
  "picture" : "icon.png",
  
  "meta" : "",
  
  "tags" : [ "weapon" ],
  
  "change_note" : "initial upload"
}
                                    

To upload a mod, simply run executable in mod_helper and it will ask you to enter a folder. Once you do that it will start uploading process. On your first upload, it might ask you to agree Steam Works agreement, if you don't do that, your mods will not be visible to public.

When you upload a mod for first time, it will create appid.txt in your mod folder which contains id for that specific mod. When you re-upload the same mod, it will replace the existing one instead.

Warnings

Each mod should have script name (which will be used for creating its lua object). For example there is already a script named 'Pistol', if you make a mod with same script name, your mod (or the existing pistol) will not work. I will try to address this issue in future and at least give a warning. Try not to pollute global lua space unless you have to and keep your script name unique.