Modding Guide

This is the web copy of the modding guide that ships with the game (_internal/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 adding alternate crafting paths for existing items (the patch entry sets "mode": "append"), with alternate components, a tool requirement, 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 sprites your mod adds or replaces (empty here; add .png files matching the game's sprite paths). Mod images take priority over the game's own: a file at an existing sprite path replaces that base-game sprite (a retexture), and a file at a new path supplies art for mod-added content. When several enabled mods ship the same path, the one later in load order wins, like data patches. The folder is picked up automatically when it exists - the manifest images block is only needed to change base_path

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 (integer 0-30, default 4): trait slots selectable during character creation. Out-of-range or non-integer values are ignored and the default kept.
  • 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 keep their width, packing the pips tighter to fit. 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 (game_log.log records one line with the removed count). To keep them, re-enable the mod before loading the save again.

Manifest

manifest.json requires schema_version (the integer 1), id (alphanumerics and underscore, 64 characters or fewer - also the log's name for your mod), and a string name of 128 characters or fewer. author, version, description and game_version_min are optional; description, when present, must be a string of 1000 characters or fewer (the mod manager renders both name and description as-is). A json_patches entry missing target or file (both must be strings), an unknown target, a patch list or entry of the wrong shape (null included), a non-string language / file in language_patches, or an images block that is not an object with a string base_path, rejects the whole mod at discovery, before anything is applied. A base_path pointing outside the mod folder is ignored at load - the images folder is simply not registered.

A patch file that fails to parse (a JSON syntax error, or a file that is not UTF-8 JSON) skips just that patch with a log line while the rest of the mod still applies, the same as a file over the security caps (10 MB, nesting depth 20, strings 10K characters). json_patches, language_patches and font_info files must carry the .json extension; any other extension skips the patch with a log line. Valid JSON with bad values degrades entry by entry as described under Data reference.

Mods are discovered through Steam Workshop subscriptions only - local folders are not scanned. To test a mod before release, upload it with Private visibility and subscribe to it yourself. Mods apply in a deterministic order - the order the mod manager lists them in - but you cannot control where your mod lands relative to another author's, so do not rely on your patch landing before or after another mod's. References the loader resolves only after every mod has loaded are the exception - occupation references to perks, traits and items; cooking recipe results and ingredient names against the foods table; beverage content against liquids; rain collector water_type; vehicle stats keys; zombie drops item names - but class and entry registration itself applies strictly in order, including within one mod: json_patches run top to bottom, so a zombie_stats patch only touches zombies registered when it runs (declare it after your zombies patch).

Data reference

Every data target below has a matching vanilla file in _internal/resources/json/ inside the game's install folder (right-click the game in Steam → Manage → Browse local files; the game ships as a onedir build, so everything but the executable lives under _internal/). 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 the drops (see Debugging your mod for how to read the log and the few silent cases). 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. A new item's name (the JSON key) follows the occupation rule - alphanumerics and underscore only, 64 characters or fewer - because the name goes straight into the item/<name> image paths; an entry breaking it is skipped. class_id (optional) overrides the generated class name; neither it nor the name generated without it may contain a dot - a dotted class name would make every save fail once such an item exists in the world, so the entry is skipped instead. 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 of alphanumerics and underscore, 64 characters or fewer (renaming redirects the item/<name> image lookups too)
  • ailments (medicines): a non-empty list of ailment names; a new entry descending from CureMed must include one
  • value, weight, rarity, ap_cost, condition, charge, broken, remaining, base_damage, reach, damage, repair_speed_mult, strength, persist: numbers >= 0 when a patch sets them, whether or not the entry touches funcs - the spawn and drop rolls compare value on every item, the melee value formula multiplies base_damage by reach, and the action panel compares ap_cost every frame
  • funcs / action_btns: lists of the game's action names. A name is dropped when the class does not carry what that action's flow reads - turnon needs the light machinery (is_on / broken / charge / charge_use), break_furniture / forceopen / reinforce / cutchainlink need ap_cost, smearguts ap_cost and remaining, fixcar condition (its repair popup reads it even without a panel button), kindle a consume method (any KindleTool line has one), mend / firestart / fixelec / fish their base class machinery, cook is_cooking_tool: true. An action_btns name additionally needs condition (the panel button's tooltip reads it), and a new entry's action_btns defaults to its funcs through that same check - a tool without condition keeps its right-click action but gets no panel button, like the vanilla lockpick. So listing a real action on an unrelated parent (say till on a medicine) degrades at load instead of crashing the action panel; a parent that already declares the action passes its instance-held attributes
  • 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. A new item must still define value, weight, rarity and loctypes itself (their presence is not checked, but they are read on every item during world generation and trade) - copy a vanilla entry to see the full set. Every new item also needs two images under the mod's images/ folder: item/<name>.png (inventory) and item/<name>_floor.png (drawn when the item lies on the ground).

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 and follow the zombie_drops field rules). 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, as long as it is declared after the zombies patch - patches apply in order, and zombie_stats only touches zombies registered when it runs.
  • zombie_stats (zombie_stats.json): keyed by the zombie's name, not the class key. max_attack / max_action / vision / difficulty are numbers >= 0. difficulty also sets the default death drop's value window (difficulty x 2.9 up to 12 + difficulty x 3, minus the zombie's no_drop names): only items lighter than 0.5, non-perishable, non-clothing and carrying a spawn rarity are eligible, and vanilla eligible values top out at 24.7 (potent_vitamins), so once difficulty x 2.9 passes 24.7 - difficulty above roughly 8.5 - the window holds nothing unless a mod also supplies eligible items. A zombie whose window no loaded item can land in - whether from difficulty, from no_drop covering every name in the window, or from item value rebalances - never yields the default drop; the loader logs a line after all mods load when no fixed-value item fits the window (items whose value varies at runtime, such as charge-scaled sights or foods, are not counted, so a logged zombie may still drop through one of those). 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 is a list of {"name": ..., "weight": ...} objects - a weighted pool rolled against the default drop (name a string, weight a finite number >= 0; a list violating this is ignored whole, as is a non-object entry). no_drop is a list of item names excluded from the default random drop; it is compared against runtime item names, and a beverage's runtime name is its content, so to exclude a drink list its liquids key, not the beverages key. Excluding every name inside the zombie's value window leaves its default drop with nothing to yield (logged - see zombie_stats). A patch that sets only one of the two keys keeps the other as it is. A drops name the game cannot create on the spot (an unknown name, the fortified-house keepsakes, salt / pepper - the shakers only resolve through the starting-item path) is removed after all mods load, with a log line. The same field rules apply when drops / no_drop are set through the zombies target.

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

Nearly 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; at each launch a file over 5 MB is emptied before logging starts (it never resets mid-session). 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.

A few rejections leave no log line, so check these by hand when something silently does nothing:

  • a zombie_stats or zombie_drops key that matches no registered zombie's name is ignored (a typo there just does nothing)
  • a no_drop name that matches no item excludes nothing, silently (unresolvable drops names, by contrast, are removed at load with a log line)
  • an occupation identify_locs name outside the location types is kept but never matches anything (the loader checks its type only)
  • an effect when key that its hook does not provide keeps the effect from ever applying, and the only trace is below the log's level - see Conditions

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" }

Data.json is one flat object of string values, like the game's own language files (trait_lucky and trait_lucky_desc are two separate keys). A non-string value - for example an object nested under a category key - is dropped at load with a log line naming the dropped keys, and the rest of the file still merges.

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: 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). Exception: the portable generator computes weight and value from base_weight / base_value plus carried fuel, so tune those base_ fields there - setting weight or value directly replaces the fuel-aware calculation.

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 the same amount units beverage capacities use.
  • rain_collect_rate (number): amount gained per turn while it rains or snows on a weather-exposed tile (same units as capacity).
  • 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) and a ground sprite (item/<name>_floor.png, drawn when it lies on the floor as an item), 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 placed-world sprites (the two item images are still needed). 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. Omitting it means add, so a gate.* hook must spell out "op": "gate" - without it the effect is treated as add and dropped at load for lacking a numeric value.
  • 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
cold_damage_mult mul scales the HP lost per turn while colder than the character's base temperature (0.5 = half). The Park Ranger's Cold Endurance is this hook

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
carry_weight_offset add weight ignored when carried weight raises move AP (the Construction Worker's Super Strength is this hook). The character panel's weight tooltip shows the total. A negative total is treated as 0 - this hook only ever lightens the load

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
attack.morale add morale change each time the character attacks a zombie or person (once per attack action; negative is a cost, e.g. -1). Breaking doors/windows is exempt. Conditions: weapon_type, attack_part
drop_chance add added to the zombie drop probability (0-1 scale) before the whole chance is scaled by zombie difficulty - and, for the default random drop, by the matching item category's sandbox setting - so 0.1 is +10 percentage points only at difficulty 0. The final chance is rolled against 0-1, so a total of 1 or more after those multipliers is a guaranteed drop; a sandbox loot setting under 100% can push it back below certainty on low-difficulty zombies (a zombie's custom drops pool skips the sandbox factor)
butcher.bonus add extra count per butchering result
dismantle.mult mul scrap yield when dismantling
cook.ap mul cooking AP cost (0.5 = half)
counter.chance add chance in percent to counter-attack after dodging a zombie (the Martial Artist's Counter Attack is this hook). Below 100 the roll is halved and then climbs back with each consecutive failed counter, so a low value still lands eventually
firstaid.heal add HP the firstaid action restores on top of its base 6. The action panel's effect line shows the resulting amount
firstaid.cooldown mul turns before firstaid can be used again (base 8, rounded)
item.condition_loss mul condition a melee weapon, tool or melee-capable other_items entry loses per use (after durability dampening and the Clean Attack perk), and a fishing rod's wear per cast. The Firefighter's Rescue Expertise is this hook. It does not reach a bow's or suppressor's per-shot wear, the wear from crafting, cooking or mending with a tool, or clothing. Conditions: item_name, category, usage

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 a book (bible and novel are the two that give morale). The Believer trait (x2) and the Pastor's Divine Reader (x3) are this hook, both with "item_name": "bible". Conditions: item_name
book.reread add extra times the same book can be read (the Student's Review is this hook; base is one read, which is also the floor)
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

Actions and production

hook op effect
harvest.mult mul crops a garden yields when harvested (the Farmer's Farm Hands is this hook). A value above 100 is dropped at load, and one harvest yields at most 100 crops however the multipliers stack
cook.bonus mul a cooking recipe's satiety_bonus / morale_bonus (the Chef's Chef's Dish is this hook)
cook.duration mul hours a cooked dish's stat bonus lasts (base 6, rounded)
craft.material_mult mul component counts in every crafting recipe, rounded up (the Engineer's Material Saving is this hook)
drive.fuel_mult mul fuel a drive consumes (the Driver's Economy Driving is this hook). With any effect present the figure is rounded up to one decimal, and 0 makes driving free
cleardebris.mult mul debris cleared per AP, by hand or with a shovel
fixcar.mult mul vehicle damage repaired per AP
repair.mult mul electronics repaired and clothing mended per AP
fish.chance mul catch chance per fishing turn
reinforce.amount add HP added to a door or window when reinforcing (base 20 with a plank, 40 with a metal sheet)
pray.morale add morale the Pastor's pray action gives each character nearby (base 5)
preach.duration mul how long the Holy status from preach lasts (base 6 turns)
kindlespecial.fuel add campfire fuel the Park Ranger's kindlespecial adds (base: one plank's fuel)

The additive action outputs above (firstaid.heal, reinforce.amount, pray.morale, kindlespecial.fuel) floor their total at 0: a negative value can cancel the base amount but never flips the action into doing the opposite. carry_weight_offset floors the same way, and book.reread floors at the base one read.

On/off behaviors (op is always gate)

hook effect
gate.attack cannot attack. The base Pacifist and Frailty traits declare this effect, so a mod that overrides their effects can rebalance them - for example, replace the gate with attack.morale to allow attacks at a morale cost
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 applies the zombified status instead of killing on the spot - the status still advances every turn and kills at full progression unless cured
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
gate.firstaid_sprain the firstaid action also cures a sprain (the Doctor's First Aider opens this at level 2)
gate.firstaid_bleeding the firstaid action also stops bleeding (First Aider level 3)
gate.counter_extra a successful counter-attack hits a second adjacent zombie (the Martial Artist's Counter Attack level 4)

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
loctype a location type, the same names identify_locs takes any hook except the grant.* hooks
inside true, false any hook except the grant.* hooks
lv 2, ">=3" any hook except the grant.* hooks
morale ">0", "<0", ">=5", "<3" food.morale, beverage.morale
item_name a food, drink, book or item name food.morale, beverage.morale, book.morale, item.condition_loss
item_has_alcohol true, false beverage.morale
weapon_type "melee", "ranged" accuracy, damage, attack.morale
weapon_category "weapon", "tool", "other" (melee-capable other_items) damage
attack_part "head", "body", "legs" accuracy, damage, attack.morale
target_out_of_sight true, false accuracy
category "weapon", "tool", "other" (melee-capable other_items) on_break_grant, item.condition_loss
usage "weapon" (the item was swung at something) or "" item.condition_loss

Any value may also be a list, which matches when the context is one of its entries: "item_name": ["axe", "fire_axe"]. time is the exception: the loader accepts exactly "day" or "night" there, and anything else - a list included - drops that effect with a WARNING line in game_log.log. The check exists because a time value the engine does not know matches neither day nor night, which would leave the effect inert everywhere with nothing in the log to show for it. A list would mean "day or night" anyway, which is what leaving time out already does.

time, loctype, inside and lv work on every hook that reads when at all - that is, everything except the grant.* hooks, which are handed out at the start of a run without consulting conditions - because they do not depend on a call site handing them context. time reads the game clock, so it holds wherever the effect is evaluated. loctype and inside read where the character stands, so a character not yet placed in the world - as during character creation, when the max_hp / max_ap hooks are read - matches neither. lv reads the level of the unique perk carrying the effect - traits and normal perks are always level 1, so "lv" is how a unique perk changes behavior by level rather than just scaling a number (value_table covers scaling). gate effects take when too, which is how the Doctor's First Aider opens its cures at levels 2 and 3.

The remaining conditions only work on the hooks listed for them. Such a key names context that only those hooks supply; on any other hook the check can never pass, so the whole effect never applies anywhere - and the only trace is a debug-level line that does not reach game_log.log (the loader validates when.time only). 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 trait that characters of that occupation automatically start with, like the base game's occupation-unique traits. A cost you set is still deducted from the character's trait points at creation, so leave cost out (or 0) to make it free like the base game's. 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 (the mod's images/ folder is picked up automatically). 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.

The base occupation perks

Every base unique perk is declared with the same effects schema your mod uses, so none of them is a black box you can only borrow by name: copy the effect, change the numbers or the when, and you have your own version on a different occupation. The hook each one uses:

perk occupation effect
fighting_instinct Soldier damage +50 per level, "weapon_type": "melee"
bulls_eye Police Officer accuracy +50 per level, "weapon_type": "ranged"
rescue_expertise Firefighter item.condition_loss x{1: 0.6, 2: 0.35, 3: 0.15, 4: 0}, "item_name": ["stone_axe", "axe", "fire_axe", "crowbar"]
farm_hands Farmer harvest.mult x(1 + 0.5 per level)
cold_endurance Park Ranger cold_damage_mult x{1: 0.5, 2: 0.25, 3: 0.12, 4: 0.05}
speed_reading Student reading_speed +0.5 per level
review Student book.reread +1 per level
super_strength Construction Worker carry_weight_offset +10 per level
energizer Athlete cond.ap +0.5, and +0.5 per level
night_watch Security Guard night_sight +1 per level
chefs_dish Chef cook.bonus x{1: 1.5, 2: 1.75, 3: 2, 4: 2.5}, cook.duration x{3: 2, 4: 3}
first_aider Doctor gate.firstaid_sprain at "lv": ">=2", gate.firstaid_bleeding at ">=3", firstaid.cooldown x0.5 at ">=4"
economy_driving Driver drive.fuel_mult x{1: 0.5, 2: 0.3333333333333333, 3: 0.25, 4: 0.2} (fuel divided by 2/3/4/5)
counter_attack Martial Artist counter.chance +33.4 per level, gate.counter_extra at "lv": ">=4"
material_saving Engineer craft.material_mult x{1: 0.8, 2: 0.65, 3: 0.5, 4: 0.35}

The occupation-unique traits work the same way: Pacifist is a gate.attack, and Divine Reader is a book.morale x3 on "item_name": "bible".

So a perk that mirrors the Pastor's church reading for a firefighter in a fire station is the same effect with a different when:

{
  "station_reader": {
    "is_unique": true,
    "occu": "firefighter",
    "effects": [
      { "hook": "book.morale", "op": "mul", "value_table": { "1": 2, "2": 3, "3": 4, "4": 5 },
        "when": { "loctype": "fire_station", "inside": true } }
    ]
  }
}

What is still fixed in code is the action list: unique_actions only accepts the eight coded buttons (see Custom occupations), and an action's own flow - which tile it targets, what animation it plays, what it costs in AP - is not moddable. What those actions produce mostly is: firstaid reads firstaid.heal / firstaid.cooldown and its two gates, pray reads pray.morale, preach reads preach.duration, kindlespecial reads kindlespecial.fuel, and boost follows from cond.ap since it hands the character next turn's AP regen early. The three with nothing to scale are listen, focusdodge and lift - they reveal, dodge or pick up rather than produce a number. The tool actions read the hooks in Actions and production.

New normal perks also join the level-up pool of existing saves (they are mixed in when the save loads), so a normal-perk mod does not require starting a new game. Unique perks are handed out only when a character is created, so a new unique perk shows up from the next run, not on characters already in a save.

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 restores a base 6 HP on its own, and its cures and shorter cooldown come from hooks the Doctor's First Aider perk opens (gate.firstaid_sprain, gate.firstaid_bleeding, firstaid.cooldown), so an occupation that takes the action without that perk - or without a perk of your own declaring those hooks - gets the base heal only. 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. A name outside these types passes loading (only the field's type is checked) and simply never matches, with no log line.

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.

A new occupation's name allows 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). Patches to existing occupation names are not name-checked.

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; the mod's images/ folder is picked up automatically). 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 occupation unlocks, does not appear in the unlock popup or the history occupation grid, and grants no achievement. The run's score still counts toward the profile level and the trait points it grants, like any other run.
  • 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 and never reads the table again. A beverages entry works the other way around: the object stores only which container it is and reads the table on every use, so bottles whose entry is gone are removed from the save on load along with the rest of the mod's content.

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