Modding Guide

This is the web copy of the modding guide that ships with the game (workshop_uploader/README.md in the install folder). The web copy tracks the latest build.

Publishes Terminus mods to Steam Workshop.

Requirements

  • Steam client running and signed in.

Usage

  1. Run WorkshopUploader.exe with the Steam client running.
  2. Browse... to a mod folder containing manifest.json.
  3. Choose visibility and click Upload to Workshop.
  4. On first upload, workshop_item.json is written inside the mod folder. Re-running with the same folder updates that item.

Example mods

examples/survival_pack/ demonstrates the content and language patch types, plus the images patch declaration:

  • crafting_recipes with alternate components, optional tools, and a built-in sound cue (the sound field names an existing game sound; audio files can't be bundled)
  • cooking_recipes with a fire-based recipe
  • foods for the cooking result
  • weapons for per-weapon balance overrides
  • structures for placeable item overrides (rain barrel capacity, snare trap catch chance)
  • language_patches for English and Korean
  • images/ for image overrides, declared in manifest.json (empty here; add .png files matching the game's sprite paths)

Each json_patches entry takes a target and a file. Keyed targets like foods and weapons add new keys and merge into matching ones field by field: only the fields you list are overridden and the rest of the entry is kept, so a patch like {"A1": {"fuel_capacity": 120}} on vehicle_stats keeps A1's other stats. Fields whose value is a list are replaced whole, not merged. The recipe lists (crafting_recipes, cooking_recipes) merge by result: an entry whose result matches a built-in recipe overrides only the fields you list and keeps the rest, so you can retune work_amount, swap the tool, or change a recipe's component order without restating the whole recipe. A new result is added at the end and must be a complete recipe. To add a second recipe for an existing result instead of overriding it (an alternate crafting path), set "mode": "append" on that patch entry. The other list targets (vehicles, npc) always append.

examples/italian_pack/ and examples/ukrainian_pack/ add new languages to the language menu.

examples/traits_pack/ adds custom character traits with gameplay effects. See Custom traits.

examples/perks_pack/ adds custom survival perks (a stackable level-up perk and a level-scaling occupation perk). See Custom perks.

examples/occupations_pack/ adds a custom occupation with its own unique perk, and rebalances a base one. See Custom occupations.

examples/config_example/ uses the config target for global balance overrides:

  • trait_point_override (number): fixes the trait point budget to a set value (a large value for effectively unlimited, 0 for none). Omit to keep the default level-based budget.
  • max_traits (number, default 4): trait slots selectable during character creation.
  • stat_max (number, default 5): the ceiling for each character stat (Strength, Health, Observation, Combat, Agility, Dexterity). Raise it (e.g. 8) to let players, companions, and NPCs train stats higher; the character-creation and companion + buttons, the stat-raising thick book, the all_around trait, and the grant.stat trait hook all respect the new ceiling, and the stat bars grow to match. Must be an integer from 1 to 10; out-of-range values are ignored and the default 5 is kept. Most stat effects scale linearly with the value, so a high cap makes stats much stronger. Observation is the exception: daytime sight tops out around 6 tiles regardless, while night sight grows by 0.5 for every 5 Observation, so raising the cap mainly extends night vision. Stats trained above a later-lowered cap (for example after disabling the mod) are preserved rather than erased and stay effective; they just can't be raised further until the cap is raised again.
  • melee_condition_loss_min (number, default 0.1): floor for per-hit melee condition loss after durability dampening. Lower it (e.g. 0.001) so weapons with low condition_loss_combat can wear far more slowly, or 0 to allow fully disabling combat wear.
  • condition_loss_mult (object, default {"head": 1.5, "body": 1, "legs": 1}): multiplier on a melee weapon's per-hit condition loss, by the body part struck. It applies only to hits against zombies, not human NPCs. Head hits wear weapons 50% faster by default. Set a part to 1 to remove its extra wear (e.g. {"head": 1} disables the head penalty), or 0 so hits there cost no condition. Parts you omit keep their default multiplier.

Include only the patch types you need. See each example's manifest.json for the format.

game_version_min (optional) in manifest.json is the lowest game build that will load the mod; older games skip it with a log message. It takes a dotted version number and accepts the full 4-part build (e.g. "1.3.1.33", as printed in the Game build: line of game_log.log on each launch), so a mod can require the specific patch build that introduced the field it uses. Shorter forms like "1.3.1" match any build of that version. Versions compare numerically per segment, not alphabetically.

Disabling or unsubscribing a mod does not break existing saves. A save that contains items or zombies added by a removed mod still loads; those objects are removed from the save on load (each removal is logged in game_log.log). To keep them, re-enable the mod before loading the save again.

Data reference

Every data target below has a matching vanilla file in resources/json/ inside the game's install folder (right-click the game in Steam → Manage → Browse local files). Those files are what the game itself loads, so each one is a complete, current example of every field a target accepts - the fastest way to write a patch is to copy an entry out of the vanilla file and change the fields you care about. The loader validates the rules listed below and drops or reverts whatever breaks them, logging each drop (see Debugging your mod). Fields outside these rules are not checked at load, but the game still reads them later, so give a new entry the same fields as a vanilla entry of the same kind rather than the bare minimum.

How patches merge is described under Example mods: keyed targets merge field by field, the recipe lists merge by result, and vehicles / npc append.

Item targets

One item class per entry, keyed by internal name. Patching an existing name updates that item's fields. A new name creates a new item and must include "parents", a list of base class names - the parents each vanilla file uses are listed below, and the registry also accepts the broader bases Weapon, RangedWeapon, KindleTool, Med, Clothing, BaseWearable and Bedding. class_id (optional) overrides the generated class name; keys starting with __ are ignored.

target vanilla file (resources/json/) covers parents used by vanilla entries
weapons weapons.json melee weapons, bows, firearms, arrows, gun accessories MeleeWeapon, BaseKnifeSpear, Archery, Arrow, Firearm, GunAccessory, Suppressor
tools tools.json tools, lighters and fire starters, fishing rods, siphon pumps Tool, MeleeTool, FixElecTool, Needle, BaseLighter, FireStarter, BaseFishingRod, BaseSiphonPump
medicines medicines.json medicine InstantMed, PersistMed, CureMed
clothing clothing.json clothes, gloves, bags, bedding BaseClothing, BaseGloves, BaseBag, BasePillow, BaseBlanket
other_items other_items.json materials, valuables, repair kits, dead animals, keepsakes Other, OtherMelee, RepairItem, BaseRepairKit, DeadAnimal, LostItem, QuestItem
structures structures.json placeables: traps, rain collectors, tent, portable generator AnimalTrap, FishTrap, BaseRainCollector, BaseTent, BasePortableGenerator

Validated on every item patch (a field that fails is ignored and the existing value kept):

  • init_condition_range, init_durability_range, condition_loss_combat, durability_range: [min, max] with two numbers
  • condition_loss_craft, charge_use: a number >= 0
  • capacity: a number > 0
  • name: a string
  • a new entry with a missing or unknown parents is skipped, as is a key that resolves to something that is not a registered item (such as weapon or item)

weapons fields are described under Tuning weapons and structures fields under Tuning placeable structures. Fields like value, weight, rarity and loctypes are not validated at load but are read on every item during world generation and trade, so a new item must define them - copy a vanilla entry to see the full set.

Table targets

target vanilla file covers load-time rules
foods foods.json food used by cooking and spawns entries must be objects; rarity / weight / decay_rate numbers >= 0; keys that collide with computed properties (satiety, value, ...) are dropped - use the base_ fields the vanilla file uses; weight without rarity is dropped (the food stays out of random spawns)
beverages beverages.json drink containers (bottles, cans) a new entry needs all of content / capacity / weight / material / reusable / rarity; capacity > 0; content must name a liquids entry or the entry is dropped (a vanilla entry broken by a patch reverts instead)
liquids liquids.json what beverages contain entry must be an object with a numeric value; broken vanilla entries revert
seafood seafood.json fish and other catches none (merged field by field)
cooking_recipes cooking_recipes.json cooking screen recipes merges by result, which must be a foods name or clean_water / salt; ingredient groups are lists of food-name alternatives, or one ["water" or "clean_water", amount] group per recipe; capacity an integer >= 1, and above 1 when the recipe takes poured water; clean_water / salt results take only water kinds and salt as ingredients; a recipe left with no valid required ingredients is dropped
crafting_recipes crafting_recipes.json crafting screen recipes merges by result (a string); components is a list of groups, each a list of [name, count] alternatives (a single pair may stand alone); tool a string or null
recipes_any recipes_any.json the item groups behind any: ingredient tokens and recipe tools (book, knife, ...) none; a list you patch replaces the vanilla list whole
upgrade upgrade.json upgrade material costs (generators, antigen extractor, ...) none; lists replace whole
vehicles vehicles.json the car pool locations spawn from appends; entries need string name / image_name / stats, other keys are stripped; an entry whose stats key is missing or invalid is dropped
vehicle_stats vehicle_stats.json stat blocks vehicles point at fuel_economy / fuel_capacity / capacity finite numbers, length an integer 2-9; broken vanilla stats revert
boats boats.json boat stat blocks none (merged field by field)
npc npc.json human NPC appearance and loadout pool appends; no field validation
npc_stats npc_stats.json combat stats per NPC role none (merged field by field)
furniture furniture.json furniture the map generator places none (merged field by field)
furniture_stats furniture_stats.json stat blocks furniture points at (capacity, item spawn rules, dismantle materials) none (merged field by field)
objects objects.json large interactive world objects (control panels, ...) none (merged field by field)

Zombie targets

  • zombies (zombies.json): one zombie per key. A new zombie must define at least sex and a string name, and once every mod has loaded it must carry each attribute the game guarantees on vanilla zombies: armor, bottom, difficulty, fire_fx, flags, flags_display_str, flags_display_weak, image, max_action, max_attack, max_hp, min_hp, target_parts, top_image, vision (drops / no_drop default to empty). A zombie still missing any of them is unregistered with a log line. The attributes may also come from the same mod's zombie_stats patch.
  • zombie_stats (zombie_stats.json): keyed by the zombie's name, not the class key. max_attack / max_action / vision / difficulty are numbers >= 0. max_hp / min_hp / armor are objects of numbers >= 0 per body part; together with what the class already has they must cover head / body / legs (a partial object is fine on an existing zombie - it merges part by part).
  • zombie_drops (zombie_drops.json): keyed by name; drops and no_drop are item name lists.

Code-defined targets

traits, perks, occupations and config have no vanilla JSON file - they are defined in code and documented in their own sections: Custom traits, Custom perks, Custom occupations, and the config fields under Example mods. Languages go through language_patches, not json_patches - see Adding a new language.

Debugging your mod

Every entry or field the loader rejects is written with its reason to game_log.log, next to the game's executable in the install folder. The log appends across launches (it resets once it grows past 5 MB), so read it bottom-up right after a launch. Each enabled mod leaves a line:

Mod loaded: Miku Expansion (miku_expansion)
Mod load failed: Broken Pack (broken_pack) - <what raised>
Mod load complete: 2/3

and WARNING lines name whatever was dropped or reverted, for example:

WARNING - Mod beverage 'x_cola': new entry missing ['reusable', 'rarity']; dropped (a beverage reads these from the table on every use)
WARNING - Cooking recipe result 'iron_sword' is not a food name; dropped
WARNING - Mod vehicle_stats O15.length: invalid 1, reverted to vanilla 3

A mod can "load" while individual fields or entries were rejected: the in-game mod manager only marks a mod that failed outright, so the log is the only place a partial rejection shows up. When a patch seems to have no effect in game, search the log for the entry name you patched before concluding the field does nothing. To then test values in a live game, see Debug console.

Adding a new language

To add a language not already in the game, the mod must provide both Data.json and FontInfo.json for that language; otherwise the patch is rejected at load (it would crash on selection).

{
  "content": {
    "language_patches": [
      {
        "language": "Italian",
        "file": "lang/Italian/Data.json",
        "font_info": "lang/Italian/FontInfo.json"
      }
    ]
  }
}

FontInfo.json must contain Normal and Bold (Italic is optional):

{
  "Normal": "Roboto-Medium.ttf",
  "Bold": "Roboto-Bold.ttf",
  "Italic": "Roboto-MediumItalic.ttf"
}

Font files are resolved against the directory holding FontInfo.json first, then the game's bundled languages/Fonts/ directory. To reuse a bundled font (Roboto covers most Latin/Cyrillic; NotoSansCJKjp covers CJK), just reference its filename. To ship your own font, drop the .ttf/.otf next to FontInfo.json and reference it by basename. Font filenames must be plain basenames (no path separators), capped at 30 MB.

To extend an existing language (add or override strings), omit font_info and use the existing language name:

{ "language": "Korean", "file": "lang/Korean/Data.json" }

Tuning weapons

The weapons target keys any weapon by internal name (e.g., aluminum_bat, metal_knife, machete). The fields below tune melee weapons' spawn condition, durability, and combat wear; ranged weapons such as bows and firearms do not use them. Tools such as fire_axe and crowbar are not weapons, so tune those through the tools target instead.

  • init_condition_range ([min, max], default [40, 100]): spawn-time condition (%) range. Condition is the wear meter that depletes with use and reaches 0 when the weapon breaks.
  • init_durability_range ([min, max], default [1, 5]): spawn-time durability tier range. Higher durability increases damage output and reduces condition loss per hit.
  • condition_loss_combat ([min, max], default [6, 8]): per-hit condition loss range during combat. The subtracted value is dampened by durability and reduced further by perks (clean attack, rescue expertise).

Example: aluminum bats spawn with at least 80% condition and high durability (4-5), and wear at about half the usual combat rate.

{
  "aluminum_bat": {
    "init_condition_range": [80, 100],
    "init_durability_range": [4, 5],
    "condition_loss_combat": [3, 4]
  }
}

See examples/survival_pack/data/weapons.json for a working override.

Tuning placeable structures

The structures target keys any placeable (deployed-on-the-ground) item by internal name:

  • Animal traps: makeshift_animal_trap, rat_trap, snare_trap, bird_trap, box_trap, cage_trap
  • Fish traps: makeshift_fish_trap, basket_fish_trap, net_fish_trap
  • Rain collectors: makeshift_rain_collector, rain_barrel
  • Others: tent, portable_generator

Patching an existing name overrides only the fields you list, like weapons. Common fields across all of them: weight, fuel (value as campfire fuel), rarity (spawn odds in furniture) and loctypes (location types it can spawn in; omitted or empty allows all), scraps (dismantle yield, e.g. [["stick", 1]]), and base_value (trade value at 100% condition; tent uses a flat value instead).

Animal traps

  • base_chance (number): catch chance per turn in percent, before situational multipliers (time since placed, bait, the trapper trait, nearby fire / traps / humans).
  • target (list): species it can catch, from rat, squirrel, rabbit, bird. Indoors only rat can be caught, outdoors everything but rat; a trap with no valid species for where it stands never triggers.
  • condition_loss (number): condition lost per catch, from 100. A catch that brings it to 0 destroys the trap.
  • can_be_placed (string): "outside" restricts placement to outdoor tiles; omit to allow indoor placement.

Fish traps

  • base_chance (number): catch chance per turn in percent. Each failed roll banks an eighth of the current chance as a bonus for later turns; the bonus resets on a catch.
  • capacity (number): how many catch stacks it holds; a catch that would exceed it is lost.
  • size (1-3): admits seafood up to this size (each entry in seafood.json has a size).
  • durability (number): dampens per-turn wear by 1 - durability / 13. Wear is higher while holding live fish or when placed in the sea. When the trap breaks, its scraps wash up on the adjacent shore tile.

Rain collectors

  • capacity (number): stored water cap in ml.
  • rain_collect_rate (number): ml gained per turn while it rains or snows on a weather-exposed tile.
  • condition_loss (number): condition lost every turn, rain or not, dampened by 1 - durability / 13 (durability is 0 when player-crafted, up to 5 on world-spawned collectors).
  • init_condition_range ([min, max]): spawn-time condition (%) range.
  • water_type (string): what filling a bottle from it yields, default dirty_water.

Tent

  • init_condition_range / init_durability_range ([min, max]): spawn-time condition (%) and durability tier ranges.

Portable generator

  • base_fuel_capacity (number): tank size before upgrades (each capacity upgrade adds 20%).
  • base_fuel_consumption_rate (number): fuel burned per powered hour before upgrades (each upgrade cuts 15%). Upgrade costs live in the upgrade target under the portable_generator key.
  • base_weight / base_value (number): weight and value when empty; carried fuel adds to both.
  • The patch covers both the inventory item and generators found in the world (placed generators read their base stats from this entry).

Example: the rain barrel holds more and fills faster, and snare traps catch more per condition:

{
  "rain_barrel": {
    "capacity": 8000,
    "rain_collect_rate": 16
  },
  "snare_trap": {
    "base_chance": 7,
    "condition_loss": 10
  }
}

New entries can also be added with parents (one of AnimalTrap, FishTrap, BaseRainCollector, BaseTent, BasePortableGenerator), the same way new weapons work. A new placeable needs an inventory icon (item/<name>.png) plus placed-world sprites: animal traps read world/trap/<name>.png; fish traps read three images, world/trap/<name>.png (placement preview), world/trap/<name>_placed.png, and world/trap/<name>_placed_hover.png; rain collectors read world/rain_collector/<name>.png; tents and generators reuse the base sprites. A missing image logs an error and renders as blank instead of crashing. Ship these under the mod's images/ directory matching those paths.

See examples/survival_pack/data/structures.json for a working override.

Custom traits

The traits target adds new character traits, selectable during character creation. Each JSON key is the trait's internal name:

{
  "iron_constitution": {
    "cost": 11,
    "effects": [
      { "hook": "max_hp", "op": "add", "value": 8 },
      { "hook": "max_ap", "op": "add", "value": 4 }
    ]
  }
}
  • cost (number): trait point price during character creation.
  • comp_trait (bool, default true): whether the trait shows on the companion customization screen. Set false to hide it there. It does not stop auto-generated NPCs or companions from rolling the trait, so it cannot make one truly player-only.
  • effects (array): what the trait does, one or more entries.
  • Patching an existing trait name (such as reading_lover) updates its fields, so you can also rebalance the base game's traits, for example changing their cost.

Every trait needs trait_<name> and trait_<name>_desc strings in a language_patches Data.json, or it shows as an untranslated key in the menu.

Effect format

{ "hook": "damage", "op": "add", "value": 30, "when": { "weapon_type": "melee" } }
  • hook (string): where the effect applies (see the reference below).
  • op (string): how it applies.
  • add: add value into the total.
  • mul: multiply by value (1 is no change).
  • gate: switch a behavior on (no value needed).
  • grant: give something (recipes, stats, items), at the start of a run or on an event such as an item breaking.
  • set: override a value.
  • value: the magnitude, or a list/object for grants.
  • when (object, optional): conditions that must all hold. See Conditions.

A trait may list several effects, and several traits may target the same hook (they stack).

Hook reference

Stats and survival

hook op effect
max_hp, max_ap add maximum HP / AP, applied at the start of a run
cond.hp, cond.ap, cond.satiety, cond.hydration, cond.energy, cond.morale add per-turn change to a meter (positive restores, negative drains)
cond.satiety.drain_mult, cond.energy.drain_mult, cond.morale.drain_mult mul scales the default per-turn drain of a meter (0.5 = drains half as fast)
cond.sleep.energy_mult mul scales energy recovered while sleeping

Skills and movement

hook op effect
reading_speed, crafting_speed add speed bonus (0.2 = +20%)
night_sight add night vision distance
move_ap add move AP modifier (negative is faster, e.g. -0.1)
search_extra_items add extra items found when searching
proficiency.melee, proficiency.ranged mul weapon proficiency multiplier

Combat

hook op effect
accuracy add hit chance revision in percent. Conditions: weapon_type, attack_part, target_out_of_sight
damage add damage revision in percent. Conditions: weapon_type, weapon_category, attack_part
drop_chance add added drop chance as a 0-1 probability, not a percent (0.1 = +10%; 1 always drops)
butcher.bonus add extra count per butchering result
dismantle.mult mul scrap yield when dismantling
cook.ap mul cooking AP cost (0.5 = half)

Consumables

hook op effect
food.morale mul morale from food. Conditions: item_name, morale
beverage.morale mul morale from drinks. Conditions: item_name, item_has_alcohol, morale
book.morale mul morale from reading the bible
mp3.morale mul morale from music
cigarette.morale mul morale from smoking
med.strength mul medicine strength (higher raises headache and faint risk)
med.effect mul medicine recovery amount

On/off behaviors (op is always gate)

hook effect
gate.attack cannot attack (pacifist)
gate.alcohol_effect immune to drunkenness and fainting from alcohol
gate.food_disease immune to food poisoning
gate.detectable zombies cannot detect you by sound
gate.sleep_disturb noise does not wake you
gate.sleep_anywhere can sleep on any tile
gate.pickpocket can steal during trades
gate.survive_bite a zombie bite causes slow infection instead of instant death
gate.reveal_contents see furniture and corpse contents without searching
gate.reveal_treasures hidden treasures are revealed within sight
gate.weather_forecast the character announces upcoming rain and snow

Start of run and character creation

hook op effect
grant.learn grant learn recipes. value: a list of recipe names, or "all_crafting" / "all_cooking"
grant.stat grant raise stats. value: {"dex": 1} (each capped at the stat ceiling stat_max, default 5) or "all" (+1 to every stat)
grant.unique_perk_lv grant start the occupation's unique perk at this level. value: an integer 1-4
companion_count add extra starting companions
reveal_locations add nearby map locations revealed at the start
unique_perk_maxlv set maximum unique perk level. value: an integer 1-4
levelup_choices set perk choices offered on level up. value: an integer 1-100
trust mul multiplier on trust gained from survivors
on_break_grant grant when a weapon or tool breaks, give items. value: [["scrap_metal", 2]]. Condition: category

Stat keys for grant.stat, with the in-game name in parentheses: str (Strength), con (Health), obs (Observation), com (Combat), agi (Agility), dex (Dexterity).

Conditions (when)

Every key in a when object must hold for the effect to apply.

key values applies to
time "day", "night" any hook except the grant.* hooks
morale ">0", "<0", ">=5", "<3" food.morale, beverage.morale
item_name a food or drink name food.morale, beverage.morale
item_has_alcohol true, false beverage.morale
weapon_type "melee", "ranged" accuracy, damage
weapon_category "weapon", "tool" damage
attack_part "head", "body", "legs" accuracy, damage
target_out_of_sight true, false accuracy
category "weapon", "tool" on_break_grant

The other conditions only have an effect on the hooks listed, because they depend on context that only those hooks provide. The morale condition compares the morale the food or drink itself would give, not the character's current morale.

Out-of-range values for unique_perk_maxlv (must be 1-4) and levelup_choices (must be 1-100) are dropped at load time rather than clamped.

Set "is_unique": true and "occu": "<occupation>" (such as chef) to make a free trait that characters of that occupation automatically start with, like the base game's occupation-unique traits (it does not cost trait points). A unique trait with no occu falls back to a normal selectable trait.

See examples/traits_pack/ for a working pack.

Custom perks

The perks target adds new survival perks or rebalances existing ones. Perks use the same effect DSL as traits (see Effect format and the Hook reference), plus extra fields for level scaling. Each JSON key is the perk's internal name:

{
  "brute_force": {
    "max_count": 3,
    "bonus": 10,
    "effects": [
      { "hook": "damage", "op": "add", "per": "count", "value_per": 10 }
    ]
  },
  "close_quarters": {
    "is_unique": true,
    "occu": "soldier",
    "effects": [
      { "hook": "damage", "op": "add", "value_table": { "1": 10, "2": 20, "3": 30, "4": 40 }, "when": { "weapon_type": "melee" } }
    ]
  }
}

There are two kinds of perk:

  • Normal perks are offered on level up and can stack up to max_count (default 1) copies. Set bonus (default 0) to the per-copy number the description shows through {num} (where {num} = bonus x copies, rounded to two decimal places for display).
  • Unique perks ("is_unique": true with an "occu") are free perks a character of that occupation starts with, alongside any base occupation perk. They level up instead of stacking: from 1 to a maximum of 3 by default, or 4 when the character has the elite trait (or an effect sets the unique_perk_maxlv hook). A unique perk with no occu falls back to a normal perk.

is_unique, occu, max_count, bonus, and effects are the only recognized perk fields; any other key is dropped at load.

Level scaling

Traits are flat, but perks scale with stack count or level. On an add/mul/set effect:

  • per ("none" default, "count", "lv"): selects the scaling index: the number of stacked copies ("count", normal perks) or the perk's level ("lv", unique perks). Only value_per and value_table scale with it; a flat value is a constant base that per never multiplies.
  • value_per (number): amount added per unit of the index, on top of the base value (final amount = value + value_per x index). For "+10% per stack" use "per": "count", "value_per": 10 — putting the 10 in value instead gives a flat +10% no matter how many copies stack (the game logs a warning for an inert per at load).
  • value_table (object): maps the unique perk's level directly to a value, e.g. { "1": 10, "2": 20 }. To index the stack count instead (for a stackable normal perk), add "per": "count" to the same effect; without it a normal perk always reads index 1. The table overrides value/value_per, and an index not listed contributes nothing.

A flat value with no scaling also works, exactly like a trait. Exception: the unique_perk_maxlv and levelup_choices hooks accept only a flat value (no per, value_per, or value_table), since their 1-4 / 1-100 ranges are validated on the flat value.

Language and icon

  • Normal perks need perk_<name> and perk_<name>_desc strings.
  • Unique perks need perk_<name> and perk_<name>_lv1 through perk_<name>_lv4. The maximum level is 3 by default, but the elite trait (or a unique_perk_maxlv effect) raises it to 4 — without the _lv4 string, elite characters see the raw key in the level-up popup. A value_table should likewise cover levels up to "4".
  • Every new perk should ship an icon at images/perk/<name>.png (declare the images patch in manifest.json). If it is missing the perk still works, but shows a blank icon in the level-up and character panels (the game logs the missing image and substitutes a 1x1 placeholder rather than crashing). Rebalancing an existing perk needs no new icon.

Patching an existing perk name updates its fields, so you can rebalance base perks, for example adding an effect to skillful_search or changing a max_count.

New normal perks also join the level-up pool of existing saves (they are mixed in when the save loads), so a perk mod does not require starting a new game.

See examples/perks_pack/ for a working pack (add the two images/perk/*.png icons before publishing).

Custom occupations

The occupations target adds an occupation to the character creation screen, or rebalances a base one. Each JSON key is the occupation's internal name:

{
  "paramedic": {
    "start_stats": { "str": 1, "con": 2, "obs": 2, "agi": 1, "dex": 3, "bonus_points": 3 },
    "starting_items": [["bandage", 2], ["weak_medicine", 1]],
    "unique_actions": ["firstaid"],
    "unique_perks": ["triage"],
    "identify_locs": ["pharmacy"]
  }
}
  • start_stats (object): the stats a character of this occupation starts with, plus the points the player distributes freely. Keys are str (Strength), con (Health), obs (Observation), com (Combat), agi (Agility), dex (Dexterity) and bonus_points; each is an integer from 0 to 10 (bonus_points up to 60) and a key you omit is 0. A new occupation must have a valid start_stats - the character creation screen reads it directly, so an occupation without it is not registered (the log says so). On an existing occupation, only the stats you list change and the rest are kept, so {"bonus_points": 13} retunes the Student and nothing else.
  • starting_items (list, optional): [[item name, count], ...] given at the start of a run. Counts are integers from 1 to 100. An item name is anything the game can resolve to an item: a key in the foods or beverages tables, salt or pepper (the shakers), or the internal name of a registered item class - the game's item tables in resources/json (weapons, tools, medicines, clothing, seafood and so on) and classes your own mod adds through an item target such as weapons or other_items. Names from other data files (furniture, vehicles, NPCs, recipes) are not items. A name the game cannot hand out at the start of a run is dropped at load with a log line, whether it is unknown or an item the game only ever builds in a specific situation: piece_of_map, which is cut from the map that does not exist yet when a run starts, and the fortified house's lost keepsakes, which are built with the resident who owns them. Ordinary loot works, including things found on corpses such as jewelry.
  • unique_actions (list, optional): occupation-only action buttons. Only the base game's occupation actions exist, since each one is a coded action panel button: boost (spend next turn's AP now), listen (hear the whole location), firstaid (heal self or nearby companions), kindlespecial (start a fire without a lighter), lift (lift furniture and crush zombies with it), focusdodge (spend all AP to dodge), pray (restore morale nearby), preach (grant a Holy status effect). Sharing one with a base occupation is fine; firstaid scales with the Doctor's First Aider perk, so an occupation that takes the action without that perk gets its level 1 effect. Any other name is dropped at load.
  • unique_perks / unique_traits (list, optional): perks and traits a character of this occupation starts with for free. unique_perks must name unique perks - a base occupation perk such as first_aider, or one your mod declares with "is_unique": true and an "occu" through the perks target. A normal or special perk name is dropped at load: the level system (veteran traits, save files) and the perk_<name>_lv1-_lv3 description strings shown in the occupation tooltip and the unlock popup only exist for unique perks. unique_traits may be base or mod traits. Unknown names are dropped at load.
  • identify_locs (string or list, optional): location types this occupation knows from the start, like the base Police Officer knowing every police station. Location types are the ones the map generates: church, clothing_store, electronics_store, fire_station, gas_station, grocery_store, gun_shop, hardware_store, library, park, pharmacy, police_station, restaurant, plus house, fortified_house, marina, military_base, research_center, terminus, checkpoint, danger, helipad, railroad, seaside and tunnel; inside_terminus and platform are generated only on the Last Escape DLC map. A companion of this occupation reveals them when recruited too.

Patching an existing occupation replaces starting_items, unique_actions and identify_locs with what you list. unique_perks and unique_traits are added instead of replaced, because the same lists are also filled by perks and traits that declare an "occu" (see Custom perks) - that way it does not matter which patch the game applies first, and a perk pack and an occupation pack can be shipped together or separately. There is no way to take a unique perk or trait away from an occupation.

Occupation names allow only alphanumerics and underscore, up to 64 characters, and cannot be one of the game's achievement ids (a run survived as that occupation would unlock an unrelated Steam achievement).

Language and icon

  • Every occupation needs occu_<name> and occu_<name>_desc strings, plus occu_loc_info_<name> if it has identify_locs (one line describing what it knows, shown in the occupation tooltip).
  • Ship an icon at images/newgame/occu_<name>.png for the character creation panel (the base icons are 51x51 in a 52x52 cell) and declare the images patch in manifest.json. A missing icon logs an error and renders blank rather than crashing. images/newgame/occu_<name>_gold.png is the optional variant shown after a player survives a run as that occupation; without it the normal icon stays.
  • images/occu/<name>.png is a second, smaller icon drawn next to a survivor's name in the Last Escape DLC character list (the base ones are 33x33 and are drawn at half scale, like item images). Ship it too if your mod is meant to be played with the DLC.

How mod occupations behave

  • They are selectable from the start: the base occupations unlock three at a time as the player survives runs, but a mod occupation has no such requirement.
  • Surviving as one is not counted as a base-game clear, so it does not advance that unlock progress, does not appear in the unlock popup or the history occupation grid, and grants no achievement. Base progression stays exactly as it is with the mod installed or removed.
  • The character creation panel holds 15 occupations; beyond that it gains page arrows at the bottom.
  • Auto-generated survivors, companions and NPCs can roll a mod occupation, so it also shows up in the world.
  • Removing the mod does not break saves: a character keeps the occupation name and the stats it was given. But everything the occupation entry itself defines is gone with the mod, so its strings and icons fall back to placeholders, its unique action button disappears from the action panel, and recruiting a companion of that occupation no longer identifies locations. What the occupation handed out is treated the same as anything else your mod added: perks and traits from your mod are dropped on load (they are resolved by name), and so are items your mod defined through a class target such as other_items, as with any removed mod's items. Base-game items and perks stay, and so does an item whose name your mod added to the foods table, since a food copies its values onto the object as it is created. A beverages entry is the exception: the object stores only which container it is and looks weight, capacity and the rest up in the table every time, so one still sitting in a save stops working once the table entry is gone. Do not give a beverage your mod added out as a starting item.

See examples/occupations_pack/ for a working pack (add images/newgame/occu_paramedic.png and images/perk/triage.png before publishing).

Debug console

For testing mods in a live game, launch the game with the --debug option (Steam: game Properties → Launch Options → add --debug; non-Steam: pass it as a shortcut argument), then press the backtick key (`) in game to open the debug console. Without the launch option the console does not exist.

Commands (run help in the console):

  • debug - enable cheat commands for the current save. Shows a warning and asks you to confirm with y: confirming permanently disables achievements for that save and shows a DEBUG marker on screen. Until then, cheat commands are rejected.
  • spawn <item> [count] - add items to the selected character's inventory (item and zombie ids are the internal names in the game's resources/json/*.json)
  • zombie [name] - spawn a zombie in the current location (default zombie_normal)
  • npc <survivor|bandit|pickpocket|starving_elder> - spawn an NPC in the current location
  • hp/ap/str/con/obs/com/agi/dex/satiety/hydration/energy/morale/med/alcohol <n> - set a value on the selected character
  • levelup - open the perk selection popup for the selected character
  • afflict <infection|zombified|disease|bleeding|sprain|burn> - apply a status ailment to the selected character
  • cure [ailment] - remove a status ailment from the selected character (no argument: remove all, including zombification)
  • time <dd:hh:mm> - set the game clock (day 0-999)
  • reveal - reveal and identify every location on the map

Notes:

  • Commands target the currently selected character and run only during the player turn.
  • help and debug work without confirmation; everything else requires the debug confirmation once per save.

What gets sent to Steam

The selected folder is uploaded recursively, plus:

  • Title from manifest.name
  • Description from manifest.description
  • Tags from manifest.type and each json_patches[].target
  • Visibility chosen via the radio button (Public / Friends Only / Private)
  • Preview from preview.png or preview.jpg if present