Weather and Storms

Underroot's weather is four kinds — rain, drought, storms, and the aurora — and all of it is tunable from data now. Every number lives in data/balance.json under the weather section, read by WeatherManager.gd and WeatherSchedule.gd. This page lists each one with its exact default, what it does, and a safe range, so a balance pass can reach it without a developer.

This replaces the old guidance that "most storm tuning is not in the JSON." That was true of the first weather build; the reworked system moved everything into balance.json. If you still have that model in your head, drop it.

How duration becomes magnitude

The system runs on one idea: duration is magnitude. When an event is scheduled it rolls a duration inside that kind's _duration_min/max, and where it lands (0..1) is its severity. Every consequence is that severity lerped between a _min and a _max value. A long drought is a severe drought; a short rain is a light one.

Storms are the exception: they are locked to a fixed 154 s to match their music, so they roll a severity randomly (uniform 0..1) instead of from duration. Everything else about them still lerps between min and max the same way.

Read the min/max pairs as "mild end" → "severe end," not "smallest number" → "largest number." For drought, several severe-end values are numerically lower (e.g. the well trickles at 0.6 in a mild drought, 0.4 in a severe one). The severity lerp always goes min → max regardless.

After any edit here, run the data validator (tools/validate_data.gdDATA OK) and the smoke test. See The Safe Change Workflow.

Scheduling and cadence

One event is pending at a time, picked by weight = 1 / mean interval days. These govern how often weather comes and how it's spaced.

Key Default Effect Safe range
time.day_duration 360.0 s The base unit. All intervals below are counted in game days, so this scales every weather gap (and the whole game's pacing) at once. 240–600
rain_interval_days_min / _max 3.0 / 5.0 Mean gap between rains (the reciprocal is rain's pick weight). 2–8
storm_interval_days_min / _max 15.0 / 20.0 Mean gap between storms. 10–30
drought_interval_days_min / _max 8.0 / 14.0 Mean gap between droughts. _min is also the hard spacing floor. 6–20
drought_first_day 12.0 Earliest a drought can occur in a run — past the tutorial and the no-water-infrastructure window. Rain/storm have no _first_day, so they use their interval min. 8–20
min_gap_days 0.5 Fallback minimum clear days after any event, if a kind has no <kind>_min_gap_days. 0.25–1
rain_min_gap_days 0.5 Minimum clear days after rain. 0.25–1
storm_min_gap_days 2.0 Minimum clear days after a storm. 1–4
drought_min_gap_days 2.0 Minimum clear days after a drought. 1–4
burst_window_days 6.0 The window the burst brake watches. 4–10
burst_max_events 3 More than this many events inside the window forces a clear stretch. 2–5
burst_clear_days 2.0 Length of that forced calm. 1–4

Overall weather occupancy sits near 30%; the gap and burst rules keep that from clumping into one punishing stretch.

Rain (harmless)

Rain refills water and helps digging. Fill is spread across the event, not dumped at the start.

Key Default Effect Safe range
rain_duration_min / _max 45.0 / 90.0 s Rain length; also sets severity. 30–150
rain_fill_pct_min / _max 0.08 / 0.22 Fraction of each water source's capacity a rain adds (pond always, well if built, tower if owned). 0.05–0.35
rain_dig_speed_min / _max 1.10 / 1.20 Dig-speed multiplier while raining (wet ground digs easier). 1.0–1.4
rain_pocket_mult_min / _max 1.5 / 2.0 Water-pocket spawn-roll multiplier — rain soaks the ground. 1.0–3.0

Drought (a slow squeeze)

Drought tightens water, slows digging, and drains the pond to expose bed caches. Milder droughts sit at the _min end; severe ones reach _max.

Key Default (mild → severe) Effect Safe range
drought_duration_min / _max 360.0900.0 s Drought length; also sets severity (longer = more severe). 240–1200
drought_well_trickle_min / _max 0.600.40 Well trickle multiplier — the well slows. 0.2–0.9
drought_dig_speed_min / _max 0.900.80 Dig-speed multiplier — the dry ground digs harder. 0.7–1.0
drought_water_drain_min / _max 1.091.18 Village + digger water-drain multiplier — the heat makes everyone thirstier. 1.0–1.4
drought_pocket_mult_min / _max 0.500.30 Water-pocket spawn-roll multiplier — the ground is parched. 0.1–1.0
drought_caches_min / _max 12 Pond-bed caches exposed as the water recedes (gold / root / exotics, worth a dive). 0–4
drought_departure_reserve_days 2.0 If village-accessible water (stored_water + well_water) falls under this many days of need, one villager leaves per day of drought. Runs alongside the normal metric-shortfall departures. 1–4

Two drought numbers are script constants, not JSON — a developer edits them:

Constant File Value Effect
DROUGHT_POND_EVAP_FRACTION WeatherManager.gd 0.7 Fraction of pond capacity that evaporates across a full drought. The receding water is what exposes the caches.
Farmer's-call threshold WeatherManager.gd pond < 60% When the farmer calls out the glinting bed (once per drought).

Storms (deadly)

A storm runs warning → active → resolution. Timing is music-locked; every loss is severity-driven and JSON-tunable.

Key Default Effect Safe range
storm_warning_duration 58.0 s Warning phase before damage (matched to the storm music intro). Changing this desyncs the music — treat as fixed. 58 (avoid)
storm_duration 96.0 s Active storm before losses resolve. Also music-matched. 96 (avoid)
storm_deaths_min / _max 3 / 14 Villagers killed (severity-lerped, rounded). Clamped so population never drops below 1. 0–25
storm_core_loss_min / _max 0.04 / 0.18 Fraction of each core material destroyed (plus one random extra the player holds). 0.02–0.30
storm_root_loss_min / _max 0.25 / 0.55 Fraction of held strange root destroyed. 0.1–0.7
storm_bind_strip_min / _max 0.20 / 0.55 Fraction of root-bound wall columns that lose their binding. 0.1–0.7
storm_wall_col_min / _max 0.15 / 0.55 Fraction of each Maw front's wall columns torn out (contiguous, inward from the chew front). With Two Fronts both walls are hit, each from its own Maw. 0.1–0.7

Storm structural constants (script, not JSON):

Constant File Value Effect
STORM_CORE_MATS WeatherManager.gd ["dirt","clay","stone","coal"] Materials a storm always hits. Excludes iron on purpose.
STORM_CUTSCENE_DEFER WeatherManager.gd 6.0 s Re-check delay when a cutscene owns the screen, so a storm never opens on top of one.

When a storm resolves, all water sources fill completely — fixed behavior, not a tunable.

Aurora (a night boon)

The aurora is a reward, not a hazard: a night that gives more yield, slows the Maw, and brings souls to a fed village. It runs on its own season track, not the weighted scheduler.

Key Default Effect Safe range
aurora_first_day_min / _max 5.0 / 8.0 Day the first season opens — reachable, so players meet it early. 3–12
aurora_second_gap_min / _max 12.0 / 16.0 Gap before the second season (front-loaded so the post-first stretch isn't a letdown). 8–20
aurora_gap_min / _max 18.0 / 23.0 Gap before every later season. 12–30
aurora_nights_min / _max 2 / 3 Nights budgeted per open season. 1–4
aurora_season_window_days 6.0 Window the season's nights must fall within before it closes. 4–10
aurora_night_fire_chance 0.7 Per-clear-night chance the aurora actually lights (so nights aren't always consecutive). 0.4–1.0
aurora_yield_base 0.40 Base chance of a bonus dig unit on a lit night (before decay/strength). 0.2–0.6
aurora_chew_relief_base 0.35 Base Maw chew reduction on a lit night. Effective chew_mult is floored at 0.05, so the Maw never fully stops. 0.2–0.5
aurora_soul_pct_base 0.05 Base soul-arrival fraction on a lit night — only pays out to a fed village, and capped by the two keys below. 0.02–0.10
aurora_soul_cap_min / _max 14 / 19 Ceiling on the souls one lit night can draw. Rolled fresh each night inside this range, so the number stays unpredictable. 8–30
aurora_astrolabe_strength 1.3 Reward multiplier once the Astrolabe has been used (pre-ritual strength is 1.0). Applies to yield and chew relief — not to the soul cap. 1.0–1.8

Aurora reward decays across a season's nights on a script constant AURORA_NIGHT_DECAY = [1.0, 0.7, 0.5] (WeatherSchedule.gd) — first night full, later nights less. The night window itself (day_time ≥ 0.78 or < 0.22) is also script-side. Aurora bonuses are paused during the offline catch-up sim, so they can't be farmed by closing the tab.

Why the souls are capped

aurora_soul_pct_base is a percentage of the village, but the ordinary daily population tick is flat-additive — its best tier gives at most +9 people per day at any village size. Uncapped, the aurora was the only proportional term in the system and ran away at scale: post-ritual it drew ~52 souls in one night at population 800, and a three-night season compounded to roughly +15% of the whole village.

The cap decays across a season on the same AURORA_NIGHT_DECAY vector as the rewards, so the season still fades instead of landing three identical nights:

Night 1 Night 2 Night 3 Season
Ceiling 14–19 10–13 7–10 31–41

It starts binding around population 215 (post-ritual). Below that the percentage is the smaller number and still rules, so early-game auroras are untouched — which is the point, since that is where the proportional design was doing its job.

The cap deliberately ignores aurora_astrolabe_strength. A post-ritual aurora burns brighter for yield and chew relief, but the souls are a hard top-out. If you want brighter auroras to draw bigger crowds too, that is a code change in WeatherSchedule.aurora_consequence(), not a JSON edit.

Tune it against the ~800 practical population ceiling, not against theoretical maximums — the surrounding systems are built for the 200–800 band. Raising the pair much past ~30 re-opens the runaway; dropping it under ~8 makes the aurora's soul beat vanish as a felt reward.

What you CAN tune from Challenges, not raw JSON

The Eye of the Storm challenge and the Harrow's "sky" dial multiply weather cadence through ChallengeManager.rain_interval_mult() / storm_interval_mult() (script-driven, not free-form JSON). These scale the intervals and the spacing floors together — floors only ever compress (clamped ≤ 1), so a stormy challenge stays relentless while a calmer one keeps the standard quiet rules.

Handle with care