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.json must not contain keys starting with _GameManager.compute_completion_pct() uses materials.size() as a denominator, so a _comment there would skew the completion percentage. Other files use a _comment key freely; DataRegistry and 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-exclusive source_type: exotic crafting material — never home-mined or Astrolabe-seeded) and hollow_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_uses and unlocked_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_recipe handle for trades.json → tool_offers: a scout offer with "gate_recipe": "iron_axe" stays hidden until CraftingUnlockManager.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:

"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 class slot) is per-run — it rides current_run.cosmetics_loadout in the save (SAVE_VERSION = 5), alongside current_run.skills and current_run.class. cosmetics_owned and the new-run loadout template live account-wide in config.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