Modding Custom Battles

Finding The Right Files

Look in limbus_data at the following folders, primarily the three bolded ones

  • "battle-ab"
  • "battle-dungeon"
  • "battle-exp-dungeon"
  • "battle-mirrordungeon"
  • "battle-railway-dungeon"
  • "battle-story"
  • "battle-thread-dungeon"

Getting Started

Once again I recommend grabbing the template Encounter I included here and tinkering with it to accomplish what you want. Generally, we can simply overwrite Railway Battles, Story Battles or Mirror Dungeon battles if we want to

Important Note: This is the one exception that I was talking about when it comes to only working in custom_limbus_data and custom_limbus_locale. If we put a battle file directly to BepInEx/plugins/Lethe/encounter.json Lethe will recognize it.

  • If you go to the story drive and scroll down the option to play this custom encounter is then available, and it updates whenever any changes is made to the file. This is the preferable way of playing a custom battle.

  • Another note is that it only recognizes this file if it is called encounter and placed within the config file, otherwise it won't load.

{
  "id": 10532,
  "stageLevel": 40,
  "stageType": "Abnormality",
  "isBatonPassOn": false,
  "includeBoss": 1,
  "participantInfo": {
    "min": 6,
    "max": 6
  },
  "waveList": [
    {
      "battleMapInfo": {
        "mapName": "Cp5_DrillingVessel_Outside",
        "mapSize": 28
      },
      "unitList": [
        {
          "unitID": 8097,
          "unitCount": 2,
          "unitLevel": 40
        },
        {
          "unitID": 8098,
          "unitCount": 3,
          "unitLevel": 40
        }
      ],
      "bgmList": [
        "Battle_Cp5_MiddleBossBattle"
      ],
      "allyPositionID": 48,
      "enemyPositionID": 49
    },
    {
      "battleMapInfo": {
        "mapName": "Cp5_DrillingVessel_Outside",
        "mapSize": 28
      },
      "unitList": [
        {
          "unitID": 80099,
          "unitCount": 1,
          "unitLevel": 60
        },
        {
          "unitID": 8097,
          "unitCount": 1,
          "unitLevel": 40
        },
        {
          "unitID": 8098,
          "unitCount": 1,
          "unitLevel": 40
        }
      ],
      "subUnitList": [
        {
          "unitID": 8097,
          "unitCount": 1,
          "unitLevel": 40
        },
        {
          "unitID": 8098,
          "unitCount": 1,
          "unitLevel": 40
        }
      ],
      "bgmList": [
        "Battle_Cp5_MiddleBossBattle"
      ],
      "allyPositionID": 48,
      "enemyPositionID": 47
    }
  ],
  "staminaCost": 20,
  "recommendedLevel": 40,
  "turnLimit": 99,
  "stageScriptList": [],
  "eventScriptName": [],
  "abnormalityEventList": []
}

Explaining The Terms

Usually a battle located within the games' files work similar to a personality.json, being a simple entry in a list. The exception to this, however, is my template (or really, any battles you put in the config file directly) as they only need a single entry.

Close down the information within the waveList for now, and it will be a lot more readable.

  • id once again, it's an ID number. This doesn't really matter unless you're overwriting something from vanilla.

  • stageLevel is also a bit of a useless number, it's simply the "Recommended Level" that Limbus gives you.

  • stageType is an important one, as it determines whether your battle is a focused/Abnormality battle (Abnormality) or a regular/human battle (Normal). For this guide, I'm going to assume you're making an abnormality battle, but most of this is still fully applicable to human battles.

    • Important Note: The only type of enemy that can appear in a "Regular" battle is an "enemy", not an " abnormality".
    • Conversely, an, "enemy," cannot appear in an, "Abnormality," battle.
  • isBatonPassOn is just a boolean value. If enabled, it turns your battle into a Chain Battle (yes that is what they call Chain Battles)

  • includeBoss determines whether the red boss "Proelium Fatale" (or whatever variation that may exist) text appears when starting the stage. If you don't want to use this, then you can either set the boolean value to 0 or delete the entry entirely.

  • participantInfo determines how many sinners you can field in a single battle. The min option does nothing, only the max option does anything.

  • staminaCost is meaningless, so is recommendedLevel. However, these are both self-explanatory.

  • turnLimit just determines how many turns someone can take before the fight is guaranteed to result in a loss.

Scripts, Scripts, Scripts

Pretty much every single abnormality will have scripts for their battle, either to make something work that they couldn't in their passive, or to add abnormality events.

Limbus implements events in an incredibly weird way, both requiring you to have the right eventScriptName (which determines when a script triggers) and the right event ID itself (which also needs to be written here.)

stageScriptList

In general, this will just be copied and pasted; however, you might be interested in scripts like 403 which adds Railway 4 deployment buffs (and also backup, but I'll get to that), Chapter7SetAllyDefaultMp which is used for Canto 7 deployment buffs, or the one I mainly want to talk about, being Refill_Abnormality which enables enemy backup.

I'll get more into why backup is weird, but that requires me to explain how waves work, as that's where the enemies are actually stored.

Waves

A wave contains all the information that most people care about, for example:

{
    "battleMapInfo": {
        "mapName": "Cp5_DrillingVessel_Outside",
        "mapSize": 28
    },
    "unitList": [
        {
            "unitID": 8097,
            "unitCount": 2,
            "unitLevel": 40
        },
        {
            "unitID": 8098,
            "unitCount": 3,
            "unitLevel": 40
        }
    ],
    "bgmList": [
        "Battle_Cp5_MiddleBossBattle"
    ],
    "allyPositionID": 48,
    "enemyPositionID": 49
}
  • battleMapInfo is pretty self-explanatory, it's the battle background we fight on. mapSize is kind of a deprecated but certain fights do specific their map size, so if you're copying and pasting a map you'll probably be taking its map size with it, though it's completely fine to just remove the mapsize

  • unitList contains all the abnormalities that immediately appear when we open up the stage, pointing to an abnormalities ID number, how many copies of that abnormalities appear, and what the level of that abno is.

  • Note: If you want to go and find an abnormality go into limbus_locale and then the enemyList (yes enemies and abnormalities are in the same locale file) and search for the specific abnormality ID you want to find. Make sure to copy and paste the abnormality's ID, not their abnormality part's ID.

  • bgmList just determines what music plays. If you only have one entry inside your list that's the only music that will play. If you put in two entries into the list, then what music plays will depend on if you're winning or losing the battle.

  • allyPositionID determines where on the map our allies spawn. Typically specific encounters on specific maps will have their own unique value; however, considering that some maps don't, it is also fine to either keep this at 1, or just remove it.

  • enemyPositionID is the same thing as allyPositionID but for abnormalities instead. If you're having issues when modifying this value, put it at 2 or keep it at the default.

Enemy Backup is stupid... like, really stupid

If you open up the wave list you might have noticed a little thing called subUnitList inside the encounter. Now this is where the game stored stuff like umbrellas (from Drifting Fox) or any enemies that get summoned by a skill (like Ricardo with Assemble for example).

{
    "subUnitList": [
        {
            "unitID": 8097,
            "unitCount": 1,
            "unitLevel": 40
        },
        {
            "unitID": 8098,
            "unitCount": 1,
            "unitLevel": 40
        },
        {
            "unitID": 8097,
            "unitCount": 1,
            "unitLevel": 40
        },
        {
            "unitID": 8098,
            "unitCount": 1,
            "unitLevel": 40
        }
    ]
}

But it's also where enemies in backup are pulled from. Whenever an enemy dies, the next entry in the list will come in to replace their spot (Though, if the unitCount of the next entry in the list is higher than 1, it'll summon multiple of them).

It's important to note, however, that backup only functions for one wave. If you have three waves, each of which have subUnit(s), then only the first wave with subUnit(s) will have functioning backup.

Why? I don't know, project moon coding.