Survival and Village
Two systems keep the base alive. SurvivalManager drains the digger's food and water and the storage that buffers them; VillageManager owns population, the three support metrics, buildings, and the daily tick.
Both are autoloads. World.gd's _process heartbeat calls SurvivalManager.process_survival(delta) and VillageManager.process_village(delta) every frame; VillageManager also does one-shot bookkeeping each in-game day in response to EventBus.new_day.
SurvivalManager
scripts/village/SurvivalManager.gd holds the digger's two live meters and the storage that overflows into them.
Meters, storage, and the well
| Field | Meaning |
|---|---|
food, water |
The live 0–100 meters. These are the source of truth for survival. |
stored_food, stored_water |
The village stores. Overflow past a full meter banks into them from day one; the matching building (food_silo / water_tower in ToolState.owned_tools) gates only the reverse flow — stores topping the digger's meter back up. |
well_water |
Reservoir filled by the pump machine; trickles into water at WELL_TRICKLE_RATE. |
well_built |
Set true once the well has ever held water. |
_dead |
Latch set on death; blocks further draining. |
Tuning is read from DataRegistry.balance["survival"] in _ready(), with hardcoded fallbacks: FOOD_DRAIN_PER_SECOND and WATER_DRAIN_PER_SECOND default to 0.04, STORAGE_CAP to 500.0, WELL_TRICKLE_RATE to 0.04. Storage caps scale with upgrades: food_storage_cap() returns STORAGE_CAP + ToolState.silo_upgrade * 500.0 (and the water tower mirror).
The drain tick
process_survival(delta) computes the per-frame drain and applies it:
var work_mult := WORK_MULT if GameManager.is_working else 1.0
var idle_mult := get_idle_mult() if not DisplayServer.window_is_focused() else 1.0
var food_drain := FOOD_DRAIN_PER_SECOND * delta * work_mult * idle_mult \
* CosmeticManager.get_effect_mult("food_drain_mult") \
* ChallengeManager.food_drain_mult()
Four multipliers stack onto the base rate:
work_mult—WORK_MULT(default2.0) whileGameManager.is_working, else1.0. Active digging burns supplies twice as fast.idle_mult— when the window is not focused, drain slows toget_idle_mult(); focused play uses1.0.- Cosmetic and challenge multipliers — traits like the Mourning Sash or The Dave slow drain, challenges can raise it.
get_idle_mult() returns IDLE_MULT (default 0.15) unless offline_mult_override is set — the donate-gated Settings slider, range 0.01–0.30, persisted in settings.json rather than the run save.
After draining, the well trickles into water, then storage tops the meters back up toward 100 when a silo/tower is owned. Emissions are throttled: food_changed, water_changed, and well_changed only fire when the value moves at least _EMIT_THRESHOLD (0.5) or hits zero, because per-frame emits drove a full HUD relayout every tick.
Death
Death is evaluated on the totals, where water_total includes well_water (the trickle is rate-limited and can lag):
var food_total := food
var water_total := water + well_water
Reaching zero on either emits EventBus.player_starving / player_dehydrated, sets _dead, builds a cause string ("starvation", "dehydration", or "starvation and dehydration"), and emits EventBus.player_died(cause).
Offline death is handled differently:
OfflineSimulatorcallsmark_dead()(which sets_deadwithout emitting) so it can construct the "while away" cause string itself. Do not rely onprocess_survivalto fireplayer_diedfor offline sessions.
Adding, draining, and offline
add_supplies({"food": x, "water": y})fills the meter to 100 and banks any overflow into the village stores (capped) — no building required. The stores are the village's larder (the daily tick eats from them regardless of buildings), and the elder chain's first task depends on gathering past full raisingfood_support. The Silo/Tower gate only the stores→meter top-up inprocess_survival(and add +500 cap each).add_food/add_waterare thin wrappers;add_well_waterfills the well.drain_food_pct(pct)/drain_water_pct(pct)drain a fraction of total stores (meter + silo), meter first — used by denied/expired project penalties so a silo can't trivially refill the loss.simulate_offline(elapsed_seconds)mirrors the live tick's modifiers (idle mult, cosmetic, challenge) but omitswork_mult, drains the well at full rate, and returns{food_consumed, water_consumed}. It only drains the digger's share; the village's share is drawn separately in the offline sim.is_alive()reports the same total logic used in the tick;revive()clears_deadfor the lineage carry-on;reset(...)restores a fresh run.
VillageManager
scripts/village/VillageManager.gd owns the surface community: how many souls live there, how well supported they feel, which buildings exist, and the once-a-day resolution.
Population and the support metrics
population starts at BASE_POPULATION (default 47, from DataRegistry.balance["village"]). peak_population is a monotonic high-water mark that never dips when villagers are lost, so run-progress-scaled systems (e.g. the Scout's Wager ante) don't cheapen after a storm.
Three floats measure how well the village is provided for, all recomputed by _refresh_metrics():
| Metric | Source | Notes |
|---|---|---|
food_support |
_calc_food_support() |
(food + stored_food) * FOOD_WEIGHT + (water + stored_water) * WATER_WEIGHT plus the farm building bonus. |
shelter_support |
_calc_shelter_support() |
"Days of fuel reserve" scaled to population, plus the longhouse bonus. |
safety_support |
_calc_safety_support() |
Wall HP, dig rate, and depth rate components, plus the watchtower bonus. |
Shelter support is buffer-based: weighted fuel stock on hand (wood, coal, deep coal, crude oil, ethanol — each with its own SHELTER_*_WEIGHT) divided by daily need gives coverage_days; holding SHELTER_TARGET_DAYS (default 12) worth scores exactly population, capped at SHELTER_COVERAGE_CAP (2.0) × population. Until the elder chain graduates the player (village_chain_done) or day 7, a smaller SHELTER_TARGET_DAYS_APPRENTICE (default 4) applies.
Safety support sums three weighted components plus the watchtower bonus:
var wall_component := (_cached_wall_hp / WALL_HP_PER_PERSON) * 0.60 * (1.0 - _safety_debuff_pct)
var tile_component := minf(tile_rate / TILE_TARGET_RATE, 2.0) * population * 0.30
var depth_component := minf(depth_rate / DEPTH_TARGET_RATE, 2.0) * population * 0.10
_cached_wall_hp is only rescanned when _wall_hp_dirty is set (a wall built/destroyed/removed), because inventory_changed — the most frequent _refresh_metrics() trigger — never affects wall HP.
_refresh_metrics() is wired to many EventBus signals in _ready(): wall_built/wall_destroyed/wall_removed (which also dirty the wall cache), storage_changed, inventory_changed, and — importantly — food_changed/water_changed, so a task reward or penalty updates the food metric on the click rather than at end of day.
Safety scars
Denied or expired safety tasks call apply_safety_debuff(pct, days), appending a scar {pct, remaining} to _safety_scars. _recompute_safety_debuff() caches the summed magnitude, clamped to SAFETY_DEBUFF_CAP (0.75), into _safety_debuff_pct so the hot path reads one float. Scars heal on their own timer, ticked down in process_village(delta); a healed scar triggers _refresh_metrics() to restore that much safety. Scars are persisted (with legacy single-scar back-compat in load_from_save).
Villager visits
process_village(delta) also drives the automatic trade visit: when SurvivalManager.food <= FOOD_THRESHOLD (default 18), _visit_cooldown has expired, and _auto_visit_armed is true, it calls _trigger_visit() (emits villager_arrived). Challenge runs start with _auto_visit_armed = false so an experienced player isn't interrupted on spawn; ringing the bell (call_villager()) re-arms it. _on_villager_left() starts the VISIT_COOLDOWN (default 120 s). The trade UI and economy constants live in scenes/ui/VillagerVisitPopup.gd, not here.
Buildings
Three buildings exist — farm (food), longhouse (shelter), watchtower (safety) — defined canonically in data/buildings.json (loaded over the BUILDING_DEFS fallback in _ready()). Each has three levels; costs[i] is the material cost for level i+1 and bonus[i] is the flat support added at that level.
The bell offers upgrades. get_building_options() returns building ids below level 3. choose_building(id) queues a build and calls start_construction(), which charges the cost immediately if affordable (announcing the spend via a toast — the only place a building's gold leaves the inventory) and sets active_build with build_days_left = 1. A build the player can't yet afford stays in queued_build and auto-starts on the daily tick once affordable.
The daily tick
_on_new_day() (bound to EventBus.new_day) runs the day's resolution in order:
_consume_village_resources()— drawspopulation * FOOD_PER_PERSON_PER_DAY(and water) from storage with ±15% variance, idle-scaled when unfocused._consume_shelter_fuel()— adds today's need to_shelter_debt, then_flush_shelter_debt()burns whole inventory units cheapest-first (wood → coal → crude oil → deep coal → ethanol). Fractional carry-over keeps the average burn exact; negative debt is a pre-payment credit carried forward._consume_black_rot()— Black Rot challenge only: the village needs onestrange_rootper soul per day from day 2 on; the unfed shortfall is culled and recorded as blighted graveyard crosses viaGameManager.record_black_rot_deaths.- Construction:
start_construction()then_process_construction()(only for builds already active at day start). _tick_population()— the migration tier check (below).- Milestone/bell bookkeeping:
_check_building_milestone,_check_population_milestone,_check_fallback_bell,_try_ring_bell. _emit_daily_journal(...)— writes the elder journal lines (Maren's stores draw, Bjorn's fuel burn, Siv's Maw wall damage).
_tick_population() compares min_metric (the lowest of the three supports) against population bands: ≥ 1.30× brings a family plus solo arrivals, ≥ 1.10× a family, ≥ 0.80× a drifter; below 0.60× and 0.40× villagers flee in growing numbers. A grace window (first 3 days, plus two days after graduated_day) suppresses decline only — growth is never graced. Population is floored at 1.
Bells and milestones
BUILDING_MILESTONES (population thresholds, overridable from balance) each credit one bell into _owed_bells when crossed; _check_fallback_bell tops up _owed_bells after FALLBACK_BELL_DAYS (default 6) of stalled growth. _try_ring_bell() surfaces one owed bell at a time — but holds if a growth cutscene fired this day (_celebrated_today) or a specialist arrival popup is active (SpecialistManager.is_popup_active()), so the two big modals never collide. try_ring_pending_bell() lets a finishing specialist popup surface a held bell promptly. _check_population_milestone() fires the 50-pop population_milestone cutscene (highest threshold only when several are crossed at once, e.g. offline catch-up).
Offline and breach helpers
VillageManager exposes several helpers the offline sim and Maw call into:
simulate_shelter_offline(days_passed, idle_mult)andsimulate_black_rot_offline(days_passed)mirror the daily fuel burn and rot cull over the away window. The rot is a discrete daily tax and is not idle-multiplied.simulate_hunger_offline(dry_days, idle_mult)applies the_tick_populationhunger tiers on the idle clock, silently, for the days stores sat empty.idle_window_seconds(food_total, water_total)returns how long food/water lasts the digger plus the village at idle drain — the single source for the supply-ETA readout (classic BottomBar; HearthCrest on the talisman HUD) and the storage-warning math.apply_breach_losses(pct, floor_pop)kills a fraction on a Maw breach (never belowfloor_pop, never increasing), and re-arms the growth cutscenes so survivors re-cross thresholds.send_off_successor()removes one villager for the lineage carry-on.
get_save_data() / load_from_save() serialize population, buildings, bell state, visit cooldown, and safety scars; keys must stay in sync between the two.
Key files
scripts/village/SurvivalManager.gd— digger food/water/storage/well drain, death check, offline drain.scripts/village/VillageManager.gd— population, support metrics, buildings, daily tick, bells, villager visits.data/buildings.json— canonical farm/longhouse/watchtower definitions (costs and bonuses).