Data Schemas Reference
The shape of every file in data/, loaded at startup by DataRegistry (see
DataRegistry and the Data Files).
Fields are read through .get("key", default), so most are optional; the tables
below mark the fields consumers actually read and give a real excerpt per file.
tools/validate_data.gd enforces the referential rules noted here — run it after
any edit.
Meta note:
materials.jsonmust not contain keys starting with_—GameManager.compute_completion_pct()usesmaterials.size()as a denominator, so a_commentthere would skew the completion percentage. Other files use a_commentkey freely;DataRegistryand the validator strip or skip it.
materials.json
Top level is material_id -> Dictionary. MaterialDef.from_dict() captures the
core physics fields; the economy and generation fields (trade_value,
durability_cost, material_tier, yield_weights, first_depth) are read
directly off the raw dictionary.
| Field | Type | Meaning |
|---|---|---|
id |
String | Material id (matches its key). |
display_name |
String | UI label. |
source_type |
String | terrain, forest, refined, supply, machine, processed, crafted, or exotic. |
dig_time / gather_time |
float | Base action seconds. |
terrain_hp |
float | HP to dig out a tile of this material. |
wall_hp |
float | HP of a wall built from it (0 = not a wall material). |
base_chew_resistance |
float | Maw resistance multiplier. |
is_wall_material / is_crafting_material / is_fuel |
bool | Category flags. |
is_machine |
bool | Present on machine-body pseudo-materials. |
trade_value |
int | Villager-economy anchor value. |
durability_cost |
float | Tool wear per dig (terrain materials). |
material_tier |
int | Overreach tier — a pickaxe one tier under pays 4x durability. |
first_depth |
int | Shallowest depth the material appears. |
yield_weights |
Array[int] | Weighted roll for variable yield (e.g. [94, 5, 1]). |
max_stack |
int | Inventory stack cap (used by consumables like dynamite, mining_charge). |
"stone": {
"id": "stone", "first_depth": 27, "display_name": "Stone",
"source_type": "terrain", "dig_time": 5.0, "gather_time": 5.0,
"terrain_hp": 50.0, "wall_hp": 150.0, "base_chew_resistance": 2.0,
"is_wall_material": true, "is_crafting_material": true, "is_fuel": false,
"trade_value": 3, "durability_cost": 1.0, "material_tier": 2,
"yield_weights": [93, 5, 2]
}
The Black Hollow feature adds
resonant_ore(a Black-Hollow-exclusivesource_type: exoticcrafting material — never home-mined or Astrolabe-seeded) andhollow_vault_block(is_wall_material,wall_hp: 9000,base_chew_resistance: 10.0— the toughest wall in the game). See Black Hollow.
tools.json
Top level is tool_id -> Dictionary. ToolDef.from_dict() captures speed and
allow-lists; max_durability, max_material_tier, family, tier, and
tool_type are read directly (e.g. by ToolState).
| Field | Type | Meaning |
|---|---|---|
id / display_name / description |
String | Identity and UI text. |
dig_speed_multiplier / gather_speed_multiplier |
float | Action-speed multipliers. |
allowed_materials |
Array[String] | Diggable material ids (empty = anything). |
allowed_resource_nodes |
Array[String] | Harvestable node types (e.g. "tree"). |
max_durability |
int | Durability; -1 = infinite (hands). |
max_material_tier |
int | Highest tier the tool digs without overreach. |
recipe_id |
String | Recipe that crafts it. |
family / tier |
String / int | Upgrade family grouping and tier. |
tool_type |
String | "processing" or "well" route ToolState.add_tool differently. |
storage_upgrades |
Array | Silo/tower upgrade tiers (cost, bonus). |
"pickaxe": {
"id": "pickaxe", "family": "pickaxe", "tier": 1, "display_name": "Pickaxe",
"dig_speed_multiplier": 2.0, "gather_speed_multiplier": 1.0,
"allowed_materials": ["dirt", "clay", "stone", "coal", "iron_ore", "sulfur", "dense_stone"],
"allowed_resource_nodes": [], "max_durability": 80,
"recipe_id": "pickaxe", "max_material_tier": 3
}
recipes.json
Top level is recipe_id -> Dictionary. RecipeDef.from_dict() captures the
craft fields; category and the unlock gates are read directly by
CraftMenu and CraftingUnlockManager.
| Field | Type | Meaning |
|---|---|---|
id / display_name / icon / description |
String | Identity and UI. |
category |
String | One of tools, structures, machines, components, processing, village. Any other value renders nowhere. |
input_materials |
Dictionary | material_id -> amount (all amounts must be > 0). |
output_id |
String | Must be a real material, tool, or machine. |
output_amount |
int | Yield per craft (must be > 0). |
craft_time |
float | Base seconds before crafting.speed_multiplier. |
required_layer |
String | Layer id that must be reached to unlock ("" = none). |
required_machine |
String | Processing machine that runs it (e.g. "smelter"). |
unlocked_from_start |
bool | Granted at run start. |
unlock_requires_machine |
String | Machine type whose placement unlocks it. |
unlock_requires_astrolabe_uses |
int | Ritual count required to unlock. |
unlock_requires_champion_skill |
String | Champion-class skill gate (class-landmark recipes) — the recipe is visible only to that skill's champion. |
unlock_requires_skill_node |
int | Minimum champion skill node for the gate above (the landmarks gate at node 3). Re-checked on EventBus.skill_leveled. |
unlock_requires_schematics |
int | Black Hollow schematic-fragment count required (account-wide, SaveManager.schematics_found()). Gates the Hollow Crucible build recipe (5); re-checked on EventBus.black_hollow_schematics_changed. |
The validator rejects a recipe that has both
unlock_requires_astrolabe_usesandunlocked_from_start— the start grant bypasses the ritual gate.
The class-landmark recipes (smokehouse, mead_hall, trading_post,
deepdrill, grand_anvil, bulwark) carry the champion-skill pair, e.g.
"unlock_requires_champion_skill": "digging", "unlock_requires_skill_node": 3.
CraftingUnlockManager._class_node_gate_open resolves the gate. See
Skill Tree and Classes.
A recipe id doubles as a
gate_recipehandle fortrades.json → tool_offers: a scout offer with"gate_recipe": "iron_axe"stays hidden untilCraftingUnlockManager.is_unlocked("iron_axe")(its inputs have been seen).
"smelt_iron": {
"id": "smelt_iron", "display_name": "Smelt Iron", "category": "processing",
"input_materials": {"iron_ore": 3, "coal": 2},
"output_id": "iron", "output_amount": 1, "craft_time": 35.0,
"required_layer": "", "required_machine": "smelter"
}
machines.json
Top level is machine_type -> Dictionary. Two broad kinds share the file:
drills/excavators (fuel-burning miners, is_processor: false) and processors
(is_processor: true, with a recipe_id). Structures and special buildings
(Astrolabe, apothecary, pump) carry extra flags.
| Field | Type | Meaning |
|---|---|---|
id / display_name / description |
String | Identity and UI. |
footprint_w / footprint_h |
int | Tile footprint. |
is_processor |
bool | Runs recipe_id when true. |
recipe_id |
String | Recipe consumed (processors). |
selectable_recipes |
Array[String] | Multi-recipe processors (e.g. glazing kiln, Grand Anvil). |
recipe_picker_label |
String | Per-machine processor recipe-picker header (e.g. "FORGE OUTPUT"). |
work_rate |
float | Dig/process rate. |
max_fuel |
float | Fuel tank capacity. |
fuel_material |
String | Fuel id (e.g. Astrolabe's strange_root). |
durability_min / durability_max |
int | Placed-durability roll range. |
repair_cost / repair_materials / repairable |
int / Dictionary / bool | Repair economy. |
max_placed |
int | Placement cap (0 = single-use/explosive; falls back to a default when omitted). |
dig_width / directional |
int / bool | Excavator width and rotate-on-place. |
is_structure / is_pump / is_explosive / requires_well |
bool | Special behaviours. |
process_section |
String | UI grouping (e.g. "buildings"). |
no_fuel |
bool | Landmark buildings/structures that draw no fuel (Trading Post, Mead Hall, Bulwark). |
support_bonus |
int | Flat village-support bonus while placed (Mead Hall). |
gold_per_pop |
float | Passive gold per population per interval (Trading Post). |
food_per_day / food_target |
float | Passive food output / cap (Smokehouse). |
wall_line |
bool | Placed on the defence wall line, not the open surface (Bulwark). |
root_radius / brace_mult_normal / brace_mult_surge / brace_seconds / rest_seconds |
int / float | Bulwark auto-root radius and brace duty-cycle tuning. |
The apothecary entry adds a brewing schema the validator checks in detail:
doses_by_fuel (fuel id -> doses, each must be is_fuel), water_per_dose,
stored_water_floor, dose_target_default/dose_target_max, and a three-entry
dose_levels array (label, save_chance in (0,1], ingredients, and a
cost on tiers beyond the first).
The Black Hollow hollow_crucible is a class-agnostic single-recipe processor
(is_processor: true, recipe_id: "cast_hollow_vault_block", no
selectable_recipes) — the smelter/fossil-grinder pattern.
"drill": {
"id": "drill", "family": "drill", "tier": 1, "display_name": "Drill",
"work_rate": 5.0, "max_fuel": 500.0, "footprint_w": 1, "footprint_h": 1,
"is_processor": false, "durability_min": 50, "durability_max": 50, "max_placed": 3
}
layers.json
An object with a single layers array; DataRegistry stores the array. Each
entry is a depth band (underground is positive Y).
| Field | Type | Meaning |
|---|---|---|
id / display_name / description |
String | Identity and UI. |
main_material |
String | Dominant material id. |
depth_start / depth_end |
int | Inclusive tile-depth range. |
blend_depth |
int | Transition blend into the layer above. |
color_hex |
String | Band color. |
ore_inclusions |
Array | {material, chance} scatter rolls within the band. |
{
"id": "stone", "display_name": "Stone", "main_material": "stone",
"depth_start": 27, "depth_end": 51, "blend_depth": 3, "color_hex": "#8e8e8e",
"ore_inclusions": [ {"material": "coal", "chance": 0.09} ]
}
discoveries.json
Top level is discovery_id -> Dictionary — underground pockets found while
digging.
| Field | Type | Meaning |
|---|---|---|
id / display_name / description |
String | Identity and UI. |
chance |
float | Base spawn chance. |
layers |
Array[String] | Layer ids the discovery can appear in. |
layer_chances |
Dictionary | Per-layer spawn chance override. |
reward_type |
String | survival, inventory, maw_resonance, plague_spore, hazard, and others. |
reward |
Dictionary | Reward payload, e.g. {"food": [3, 8]} (min/max range). |
layer_rewards |
Dictionary | Per-layer reward override (must be a spawn layer). |
variety_names |
Array[String] | Flavour names (e.g. mushroom varieties). |
color |
Array[float] | RGB hint color. |
"water_pocket": {
"id": "water_pocket", "display_name": "Water Pocket", "chance": 0.04,
"layers": ["clay", "stone", "shale_coal", "iron_rock"],
"reward_type": "survival", "reward": {"water": [5, 20]},
"color": [0.40, 0.70, 0.95]
}
black_hollow.json
Two top-level arrays: expeditions (the five Wren's Expeditions routes) and
notes (the 15-note chronicle). Loaded by DataRegistry
(get_expeditions / get_expedition / get_chronicle_notes); the validator
cross-checks material_pool / exotic_pool ids against materials.json and
layer_id against the layers. See Black Hollow
and Black Hollow Expeditions.
| Field (expedition) | Type | Meaning |
|---|---|---|
id / display_name / layer |
String | Identity and UI. |
layer_id |
String | Home layer that must be reached (by depth) to unlock the route. |
entry_cost |
int | Gold charged per attempt. |
start_depth |
int | Ladder rows before digging begins. |
days_away |
float | Village time a successful trip costs. |
cooldown_days |
float | Wren's rest after a successful bank (game-days). |
material_pool |
Array | {id, w} weighted common-material roll. |
exotic_pool |
Array[String] | Exotic ids that can appear (must exist in materials.json). |
bands |
Array | {depth, mult} — reached-depth gold multiplier curve (banked gold only). |
exotic_from / relic_from / schematic_from / resonant_from / vein_from |
int | Depth floors (tiles below the dig face) below which each special can appear. |
Each notes entry is {id, act, title, voice, text} — an ordered fragment of
Black Hollow's history, logged account-wide (SaveManager.chronicle_notes) when
its relic is banked. Special spawn rates and the schematic/resonant per-descent
caps are code constants in BlackHollowOverlay, not data — see
Black Hollow Expeditions.
"expeditions": [
{
"id": "rusted_works", "display_name": "Rusted Works", "layer": "Iron-ore workings",
"layer_id": "iron_rock", "entry_cost": 200, "start_depth": 6, "days_away": 0.6, "cooldown_days": 3,
"material_pool": [{"id": "stone", "w": 4}, {"id": "iron_ore", "w": 3}],
"exotic_pool": ["ancient_clay"],
"bands": [{"depth": 0, "mult": 1.0}, {"depth": 18, "mult": 1.5}, {"depth": 40, "mult": 2.0}, {"depth": 66, "mult": 2.5}],
"exotic_from": 8, "relic_from": 20, "schematic_from": 34, "resonant_from": 12, "vein_from": 20
}
],
"notes": [
{"id": "n01", "act": 1, "title": "Founding stone", "voice": "Alder", "text": "Broke ground where the old road ends. …"}
]
projects.json
Top level is project_id -> Dictionary — villager tasks. Categories seen in
data: supply_run, construction, craft_request, exploration,
emergency, luxury, epic.
| Field | Type | Meaning |
|---|---|---|
category |
String | Task family (drives spawn logic and UI). |
title / flavour |
String | UI text. |
materials |
Dictionary | material_id -> [min, max] amount asked. |
reward_type |
String | food, water, gold, safety_boost, population, none. |
reward_scale |
Array[int] | [min, max] reward magnitude. |
reward_pool |
Array | Weighted alternative rewards ({type, scale, weight}). |
deny_penalty_type |
String | Penalty resource on refusal (none = no penalty). |
deny_penalty_pct |
Array[float] | [min, max] fraction penalty. |
min_depth |
int | Depth gate before the task can spawn. |
spawn_weight |
float | Relative spawn frequency. |
"coal_supply": {
"category": "supply_run", "title": "Coal for the hearths",
"materials": { "coal": [4, 8] }, "reward_type": "food",
"reward_scale": [14, 26],
"reward_pool": [
{ "type": "food", "scale": [14, 26], "weight": 2.0 },
{ "type": "gold", "scale": [8, 14], "weight": 1.0 }
],
"deny_penalty_type": "food", "deny_penalty_pct": [0.12, 0.22], "min_depth": 0
}
balance.json
An object of tuning sections, each a key -> number dictionary. Scripts read
via balance.get(section).get(key), so a missing key silently falls back to a
hardcoded default — tools/validate_data.gd gates every key the code reads via
REQUIRED_BALANCE_KEYS. Sections and a sample of their keys:
| Section | Sample keys |
|---|---|
survival |
food_drain_per_second, water_drain_per_second, work_mult, idle_mult, storage_cap, well_trickle_rate |
village |
base_population, food_per_person_per_day, shelter_target_days, wall_hp_per_person, artificer_population, building_milestones |
maw |
start_x, grace_period, base_chew_rate, pressure_growth_per_day, pressure_max, hunger_step_thresholds, repel_pushback, breach_stun |
walls |
max_height_units, mix_bonus_per_type, roots_required |
dynamite / mining_charge |
fuse_min, fuse_max, blast geometry, blast_food_dmg, blast_water_dmg |
world |
collapse_max_range, collapse_fill_near, collapse_fill_far |
time |
day_duration |
crafting |
speed_multiplier |
trading |
basic_efficiency, astrolabe_efficiency, astrolabe_auto_interval |
astrolabe |
chew_increase_base, chew_increase_volume, exotic_chance_per_use, exotic_chance_cap |
skills |
node_fractions, node_pct, champion_fill_mult, inherit_factor, anchors, champion_class, class_info |
"maw": {
"start_x": 620.0, "grace_period": 360.0, "base_chew_rate": 1.0,
"pressure_growth_per_day": 0.04, "pressure_max": 5.0,
"hunger_step_thresholds": [1.25, 1.5, 2.0, 2.5, 3.0, 4.0, 4.5]
}
The skills block
The skill/class tuning. SkillManager / SkillMath read it via
DataRegistry.balance.get("skills", {}).
| Key | Type | Meaning |
|---|---|---|
node_fractions |
Array[float] (7) | Cumulative XP threshold per node, as a fraction of a skill's node-5 anchor ([0.02, 0.06, 0.15, 0.40, 1.00, 2.25, 4.50]). |
node_pct |
Array[float] (7) | Cumulative % bonus per node ([0.04, 0.10, 0.18, 0.30, 0.45, 0.60, 0.75]). |
champion_fill_mult |
float | Champion effective-XP multiplier — half the raw cost (2.0). |
inherit_factor |
float | Fraction of the % bonus an heir keeps on lineage death (0.5). |
anchors |
Dictionary | Per-skill node-5 XP: {digging 4000, foraging 500, crafting 800, building 20000, population 80, trading 60}. |
champion_class |
Dictionary | skill -> class (digging miner, foraging hunter, crafting smith, building mason, population chieftain, trading merchant). |
class_info |
Dictionary | Per-class UI + handicap data (below). |
Each class_info.<class> entry: name, skill, emblem, accent (hex),
landmark, landmark_hint, blurb, effect_label, capstone,
capstone_hint, train_hint, and handicap = {label, mults}. mults is an
area -> multiplier map read by SkillManager.class_penalty(area); the areas
in use are tool_durability, dig_speed, craft_speed, harvest_yield,
trade, wager, and population (direction baked into the value).
"skills": {
"node_fractions": [0.02, 0.06, 0.15, 0.40, 1.00, 2.25, 4.50],
"node_pct": [0.04, 0.10, 0.18, 0.30, 0.45, 0.60, 0.75],
"champion_fill_mult": 2.0, "inherit_factor": 0.5,
"anchors": {"digging": 4000, "foraging": 500, "crafting": 800,
"building": 20000, "population": 80, "trading": 60},
"champion_class": {"digging": "miner", "foraging": "hunter", "crafting": "smith",
"building": "mason", "population": "chieftain", "trading": "merchant"},
"class_info": {
"miner": {"name": "Miner", "skill": "digging", "landmark": "The Deepdrill",
"effect_label": "dig speed", "capstone": "Rich Veins",
"handicap": {"label": "Rough on tools — they wear 15% faster.",
"mults": {"tool_durability": 1.15}}}
}
}
See Skill Tree and Classes for the full class table and application sites.
trades.json
The villager visit economy. Top level is a set of named arrays plus a wager
object.
| Key | Shape | Meaning |
|---|---|---|
material_food_trades |
Array | {material, cost, food_per} — units of material for food. |
gold_trades |
Array | {gives_id, gives_amount, gold_cost, type}; type is supply (to bars) or item (to inventory). Optional challenge gate. |
tool_offers |
Array | {tool_id, display, ...}. Priced either by material-cost keys (barter, e.g. dirt/clay) or by gold_cost. Optional consumable (bool — repeatable stackable buy → inventory, capped to max_stack) and gate_recipe (string — hide the row until that recipe is unlocked). |
cosmetic_offers |
Array | {id, gold_cost} cosmetics for sale. |
wager |
Dictionary | Scout's Wager: cost, odds, exotic_pool, consolation_pool. |
"material_food_trades": [
{"material": "dirt", "cost": 3, "food_per": 1},
{"material": "clay", "cost": 1, "food_per": 1}
],
"gold_trades": [
{"gives_id": "food", "gives_amount": 5, "gold_cost": 10, "type": "supply"}
],
"tool_offers": [
{"tool_id": "pickaxe", "display": "Pickaxe", "dirt": 50, "clay": 25},
{"tool_id": "iron_axe", "display": "Iron Axe", "gold_cost": 500, "gate_recipe": "iron_axe"},
{"tool_id": "dynamite", "display": "Dynamite", "gold_cost": 250,
"consumable": true, "gate_recipe": "dynamite"}
]
buildings.json
Bell-tower village buildings. Top level is building_id -> Dictionary.
| Field | Type | Meaning |
|---|---|---|
display |
String | UI label. |
metric |
String | food, shelter, or safety (validator-enforced). |
champion / pitch |
String | Elder name and flavour. |
costs |
Array[Dictionary] | Per-level material costs (costs[i] = level i+1). |
bonus |
Array[int] | Flat support added to metric at each level. |
"farm": {
"display": "Farm", "metric": "food", "champion": "Elder Maren",
"costs": [
{"wood": 20, "stone": 10},
{"wood": 10, "iron": 5, "gold": 30}
],
"bonus": [30, 65, 110]
}
cosmetics.json
Three top-level keys: slot_order (Array of slot names), items
(item_id -> Dictionary), and dyes (dye_id -> Dictionary).
| Field (item) | Type | Meaning |
|---|---|---|
slot |
String | Slot the item fills (form, skin, headwear, hair, beard, tunic, boots, extra, class). |
name |
String | UI label. |
kind |
String | color, style, or style_color. |
source |
String | free, milestone, discovery, scout, wager, secret. |
default |
bool | The humble starting variant for its slot. |
color |
String | Hex color (color/style_color kinds and dyes). |
trigger |
String | Milestone id that unlocks it (e.g. "iron_pickaxe", "mastery:black_rot", "champion:miner"). |
hidden_until_owned |
bool | Hidden from the customizer until owned. |
effect |
Dictionary | Gameplay multipliers, e.g. {"dig_speed_mult": 1.4}. |
"boots_furcuff": {
"slot": "boots", "name": "Fur-cuffed Boots", "kind": "style",
"source": "discovery", "effect": {"dig_speed_mult": 1.1}
}
The class cosmetic slot
slot_order ends with a dedicated class slot (additive to extra), the home
of the class signature accessories. Two families:
- Signature accessories — one per class,
source: free(always equippable), auto-seeded for the calling on new game:class_ropecoil(Miner),class_hunterkit(Hunter),class_apron(Smith),class_toolbelt(Mason),class_pauldrons(Chieftain),class_moneybag(Merchant).class_noneis thedefault: truevariant. - Prestige variants —
class_*_prestige,source: milestone,trigger: "champion:<class>",hidden_until_owned: true. Granted account-wide byCosmeticManagerwhen the class's champion reaches node 7.
"class_ropecoil": {"slot": "class", "name": "Miner's Rope", "kind": "style", "source": "free"},
"class_ropecoil_prestige": {"slot": "class", "name": "Miner's Gilded Line", "kind": "style",
"source": "milestone", "trigger": "champion:miner", "hidden_until_owned": true}
dyes are simpler {name, color, source, default} entries for the tunic dye
picker, read via DataRegistry.get_cosmetic_dye().
The equipped loadout (including the
classslot) is per-run — it ridescurrent_run.cosmetics_loadoutin the save (SAVE_VERSION = 5), alongsidecurrent_run.skillsandcurrent_run.class.cosmetics_ownedand the new-run loadout template live account-wide inconfig.json. See Save System and Migration.
codes.json
Redeemable codes, keyed by the lowercase SHA-256 hex digest of the uppercase
code string so plaintext never ships in the pck. The validator rejects any key
that is not a 64-char hex digest. Player input is normalized and hashed by
DataRegistry.get_code().
| Field | Type | Meaning |
|---|---|---|
repeatable |
bool | false = one redemption per account (tracked in config.json). |
rewards.tools |
Dictionary | tool_id -> count (routed to ToolState.add_tool). |
rewards.materials |
Dictionary | material_id -> amount (routed to Inventory.add). |
rewards.cosmetics |
Array[String] | Cosmetic ids (routed to CosmeticManager.unlock). |
"1ac05da52cdab0440527ec1cd116f8e277b5feebc5c79f4fd1fa7b53e9ccd06a": {
"repeatable": false,
"rewards": {
"tools": { "quartzite_pickaxe": 1 },
"cosmetics": ["form_axel"]
}
}
Key files
scripts/resources/DataRegistry.gd— loads and serves every file above.tools/validate_data.gd— referential validator (DATA OK/DATA FAIL).scripts/resources/MaterialDef.gd,ToolDef.gd,RecipeDef.gd— typed wrappers.