Villager Tasks and Projects
The village periodically asks the digger for help: bring coal for the hearths, patch the
east wall, dig to a new depth. Each request is a "project" defined in data/projects.json.
Below: how a project is authored, how the game picks which ones appear and how often, how
asks and rewards scale with depth, and the one authoring rule you can't break. Every task
in the file is listed with its exact numbers.
In plain terms
Each entry in projects.json is a template. To spawn a task, the game picks an
eligible template, rolls the exact material amounts and reward inside the authored ranges,
and scales both by how deep the player has dug. The player can fulfil it (hand over the
materials for the reward), deny it (take a smaller penalty), or ignore it until it
expires (a larger penalty). A few task types carry no material cost. They ask the
player to do something — reach a depth, mine a new material, find a discovery — and the
reward is claimed when the objective is met.
Ranges in the JSON are written [low, high]. A material ask of "coal": [4, 8] means
"somewhere from 4 to 8 coal, before depth scaling."
Every field on a task template
| Field | Where it lives | What it does |
|---|---|---|
category |
projects.json → per task |
Which of the seven task families this belongs to. Sets the expiry window (below) and colours how the task reads. |
title / flavour |
projects.json → per task |
The card's headline and one-line story. No mechanical effect. |
materials |
projects.json → per task |
The delivery ask: one or more material_id: [low, high] ranges. Omitted on objective tasks that ask the player to do something instead. |
reward_type |
projects.json → per task |
The single reward if there is no reward_pool. One of food, water, gold, population, safety_boost, or a material id. |
reward_scale |
projects.json → per task |
[low, high] amount for the single reward, before depth scaling. |
reward_pool |
projects.json → per task |
Optional list of weighted reward options { "type", "scale": [lo,hi], "weight" }; the game rolls one per spawn so the same task can pay differently each time. Overrides reward_type/reward_scale when present. |
reward_bundle |
projects.json → per task |
Optional list that grants every listed reward at once (used by epic tasks). Each entry is { "type", "scale": [lo,hi] }. |
deny_penalty_type |
projects.json → per task |
What denying/ignoring hurts: food, water, safety, or none. |
deny_penalty_pct |
projects.json → per task |
[low, high] fraction lost on deny (e.g. [0.12, 0.22] = 12–22%). |
min_depth |
projects.json → per task |
The task cannot appear until the player has dug at least this deep (in tiles). |
max_depth |
projects.json → per task |
Optional. The task stops appearing once the player digs past this depth. |
spawn_weight |
projects.json → per task |
Relative chance of being picked among eligible tasks. Default 1.0; lower = rarer. |
objective_type |
projects.json → per task |
For non-delivery tasks: deliver (default), find_discovery, mine_material, or reach_depth. |
objective |
projects.json → per task |
The target for a non-delivery task, e.g. { "depth_delta": 25 } or { "material_id": "copper_ore" }. |
The seven categories
Category sets how long a task lives before it expires. The expiry windows themselves are a
script constant in ProjectManager (_EXPIRY_DAYS), not in the JSON; the JSON
category string only chooses which window applies.
| Category | Expiry window (in-game days) | Role |
|---|---|---|
emergency |
0.3–0.7 |
Short-fuse crises. Harshest deny penalties; must be answered fast. |
supply_run |
1.0–2.0 |
Everyday "bring us X material" asks. The staple of the feed. |
construction |
1.5–2.5 |
Build/repair the village; usually pays safety. |
craft_request |
1.5–2.5 |
Bring ore/fuel; the village crafts and hands back a finished good. |
exploration |
2.0–3.5 |
Go do something down there (mine, dig deeper, find a discovery). |
luxury |
2.0–3.5 |
Optional feel-good asks; deny penalty is usually none. |
epic |
3.0–5.0 |
Long, expensive multi-material asks that pay a whole reward bundle. Rare (spawn_weight 0.3). |
The rolled expiry is a random value inside the window. Ignoring a task until it lapses is
worse than denying it: expiry applies the deny penalty multiplied by 1.75
(_EXPIRE_PENALTY_MULT, a script constant).
How the feed is shaped
ProjectManager constants govern the rhythm of tasks, not JSON. Knowing them lets you read
what the JSON values will actually produce.
| Lever | Where it lives | Default | Effect |
|---|---|---|---|
| Base spawn interval | ProjectManager._SPAWN_INTERVAL_BASE |
0.5 days (±0.2 jitter) |
Roughly two tasks a day at baseline. |
| First task delay | ProjectManager._FIRST_SPAWN_DAYS |
1.5 days |
The opening task lands about halfway through day 2 so it does not crowd the start. |
| Depth speed-up | ProjectManager._DEPTH_FACTOR_PER_10 / _DEPTH_FACTOR_CAP |
+10% per 10 tiles, capped 3× |
Deeper digs get tasks faster, up to three times baseline. |
| Pile-up slowdown | ProjectManager._PILE_UP_FACTOR |
0.20 per open task |
Each task already waiting stretches the next interval, so the board does not flood. |
| Population gate | ProjectManager._try_spawn |
pop < 5 blocks spawning |
No tasks until the village has at least five people. |
Eligibility. A template can be picked only if the player's depth is at least its
min_depth and not past its max_depth, and, for delivery tasks, every asked material has
been reachable at the current depth. Objective tasks add their own checks. A reach_depth
task only appears while a full descent of its depth_delta still fits above the dig floor;
a mine_material task for a specific material stops offering once the player has already
dug that material.
How spawn_weight shapes the mix
Among the eligible templates, the game does a weighted random pick using each
template's spawn_weight (default 1.0 when omitted). A template at 0.5 surfaces about
half as often as a default one; an epic at 0.3 is deliberately rare. A low weight never
removes a task from the pool, it just comes up less. Use spawn_weight to keep demanding
asks — iron-heavy, deep-material — from dominating the feed while still letting them appear.
Raise a weight above 1.0 and the task shows up more often than baseline.
How ask budgets scale with depth
A depth scale multiplies both the material ask and the reward at spawn time:
1.0 + clamp(depth ÷ 100, 0, 3), i.e. from 1× at the surface up to 4× at depth 300 and
below (_DEPTH_SCALE_MAX = 4.0). So "coal": [4, 8] on a surface task asks for 4–8 coal,
but the same template deep down asks for up to four times that, and pays up to four times
the reward to match. That's why the authored ranges look small: they're the shallow
budget, and the game inflates them with depth. One exception: rewards of type
reinforced_frame are a fixed count and never depth-scale — a frame is one upgrade no
matter the depth.
Two reward conversions to know
- Safety rewards pay as strange root. A
safety_boostreward is converted at spawn time intostrange_root— the material the player spends to root-bind walls — using the per-bind cost (balance.json→walls→roots_required, default5). The card, the payout, and the completion signal all show the converted root count. - Food/water deny penalties are softened. A
foodorwaterdeny penalty drains a percentage of total stores (meter plus silo), so the authored percentages are scaled down by0.65(FOOD_WATER_PENALTY_SCALE) to keep the hit real but fair. Safety penalties are not scaled.
The reward-must-differ-from-ask rule
Never give a delivery task a reward material that it also asks for. The ask and the
reward both scale with the same depth multiplier, so rewarding an iron task with iron nets
the player a loss on deep spawns: they hand over 4× iron and get back 4× a smaller iron
number. This is authoring discipline, not something the code enforces. Nothing in
ProjectManager checks it, so keeping every delivery task's reward types distinct from its
materials keys is on the designer. Objective tasks that ask no materials are exempt —
there's nothing to double up on.
Every task in projects.json
Ranges are the authored (shallow) budget before depth scaling. "Reward" shows the single
reward_type/reward_scale, or "pool" / "bundle" where the task rolls or grants several.
supply_run
| id | Ask (range) | Reward | Deny | min_depth |
spawn_weight |
|---|---|---|---|---|---|
coal_supply |
coal [4,8] |
food [14,26] (pool: food/water/gold) |
food [0.12,0.22] |
0 |
(1.0) |
wood_supply |
wood [6,12] |
food [12,24] (pool: food/water/gold) |
food [0.10,0.18] |
0 |
(1.0) |
stone_supply |
stone [6,12] |
water [12,24] (pool: water/food/gold) |
water [0.10,0.18] |
0 |
(1.0) |
iron_supply |
iron [1,2] |
gold [14,22] (pool: gold/food) |
food [0.10,0.18] |
77 |
0.5 |
deep_coal_supply |
deep_coal [2,4] |
food [22,40] (pool: food/gold) |
food [0.15,0.25] |
80 |
0.7 |
sulfur_supply |
sulfur [3,6] |
water [16,30] (pool: water/gold) |
water [0.12,0.22] |
60 |
(1.0) |
quartz_supply |
quartz [1,2] |
gold [22,34] (pool: gold/food) |
food [0.10,0.18] |
100 |
0.5 |
kiln_fuel |
coal [3,6], clay [5,10] |
food [16,28] (pool: food/water) |
food [0.10,0.20] |
27 |
(1.0) |
forge_run |
iron_ore [2,4], coal [3,6] |
iron [2,3] |
water [0.12,0.22] |
77 |
0.8 |
charcoal_burn |
wood [6,12] |
coal [6,10] |
food [0.10,0.18] |
20 |
0.8 |
construction
| id | Ask (range) | Reward | Deny | min_depth |
spawn_weight |
|---|---|---|---|---|---|
wall_repair |
stone [5,10], clay [3,6] |
safety_boost [14,26] |
safety [0.12,0.22] |
0 |
(1.0) |
longhouse_patch |
wood [6,12] |
water [14,26] (pool: water/food) |
water [0.10,0.18] |
0 |
(1.0) |
watchtower_brace |
iron [1,2], stone [4,8] |
safety_boost [16,30] (pool: safety/gold) |
safety [0.15,0.25] |
40 |
0.6 |
deep_vault_work |
dense_stone [3,5], coal [3,6] |
safety_boost [18,34] (pool: safety/reinforced_frame) |
safety [0.18,0.30] |
80 |
0.8 |
iron_gate |
iron [2,3] |
safety_boost [20,36] (pool: safety/gold) |
safety [0.18,0.30] |
40 |
0.6 |
deep_reinforcement |
dense_stone [3,6], iron [1,2] |
safety_boost [20,36] (pool: safety/iron/steel) |
safety [0.15,0.28] |
113 |
0.8 |
aqueduct_lining |
limestone [3,6], clay [4,8] |
water [22,40] (pool: water/gold) |
water [0.15,0.25] |
201 |
0.9 |
craft_request
| id | Ask (range) | Reward | Deny | min_depth |
spawn_weight |
|---|---|---|---|---|---|
pickaxe_request |
iron [1,2], coal [2,4] |
water [16,28] (pool: water/gold) |
water [0.12,0.22] |
40 |
0.7 |
bronze_fittings |
copper_ore [3,5], tin_ore [3,5] |
bronze [2,3] (pool: bronze/gold) |
food [0.12,0.22] |
60 |
0.8 |
limestone_blocks |
limestone [4,8] |
water [16,28] (pool: water/gold) |
water [0.08,0.16] |
20 |
0.8 |
copper_pipe |
copper_ore [3,5] |
water [16,28] (pool: water/gold) |
water [0.15,0.25] |
50 |
0.8 |
iron_tools |
iron [1,2], coal [3,5] |
food [20,36] (pool: food/gold) |
food [0.15,0.25] |
40 |
0.7 |
smithy_smelt |
iron_ore [4,6] |
iron [2,3] |
food [0.12,0.22] |
77 |
0.7 |
smelter_charge |
coal [4,8], sulfur [2,4] |
food [18,32] (pool: food/crude_oil) |
food [0.12,0.22] |
52 |
(1.0) |
alloy_order |
copper_ore [2,4], iron [1,2] |
steel [1,2] (pool: steel/bronze/gold) |
safety [0.15,0.25] |
141 |
0.7 |
mortar_mix |
limestone [4,7], clay [4,8] |
food [22,36] (pool: food/gold) |
food [0.10,0.18] |
201 |
(1.0) |
deep_fuel_blend |
deep_coal [2,4], sulfur [2,4] |
crude_oil [3,5] (pool: crude_oil/deep_coal/gold) |
food [0.15,0.25] |
233 |
0.8 |
exploration
These use objective_type other than delivery where noted; they carry no material cost.
| id | Objective / Ask | Reward | Deny | Depth gate | spawn_weight |
|---|---|---|---|---|---|
fossil_hunt |
deliver fossil [2,4] |
population [1,1] |
none | min 30 |
(1.0) |
crystal_collection |
deliver quartz [1,3] |
food [26,46] (pool: food/gold) |
none | min 100 |
0.6 |
strange_root_study |
deliver strange_root [2,4] |
water [16,28] (pool: water/food) |
none | min 20 |
(1.0) |
chest_hunt |
find_discovery treasure_chest |
gold [18,30] (pool: gold/food) |
safety [0.05,0.10] |
min 40 |
(1.0) |
prospect |
mine_material any_new |
gold [10,18] (pool: gold/food) |
food [0.15,0.25] |
min 0, max 60 |
(1.0) |
copper_find |
mine_material copper_ore |
gold [14,24] (pool: gold/iron/bronze) |
safety [0.12,0.20] |
min 100, max 260 |
(1.0) |
dig_deeper |
reach_depth +25 |
safety_boost [18,30] (pool: safety/gold) |
safety [0.18,0.28] |
min 0, max 120 |
(1.0) |
emergency
| id | Ask (range) | Reward | Deny | min_depth |
spawn_weight |
|---|---|---|---|---|---|
maw_barrier |
stone [8,14], iron [1,2] |
safety_boost [24,42] |
safety [0.25,0.40] |
40 |
0.7 |
flood_warning |
clay [6,12] |
water [18,32] |
water [0.25,0.40] |
0 |
(1.0) |
fire_risk |
clay [4,8], dirt [6,12] |
safety_boost [14,26] |
safety [0.30,0.40] |
0 |
(1.0) |
emergency_patch |
stone [8,14], iron [1,2] |
safety_boost [24,44] |
safety [0.28,0.42] |
77 |
0.8 |
flood_seal |
clay [6,10], dense_stone [2,4] |
water [22,38] |
water [0.28,0.42] |
113 |
0.9 |
luxury
| id | Ask (range) | Reward | Deny | min_depth |
spawn_weight |
|---|---|---|---|---|---|
elder_gift |
quartz [1,2] |
population [1,1] |
none | 263 |
0.8 |
feast_supply |
coal [5,10] |
food [20,38] |
none | 0 |
(1.0) |
rare_minerals |
prismatic_shard [1,2] |
population [1,1] |
none | 113 |
0.8 |
epic
Epic tasks grant a reward_bundle (every listed reward at once) and are rare
(spawn_weight 0.3).
| id | Objective / Ask | Reward bundle | Deny | min_depth |
|---|---|---|---|---|
deep_bastion |
dense_stone [4,7], iron [2,3], limestone [3,5] |
safety_boost [30,50] + gold [20,32] + reinforced_frame [1,2] |
safety [0.20,0.35] |
201 |
great_descent |
reach_depth +50 |
gold [24,38] + steel [1,2] + safety_boost [20,34] |
safety [0.18,0.30] |
10 |
the_long_haul |
deep_coal [3,5], sulfur [3,5], iron [1,2] |
food [30,50] + gold [18,30] + crude_oil [3,5] |
food [0.18,0.30] |
233 |
relic_recovery |
find_discovery crystal_pocket |
gold [22,36] + quartz [2,3] + population [1,1] |
safety [0.10,0.20] |
201 |
Safe to change or handle with care
min_depthgates progression. A task whose asked materials only exist deeper than itsmin_depthstill gets filtered out by the reachability check, but amin_depthset too shallow can surface a task the player can't yet supply. Matchmin_depthto where the asked materials first appear.deny_penalty_pctonemergency/epicis the harshest in the game (up to 0.42 for safety). Combined with the short emergency expiry window and the1.75×expiry multiplier, ignoring one of these hurts. Tune with that stacking in mind.spawn_weightis a relative dial, not an on/off switch. Small changes rebalance the whole feed because every eligible template competes in the same weighted draw.- The reward-must-differ-from-ask rule isn't enforced by code. Double-check any new or
edited delivery task so its reward types never overlap its
materialskeys.
After editing projects.json, run the data validator and smoke test before shipping — see
The Safe Change Workflow.