ATMOSPHERICS WAVE · CIVILIAN STREAM

The valley keeps a day now — and learns to go quiet before the shooting

Diurnal pattern-of-life (#13) & the pre-contact "calm before" tell (#6) · lib/sim/ai/civilian.ts · 2026-06-06

Two things were missing from the soul of the valley. First, it never slept — villagers wandered 24/7 at constant population, day and night identical. Second, the flagship counterinsurgency tell the tutorial explicitly teaches — civilians melting away before an ambushdid not exist in the engine. A patient player had nothing to read. Both are fixed, deterministically, off data the simulation already computes, with zero new saved state.

1 · What a skeptic should check first: is it really there?

Everything below is produced by a headless probe you can run yourself — scripts/atmospherics-probe.ts. It does not trust the same clock the brain reads: the brain only sees time through sim.light, while the probe reads ground-truth time straight from world.secondsOfDay (an independent oracle). "Indoors" isn't a guess either — it's the exact snapped home point the mover actually walks to.

2 · The daily rhythm (diurnal pattern-of-life)

The World writes sim.light (ambient 0–1) every tick from the solar clock. The brain keys the outdoor occupancy to that light — a pure function of how bright it is:

The clever part is the restraint: occupancy is keyed to the magnitude of the light, which is symmetric across dawn and dusk — so the brain never has to tell a sunrise from a sunset. We model the thing that's seen and measured (how many are out), not the event.

The outdoor curve, by the hour, before → after (survey-7, the tuned seed):

HourLightBefore (flat)AfterReads as
06:000.53100%46%dawn — emerging
12:001.00100%83%full pattern of life
19:000.15100%33%dusk — melting in
00–040.05100%0%indoors, valley asleep
An honest residual. On a hot seed (survey-43, 50 civilians) the deep-night floor is ~13% of the population, not zero. That is not a bug in the rhythm: those villagers are correctly fleeing/clearing insurgents infiltrating the draws after dark (verified — an armed fighter 9–15 m from each of them). The gate accepts that real floor: night ≤ 22% of population and ≤ 35% of midday. Both seeds pass.

3 · The calm before — civilians melt away before the first shot

In the same single pass the brain already makes over nearby armed men, it now also flags staged fighters: an ambush cell holding its fire (brainState === "ambush") or an infiltrator moving concealed ("patrolling" + technique === "concealed") — alive, but not yet shooting — out to a 150 m sense radius (deliberately wider than the 45 m "they're on top of us" ring, so the gentle melt fires first). Sensed villagers quietly walk home, children first, and their departures are staggered by a seeded dice roll so the fields thin over a few seconds rather than teleporting empty. That thinning is the absence an alert player learns to read.

Measured 90 s before any round is fired (proven by asserting zero muzzle/blast effects during the window):

SeedThreatened cohort closes its distance homeControl cohortShots firedChildren
survey-7 (tuned)56% (4 → 2 out)0% — barely moves0lead
survey-43 (held-out)48% (18 → 9 out)24%0kids halve distance at t=42 s, before adults

The control cohort — outdoor civilians who can't sense any staged cell — staying put is what proves the drop is the signal, not the clock.

4 · Why you can trust it (determinism & the standing checks)

Every decision derives only from the unit-id hash (trait()), the seeded sim.rng, the deterministic sim.light, and sim unit state — never Math.random or the wall clock. The probe re-runs each seed twice and asserts an identical civilian-position fingerprint. No new field was added to the unit, so the save round-trip is byte-identical.

5 · The one thing deliberately left unbuilt (restraint logged)

A typed sim.dayFrac seam would be the cleanest way to tell a dawn event from a dusk event — but it needs editing files this wave doesn't own, and the occupancy design genuinely doesn't require it. A future feature that truly needs the distinction (roosters at dawn only, say) should add that sanctioned seam — not a render-side or WeakMap hack, which would break serialize-replay determinism.

Appendix · verbatim probe output

The full per-hour tables and gate lines are saved alongside this report: diurnal-survey-7.txt, melt-survey-7.txt, and the two …-survey-43-holdout.txt files. The diurnal gate block from the tuned seed:

  ── GATES ──
  night(00-04) outdoor avg 0.0  = 0.0% of pop, 0.0% of midday  [pop<=22% & midday<=35%]  PASS
  midday(12) outdoor 20/24 = 83%  [>=60%]  PASS
  rising edge: dawn(06) 11 < midday 20  PASS
  falling edge: dusk(19) 8 < midday 20  PASS
  DETERMINISM (diurnal): two same-seed runs civ-pos-hash 2884555957 vs 2884555957  PASS
ATMOSPHERICS-PROBE OK