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:

get_idle_mult() returns IDLE_MULT (default 0.15) unless offline_mult_override is set — the donate-gated Settings slider, range 0.010.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: OfflineSimulator calls mark_dead() (which sets _dead without emitting) so it can construct the "while away" cause string itself. Do not rely on process_survival to fire player_died for offline sessions.

Adding, draining, and offline

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:

  1. _consume_village_resources() — draws population * FOOD_PER_PERSON_PER_DAY (and water) from storage with ±15% variance, idle-scaled when unfocused.
  2. _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.
  3. _consume_black_rot() — Black Rot challenge only: the village needs one strange_root per soul per day from day 2 on; the unfed shortfall is culled and recorded as blighted graveyard crosses via GameManager.record_black_rot_deaths.
  4. Construction: start_construction() then _process_construction() (only for builds already active at day start).
  5. _tick_population() — the migration tier check (below).
  6. Milestone/bell bookkeeping: _check_building_milestone, _check_population_milestone, _check_fallback_bell, _try_ring_bell.
  7. _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:

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