Modding DevLog 1 - Health Per Level mod for Escape from Tarkov

Table of contents

No heading

No headings in the article.

I will be going over the process of creating my mod for the single-player version of Escape from Tarkov on the SPT-AKI single-player emulator, which managed to hit almost 5K downloads within a month, along with multiple contributors being interested in the mod. So, how did I manage to put this mod together that blew up even with such a small community?

Let's start with how this idea came to be in the first place. Tarkov claims to be an RPG game in which throughout your playthrough you level up and your character gets "stronger" the more you play. While this is the case, it felt rather underwhelming. Most bonuses are as small as single-digit percentage boosts which you can barely feel, so I decided to add a more tangible perk, like increased maximum health per character level.

I started by thinking about what data I need for this to work in the first place and it's rather simple; you need max-health and player level.

  private GlobalBodyParts: BodyPartsSettings;
  private PMCBodyParts: BodyPartsHealth;
  private SCAVBodyParts: BodyPartsHealth;
  private PMCLevel: number;
  private SCAVLevel: number;
  private logger: ILogger;

  postDBLoad(container: DependencyContainer): void {
    const dbServer = container
      .resolve<DatabaseServer>("DatabaseServer")
      .getTables().globals;
    this.GlobalBodyParts =
      dbServer.config.Health.ProfileHealthSettings.BodyPartsSettings;
  }

By digging into the games' source files, I found the server file which hosts the local server needed to run the game, and that same server file also happens to process your current accounts' information, which includes your character, its level and every other stat I needed.

I decided to separate the scav and pmc levels to prevent people from abusing free gear to grind levels and boost their pmc's health. Also added a new layer of depth to the mod.

Also added a very basic config setting in the file to allow people to change how much they want each value to increase.

  private IncreasePerLevel: { [key: string]: number } = {
    //Change the numbers here to change the increase in health per level.
    Chest: 2,
    Head: 2,
    LeftArm: 3,
    LeftLeg: 3,
    RightArm: 3,
    RightLeg: 3,
    Stomach: 2,
  };

  private BaseHealth: { [key: string]: number } = {
    //Change the numbers here to set the base health per body part.
    Chest: 85,
    Head: 35,
    LeftArm: 60,
    LeftLeg: 65,
    RightArm: 60,
    RightLeg: 65,
    Stomach: 70,
  };

Now that we have the player's account level and body part information, we can finally move on to our functions that are going to change the player's characters' maximum health for each body part. The function for this is rather simple, but it will make more sense when we get to the actual implementation.

  private calcPMCHealth(
    bodyPart: BodyPartsHealth,
    accountLevel: number,
    preset
  ) {
    for (let key in this.IncreasePerLevel) {
      bodyPart[key].Health.Maximum =
        preset[key] + (accountLevel - 1) * this.IncreasePerLevel[key];
    }
  }

  private calcSCAVHealth(
    bodyPart: BodyPartsHealth,
    accountLevel: number,
    preset
  ) {
    for (let key in this.IncreasePerLevel) {
      bodyPart[key].Health.Maximum =
        preset[key] + (accountLevel - 1) * this.IncreasePerLevel[key];
    }
    for (let key in this.IncreasePerLevel) {
      bodyPart[key].Health.Current =
        preset[key] + (accountLevel - 1) * this.IncreasePerLevel[key];
    }
  }

In simple terms, all it's really doing is matching the "key" with the body part and increasing the maximum health using the config we assigned. The "key" here is the body part. Soon, we will take the real-time information from the server as the game is running and edit the values accordingly.

But, how do we actually "inject" our code into a running game? Just creating a file with some code doesn't allow it to run real-time alongside the game. This is the part where we use the predisposed functions by the SPT-AKI team. The function that we need is the "StaticRouterModService". This allows us to read the router calls made by the server to implement our own "action", which in our case is my code.

  preAkiLoad(container: DependencyContainer): void {
    const staticRMS = container.resolve<StaticRouterModService>(
      "StaticRouterModService"
    );
    const pHelp = container.resolve<ProfileHelper>("ProfileHelper");
    this.logger = container.resolve<ILogger>("WinstonLogger");
    staticRMS.registerStaticRouter(
      "HealthPerLevel",
      [
        {
          url: "/client/game/start",
          action: (url: any, info: any, sessionID: any, output: any) => {
            try {
              this.PMCBodyParts =
                pHelp.getPmcProfile(sessionID).Health.BodyParts;
              this.PMCLevel = pHelp.getPmcProfile(sessionID).Info.Level;

              this.SCAVBodyParts =
                pHelp.getScavProfile(sessionID).Health.BodyParts;
              this.SCAVLevel = pHelp.getScavProfile(sessionID).Info.Level;

              this.calcPMCHealth(
                this.PMCBodyParts,
                this.PMCLevel,
                this.BaseHealth
              );
              this.calcSCAVHealth(
                this.SCAVBodyParts,
                this.SCAVLevel,
                this.BaseHealth
              );
            } catch (error) {
              this.logger.error(error.message);
            }
            return output;
          },
        },
        {
          url: "/client/items",
          action: (url: any, info: any, sessionID: any, output: any) => {
            try {
              this.PMCBodyParts =
                pHelp.getPmcProfile(sessionID).Health.BodyParts;
              this.PMCLevel = pHelp.getPmcProfile(sessionID).Info.Level;

              this.SCAVBodyParts =
                pHelp.getScavProfile(sessionID).Health.BodyParts;
              this.SCAVLevel = pHelp.getScavProfile(sessionID).Info.Level;

              this.calcPMCHealth(
                this.PMCBodyParts,
                this.PMCLevel,
                this.BaseHealth
              );
              this.calcSCAVHealth(
                this.SCAVBodyParts,
                this.SCAVLevel,
                this.BaseHealth
              );
            } catch (error) {
              this.logger.error(error.message);
            }
            return output;
          },
        },
      ],
      "aki"
    );
  }

Let's break down the main function of the mod, the reason why this mod exists in the first place, which is the "staticRMS" that we set up, 1 line after calling the preAkiLoad function.

This allows me to set up extra actions for router calls. I chose the "url: "/client/game/start"" and "url: "/client/items"" calls, which run my code every time the game starts and every time the player opens their item (stash) tab in the main menu.

Lastly, the rest of the code is rather simple. The action command provided by StaticRouterModService allows me to get the scav and pmc profiles of the player and later allows me to modify the values through the server which then updates the "dbServer" const I defined earlier, which holds the database server and overwrites the player's data file.

That pretty much concludes how I wrote the "Health Per Level" mod for Escape from Tarkov SPT-AKI version, the single-player emulator for the game, which turned out to grow to be one of the biggest mods on the website, especially when it was released. The combination of an interesting and unusual idea coupled with a hard but manageable implementation.

If you are interested in checking the mod or its repository on GitHub, feel free to check them out from the links. If you are interested in my work, feel free to check out my GitHub profile or my Steam workshop where I have many more mods for games.