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
- Run
WorkshopUploader.exewith the Steam client running. - Browse... to a mod folder containing
manifest.json. - Choose visibility and click Upload to Workshop.
- On first upload,
workshop_item.jsonis 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_recipeswith alternate components, optional tools, and a built-in sound cue (thesoundfield names an existing game sound; audio files can't be bundled)cooking_recipeswith a fire-based recipefoodsfor the cooking resultweaponsfor per-weapon balance overridesstructuresfor placeable item overrides (rain barrel capacity, snare trap catch chance)language_patchesfor English and Koreanimages/for image overrides, declared inmanifest.json(empty here; add.pngfiles 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,0for none). Omit to keep the default level-based budget.max_traits(number, default 4): trait slots selectable during character creation.stat_max(number, default5): 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, theall_aroundtrait, and thegrant.stattrait 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 default5is 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, default0.1): floor for per-hit melee condition loss after durability dampening. Lower it (e.g.0.001) so weapons with lowcondition_loss_combatcan wear far more slowly, or0to 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 to1to remove its extra wear (e.g.{"head": 1}disables the head penalty), or0so 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 numberscondition_loss_craft,charge_use: a number >= 0capacity: a number > 0name: a string- a new entry with a missing or unknown
parentsis skipped, as is a key that resolves to something that is not a registered item (such asweaponoritem)
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 leastsexand a stringname, 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_dropdefault to empty). A zombie still missing any of them is unregistered with a log line. The attributes may also come from the same mod'szombie_statspatch.zombie_stats(zombie_stats.json): keyed by the zombie'sname, not the class key.max_attack/max_action/vision/difficultyare numbers >= 0.max_hp/min_hp/armorare objects of numbers >= 0 per body part; together with what the class already has they must coverhead/body/legs(a partial object is fine on an existing zombie - it merges part by part).zombie_drops(zombie_drops.json): keyed byname;dropsandno_dropare 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, fromrat,squirrel,rabbit,bird. Indoors onlyratcan be caught, outdoors everything butrat; 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 inseafood.jsonhas asize).durability(number): dampens per-turn wear by1 - durability / 13. Wear is higher while holding live fish or when placed in the sea. When the trap breaks, itsscrapswash 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 by1 - 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, defaultdirty_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 theupgradetarget under theportable_generatorkey.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, defaulttrue): whether the trait shows on the companion customization screen. Setfalseto 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 theircost.
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: addvalueinto the total.mul: multiply byvalue(1is no change).gate: switch a behavior on (novalueneeded).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. Setbonus(default 0) to the per-copy number the description shows through{num}(where{num}=bonusx copies, rounded to two decimal places for display). - Unique perks (
"is_unique": truewith 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 theelitetrait (or an effect sets theunique_perk_maxlvhook). A unique perk with nooccufalls 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). Onlyvalue_perandvalue_tablescale with it; a flatvalueis a constant base thatpernever multiplies.value_per(number): amount added per unit of the index, on top of the basevalue(final amount =value+value_perx index). For "+10% per stack" use"per": "count", "value_per": 10— putting the 10 invalueinstead gives a flat +10% no matter how many copies stack (the game logs a warning for an inertperat 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 overridesvalue/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>andperk_<name>_descstrings. - Unique perks need
perk_<name>andperk_<name>_lv1throughperk_<name>_lv4. The maximum level is 3 by default, but theelitetrait (or aunique_perk_maxlveffect) raises it to 4 — without the_lv4string, elite characters see the raw key in the level-up popup. Avalue_tableshould likewise cover levels up to"4". - Every new perk should ship an icon at
images/perk/<name>.png(declare theimagespatch inmanifest.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 arestr(Strength),con(Health),obs(Observation),com(Combat),agi(Agility),dex(Dexterity) andbonus_points; each is an integer from 0 to 10 (bonus_pointsup to 60) and a key you omit is 0. A new occupation must have a validstart_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 thefoodsorbeveragestables,saltorpepper(the shakers), or the internal name of a registered item class - the game's item tables inresources/json(weapons, tools, medicines, clothing, seafood and so on) and classes your own mod adds through an item target such asweaponsorother_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 asjewelry.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;firstaidscales 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_perksmust name unique perks - a base occupation perk such asfirst_aider, or one your mod declares with"is_unique": trueand an"occu"through theperkstarget. A normal or special perk name is dropped at load: the level system (veteran traits, save files) and theperk_<name>_lv1-_lv3description strings shown in the occupation tooltip and the unlock popup only exist for unique perks.unique_traitsmay 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, plushouse,fortified_house,marina,military_base,research_center,terminus,checkpoint,danger,helipad,railroad,seasideandtunnel;inside_terminusandplatformare 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>andoccu_<name>_descstrings, plusoccu_loc_info_<name>if it hasidentify_locs(one line describing what it knows, shown in the occupation tooltip). - Ship an icon at
images/newgame/occu_<name>.pngfor the character creation panel (the base icons are 51x51 in a 52x52 cell) and declare theimagespatch inmanifest.json. A missing icon logs an error and renders blank rather than crashing.images/newgame/occu_<name>_gold.pngis the optional variant shown after a player survives a run as that occupation; without it the normal icon stays. images/occu/<name>.pngis 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 thefoodstable, since a food copies its values onto the object as it is created. Abeveragesentry 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 withy: 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'sresources/json/*.json)zombie [name]- spawn a zombie in the current location (defaultzombie_normal)npc <survivor|bandit|pickpocket|starving_elder>- spawn an NPC in the current locationhp/ap/str/con/obs/com/agi/dex/satiety/hydration/energy/morale/med/alcohol <n>- set a value on the selected characterlevelup- open the perk selection popup for the selected characterafflict <infection|zombified|disease|bleeding|sprain|burn>- apply a status ailment to the selected charactercure [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.
helpanddebugwork without confirmation; everything else requires thedebugconfirmation 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.typeand eachjson_patches[].target - Visibility chosen via the radio button (Public / Friends Only / Private)
- Preview from
preview.pngorpreview.jpgif present