# 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.gd` → `DATA OK`) and the smoke test. See [The Safe Change Workflow](/docs/underroot/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.0` → `900.0` s | Drought length; also sets severity (longer = more severe). | 240–1200 |
| `drought_well_trickle_min` / `_max` | `0.60` → `0.40` | Well trickle multiplier — the well slows. | 0.2–0.9 |
| `drought_dig_speed_min` / `_max` | `0.90` → `0.80` | Dig-speed multiplier — the dry ground digs harder. | 0.7–1.0 |
| `drought_water_drain_min` / `_max` | `1.09` → `1.18` | Village + digger water-drain multiplier — the heat makes everyone thirstier. | 1.0–1.4 |
| `drought_pocket_mult_min` / `_max` | `0.50` → `0.30` | Water-pocket spawn-roll multiplier — the ground is parched. | 0.1–1.0 |
| `drought_caches_min` / `_max` | `1` → `2` | 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

- **`day_duration` is a global lever.** It rescales the Maw's escalation clock, survival drain framing, villager daily ticks, and every "per day" cadence at once — not just weather. Don't reach for it to change how often it rains. See [The Levers That Matter Most](/docs/underroot/the-levers-that-matter-most).
- **Don't move the storm timings.** `storm_warning_duration` and `storm_duration` are matched to the storm track; changing them desyncs the music even though they're now JSON keys.
- **Mind the min/max direction on drought and pockets.** Severity lerps min → max, and for those the severe end is the lower number. Edit the pair, then verify in-editor that a long event feels worse, not better.
- **Any new proportional population source needs its own ceiling.** The aurora is the cautionary tale: the daily tick it competes with is flat, so a percentage term will outgrow it at scale unless it is capped.
- Everything else here is a safe data edit: change the number, run the validator and smoke test, and check the feel in the editor.

## Related

- [Maw Threat and Escalation](/docs/underroot/maw-threat-and-escalation)
- [Survival and Supply Drain](/docs/underroot/survival-and-supply-drain)
- [The Astrolabe Consequence Curve](/docs/underroot/the-astrolabe-consequence-curve)
- [Challenge and Harrow Modifiers](/docs/underroot/challenge-and-harrow-modifiers)
- [Where Balance Lives](/docs/underroot/where-balance-lives)
- [Weather (developer)](/docs/underroot/weather)