# CLAUDE.md Guidance for Claude Code when working in this repository. ## Project A C++/raylib simulation/game for creating semi-realistic fantasy & sci-fi worlds/planets. Built in phases. Between phases the user can edit the world or trigger events (meteor impact, magical events, sci-fi terraforming). **Core principle:** the planet geometry is FIXED. Cells (vertices/faces) never move. Only their *properties* flow over the fixed grid (Eulerian, not Lagrangian). This keeps the data structure stable across all phases and makes erosion, climate and drift far simpler to implement later. ## Roadmap The project has two big arcs. **World Creation** builds a plausible planet through a set of geological/environmental stages that **overlap and run together on a geological clock** (My) — they are not strict sequential "phases" (the code keeps `phase*` names internally for save/config compatibility, but think of them as continuous stages). Once a world is "done", the long-term goal is a separate **Live World** mode that runs the finished planet at a *much* slower, real-time-ish clock (hours/days/weeks/months) with dynamic weather and life. **World Creation (geological clock, mostly done):** - **Tectonics & landmass** *(done)* — icosphere geometry, plate assignment, boundary stress forming mountains (convergent) and trenches/rifts (divergent). The initial "forming" pass settles to isostatic equilibrium, then plate motion continues. - **Continental drift & erosion** *(done)* — real plate motion (cm/yr, My), the plate lifecycle (fission, stalemate kick, spreading-born plates, merging), diffusive erosion + a sea-level controller (~30% land), taller persistent mountains (collision + arc + isostatic persistence), seafloor aging→depth. `planet.cfg` + `planet.save`. See [[phase2-design-direction]]. - **Hydrology** *(done)* — rivers, lakes and fluvial erosion as a macro drainage network (depression-fill→lakes, steepest-descent→rivers, mass-conserving stream-power incision). Runs at a finer timestep alongside continuing drift. See [[phase3-hydrology]]. - **Climate** *(done)* — continuous per-cell temperature + orographic precipitation (`Planet::computeClimate`); runs from forming onward (a live "base climate" that updates as terrain changes). Color modes `6`/`7`. - **Biomes** *(done)* — per-cell `Cell.biome` (13 biomes incl. polar Ice) from elevation + the climate fields (`Planet::classifyBiomes`), color mode `5`, saved per cell. - **Biota — flora, fauna & funga** *(done — see `docs/fauna-flora-plan.md` + `docs/fauna_generation_plan.md`)* — two layers: per-cell **density scalars** (flora, fauna, funga ∈ [0,1]) derived from the climate fields each tick (drive the colour views), plus a discrete **slot/point population** of broad archetypes (Class/Order/Family/Size), generated **on demand** (`L`) and **saved** (save v7). Fauna is a herbivore/carnivore/ omnivore food chain (predators gated on local prey); funga uses a flora-like but moisture/organic-matter-led rule. The living/evolving ecosystem is reserved for Live World. - **Seasons (obliquity)** *(done)* — `axialTilt` drives per-cell summer/winter temperatures (`computeClimate`: `sTempSummer`/`sTempWinter` = annual mean ± a tilt/latitude/continentality amplitude); winter temp feeds the Tundra/Taiga biome cutoffs (`biomeSeasonWeight`). Static fields (the live yearly cycle is reserved for Live World). Color key `6` cycles the temp views. > Durable design context (module layout, save format, climate/biome model, conventions) > lives in **`docs/design-notes.md`** — important because Claude's auto-memory does not > travel with the repo. **Live World (in progress):** with planet creation finished, run the world at a slow real-time scale with dynamic **weather** (clouds, rain, storms, fronts), day/night, and living **ecosystems / civilization** evolving in real time. This is a separate large effort; the fixed-grid Eulerian model + the climate fields are the groundwork for it. - **Clock + day/night groundwork** *(done — see `PlanetLive.cpp`)* — a slow real-time clock (hours → up to ~20 years/sec, `liveTime`/`liveRate`, key `W` to enter once settled, `[`/`]` ramp the rate), a moving **day/night terminator** (sun from time-of-day rotation + seasonal declination via `axialTilt`; key `N`), a raylib-free per-cell **insolation field** (`computeInsolation`, the weather hook), a **live seasonal temperature** cycling the static summer/winter fields over the year (`computeLiveSeason`), and a **moving snow / sea-ice line**. Day/night + snow are render overlays over any colour mode (3D + 2D). Save **v8** adds the live-clock state. Still **future:** actual weather, day/night temperature swing, precipitation/evaporation, living/evolving ecosystems. - **Moons + tides + distant sun** *(done — see `PlanetOcean.cpp`)* — **1–3 moons** generated per world from a separate RNG (no tectonic perturbation) and **saved (v9)**; they orbit on the live clock and, with the sun, raise an **equilibrium tide** (`computeTides` → `sTide`, two bulges via `cosθ²−⅓`, semidiurnal). Tides show as a **tide-coloured coastline** (`buildCoastline` + a diverging `tideColor`, key `T`, 3D + 2D). The 3D **sun** is now small + far with a faint halo; **moons** render with sun-lit **phases** (offset-dark-sphere) + faint **orbit rings**, plus **eclipses** — solar (a moon transiting the sun darkens a shadow spot in the day/night overlay), lunar (a moon in the planet's shadow dims red). Knobs `tideAmplitude`/`tideSunFactor`. - **Ocean currents (+ climate feedback)** *(done — see `PlanetOcean.cpp`)* — `computeOceanCurrents()` builds a per-ocean-cell tangent velocity from wind stress (`sWind`) + Coriolis deflection (right N / left S) + coast-following (gyres) + smoothing. `computeClimate()` calls it and feeds **warm (poleward) / cold (equatorward)** currents back into `sTemp` as a bounded coastal anomaly (`climateCurrentFactor`), so biomes shift naturally. Rendered as warm/cold **current arrows** over the sea (key `O`, 3D + 2D). This completes the Live World ocean/sky pass. - **Weather — dynamic clouds & rain** *(done — see `PlanetWeather.cpp`)* — a per-cell humidity/cloud/rain cycle advanced on the live clock: **evaporate** over warm sunlit seas → **advect** humidity & cloud along the prevailing wind → **condense** into cloud (extra on windward upslopes) → **rain** out → **dissipate**. Rendered as a translucent moving cloud shell (white → dark storm where it rains) over the globe + 2D map (key `K`). Saved (v10). - **Weather — moving systems (lows, hurricanes & typhoons)** *(done — see `PlanetWeather.cpp`)* — drifting low-pressure **agents** (`WeatherSystem`) spawn over warm tropical seas / mid-latitude oceans, travel with the steering wind (poleward recurve), intensify over warm water, decay over land, and **stamp** travelling cloud/rain onto the grid — so the sky visibly evolves. The intense tropical ones are **hurricanes/typhoons** (spin by hemisphere, eye + animated spiral marker). Saved (v11, so a load resumes active storms). This makes the weather visibly move (the base field alone relaxes to a static pattern). - **Volcanoes & volcanic islands** *(done — see `PlanetVolcano.cpp`)* — on entering Live World a one-time pass (`placeVolcanoes`, separate RNG → tectonic determinism intact) seeds volcanoes by tectonic context: **very high** probability on young spreading-ridge / "new-plate" cells (baby plates), **medium** on normal plate borders, **low** elsewhere (hotspots). They are stateful lifecycle agents: some start pre-built, growing vents can breach submarine cells into volcanic islands, tall vents can go dormant, dormant vents explode and shave their peak, then puff ash while regrowing weaker. Volcano state + `sVolRng` are captured in the Live World step-back snapshot, so `,`/`.` reverses height, dormancy, explosions and ash timers. Rendered as growing, dormant and post-explosion cone markers (3D + 2D, key `V`); saved (v15). Knobs `volcano*`. **Civilizations (in progress — the long arc after the world is finished):** the eventual goal is people who eat, name their world, found villages→cities, build kingdoms/empires, draw cultural + geographic borders, and go to war. Built in phases (cell = territory, settlements = point agents, all on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived ecoregions atlas):** - **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the foundation everything civic references. `Planet::generateGeography()` extracts named features from the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land), **oceans** (the connected world ocean split into basins by a distance-from-land watershed → ~4–6 named oceans) **/ seas** (small water bodies), **lakes** (inland filled basins), **mountain ranges + peaks** (connected high terrain), **rivers** (largest discharge mouths traced upstream via `flowTo`) — and names each with a deterministic procedural namer (`NameGen`: syllable banks, a "language" per continent so a region's places share a sound). A separate RNG (`sGeoRng`) keeps tectonic determinism intact. Generated once on a settled world (key `M`, in or out of Live World), drawn as labels on the globe + 2D map (minor features only when zoomed), listed in an **Atlas** tab (click a row to fly there), and shown in cell-info as a "region" line. Per-cell feature-index arrays give O(1) "which features is this cell in" (the hook for territory/borders later). Names dedupe on the proper-noun root (no two features share a base name); a **new volcanic island** is named on the fly when it breaches (the island-formation event carries its name). Saved (**v17**). Knobs `geo*`. - **Ecoregions (ecological provinces)** *(done — see `PlanetEcoregions.cpp`)* — `generateEcoregions()` flood-fills cells sharing a biome + land/water context + productivity band into named ecological provinces (dominant flora/fauna/funga archetype + flora/fauna/funga productivity per region), a separate `sEcoRng`. Key `E` (colour mode `Ecoregion` + an **Eco** tab); saved (**v19**; v18 added a geography reshuffle salt). Productivity feeds settlement habitability. - **Settlements & habitability (Step 2)** *(done — see `PlanetCiv.cpp`)* — a derived per-cell **habitability/food** score (`computeHabitability`: climate comfort + water access (rivers/lakes/ coast) + food (flora/fauna density + ecoregion productivity), gated by freezing winters / high terrain; colour mode `Habitability`, key `I`). On key **`U`** ("the dawn") `placeSettlements()` seeds a fixed set **once** by **habitability-weighted random sampling** (weight = habitability^`civClusterExp`) with a **soft Gaussian suppression** (`civMinSpacingRadians`) around each pick — so settlements **cluster** on good land (rivers/coasts/fertile valleys) at irregular spacing rather than an even lattice (separate `sCivRng`; named from the continent's `NameGen` bank). `stepCivilization(dtHours, liveTime)` runs each live frame: population moves **logistically toward a food-driven carrying capacity**, but everything is **environment-driven and dynamic** (not the old "grow the same everywhere"): the growth **rate** scales with habitability (fertile cells boom, marginal crawl); the capacity `K = civMaxPopulation·habitability· **siteQuality**·conditions` where **siteQuality** spreads max size by orders of magnitude (a great river — `discharge` is log-scaled — or a coast hosts a metropolis, a dry inland cell a town); and **conditions** are deterministic time-varying drivers (pure functions of (region, year, seed) → constant within a year, reversible on step-back): **regional droughts** (multi-year, worse in arid regions), year-to-year **harvests** (bigger swings in continental interiors), rare **cold years** and river **floods**, plus **volcano ash**. **Storms** over a town kill people directly (hurricanes worst, `civStormDeathRate`/`civHurricaneDeathMult`); sustained famine / acute disasters can **collapse** a settlement to ruins (kept in the set, can revive). Tiers village→town→city; markers (3D + 2D) sized by tier and **withered-tinted** by hardship + a **Civ** tab + cell-info "conditions/drought" line + kind=3 `WorldEvent`s ("X grew into a city", "Hurricane devastates X", "Famine shrinks X to a Town", "X was abandoned"). The set is fixed, so the step-back snapshot only restores the per-settlement **population** vector; conditions recompute. Saved (**v20**). Knobs `civ*`. - **Territory, nations & political borders (Step 3)** *(done — see `PlanetNation.cpp`)* — settlements are grouped into **realms** and claim land. `computeTerritory()` (deterministic, no RNG): each living settlement projects an **influence range** that scales with population (`civTerritory*`); **realm grouping** processes settlements largest→smallest — a settlement joins the nearest larger **capital** whose annexation reach (`civVassalRange`) covers it (a vassal town → kingdom) else founds its own nation; tier = **City-state / Kingdom / Empire** by member count / total pop (`civEmpireMinMembers`/ `civEmpirePop`); each land cell goes to the settlement maximising `range − distance` (else **wilderness** −1), so its nation is that settlement's → **influence-limited territory with wilderness frontiers**. Borders trace the per-cell `cellNation()` edges (the plate dual-contour reused as `buildNationBorders`). Render: a **Territory** colour mode (`nationColor`) + dark border lines + realm labels at capitals (key **`P`**), a **Realms** tab, a cell-info realm line, and kind=4 `WorldEvent`s ("The Kingdom of X is founded", "X rises to an Empire", "the X collapsed"). Territory + nations are a **pure function of the (saved) settlements**, so they're **recomputed** (each sim year / on placement / load / step-back) — **no save state, no version bump**, peaceful & population-driven. Knobs `civTerritory*`/ `civVassalRange`/`civEmpire*`. - **Culture, beliefs & governments (Step 4)** *(done — see `PlanetCulture.cpp`)* — settlements on the same continent share a **culture** (a people/language family). `computeCultures()` (deterministic, no RNG, runs right after `computeTerritory()`) groups living settlements by continent (`Settlement.regionId`) into **one culture per inhabited continent**, each with: a generated **people name** ("the Velmar", from the continent's `NameGen` bank), a dominant **ethos** picked from the aggregate environment of its cells (coastal→**Seafaring**, mountains/hills→**Highland**, desert/dry→**Nomadic**, fertile→**Agrarian**, else a hash-picked **Mercantile/Warlike**), and a **religion** — a `Faith` focus biased by the dominant biome (coast→Sea, mountains→Sky, desert→Sun, cold→Ancestors, forest→Harvest…) plus a generated **faith name** ("the Tidewardens"). Each **realm** (Step-3 `Nation`) also gets a **government** (`GovType`) from its tier + a deterministic pick (City-state→Republic/Chiefdom/Theocracy, Kingdom→Kingdom/Duchy/Theocracy, Empire→ Empire/Autocracy/Confederation), folded into its **name** ("Republic of X", "Duchy of X", "X Theocracy", "X Confederation", "X Dominion"…) — **culture-driven renaming**. **Culture-driven grouping**: realm vassalage is now restricted to the **same continent**, so every kingdom/empire is **mono-cultural**. Render: a **Culture** colour mode (`cultureColor`, cultural blocs over the political map) + pale **cultural-region borders** + a **Cultures** tab (key **`X`**), a cell-info culture/faith line, and the government-aware realm labels. Like territory, cultures are a **pure function of the (saved) settlements + geography**, so they're **recomputed** each sim year / on placement / load / step-back — **no save state, no version bump**. Knobs: none (ethos/faith/government rules are internal constants). - **Conflict, war & shifting borders (Step 5)** *(done — see `PlanetConflict.cpp`, save v21)* — realms stop coexisting peacefully and **go to war**. `stepConflict(year)` runs once per sim year (in the viewer's year-tick, before `computeTerritory`): neighbouring realms (adjacency by **settlement proximity**, since territory is influence-limited) grow **hostile** from ambition (size gap) + ideology (culture/faith difference) + contested-frontier + a per-pair yearly streak, and **declare wars** (cap `warMaxConcurrent`). Each war-year runs a **battle** (`warscore += (strA−strB)/(strA+strB)·rand`, strength = realm `totalPop` × a Warlike-ethos bonus × defender home advantage), inflicts **casualties** on each side's frontier city, and once a side is clearly winning it **conquers** a loser frontier city — its `sSettleAllegiance` **flips to the victor's capital** (or, with `warSackChance`, the city is **sacked** to ruins). `computeTerritory()` honours allegiance (overriding the mono-cultural rule), so **borders move** as cities change hands. Conquered foreign/distant cities **revolt** over time (contestable), and a realm that loses its capital collapses — so **empires rise and fall**. Wars are **stateful & path-dependent** (unlike the derived Steps 3–4): the state — per-settlement allegiance + active `wars` + a separate war RNG (`sWarRng`, tectonic determinism intact) — is **saved (v21)** and **snapshotted** so `,`/`.` rewind conquests/revolts. Render: **red war-front** lines + a red at-war marker in the **Realms** tab (with an active-wars list) over the Territory view (`P`), a cell-info "AT WAR" flag, an active-war HUD count, and **kind=5** `WorldEvent`s ("The X Empire declares war on the Y Kingdom", "The X captures/sacks Z", "Z revolts against the X", "Peace between…"). Knobs `war*`. - **Diplomacy, alliances & coalitions (Step 6)** *(done — see `PlanetConflict.cpp`, save v22)* — wars stop being isolated 1-v-1 grudges. Each pair of nearby realms carries an **attitude** (−1..+1) that **drifts** each year in the diplomacy pass of `stepConflict` (shared vs different culture/faith affinity, plus a penalty while at war, plus recovery under a truce, plus a per-pair personality noise), crystallising via thresholds (hysteresis) into a **`DiploTie`** = Alliance / Non-aggression / Rival. **Allies never fight** (war declaration skips allied/non-aggression/truced pairs) and **join each other's wars** — when A attacks B, each ally of B declares its own war on A (a **coalition**, via the existing 1-v-1 machinery), so an aggressor is worn down on many fronts. Hostility now rises as **attitude falls** (rivals war), and a war ends in a real **peace treaty** (a `truceUntil` + a lasting attitude **grudge**). Ties are keyed on **capital settlement index** (the same stable realm id wars use) and, like war, are **stateful** — saved (**v22**) + snapshotted (step-back rewinds alliances/rivalries). Render: **alliance (green) / rivalry (dark-red) arcs** between capitals over the Territory view (`P`), an allies/rivals summary per row + the active-wars list in the **Realms** tab, a cell-info "allies/rivals" line, and **kind=6** `WorldEvent`s ("The X and the Y form an alliance", "…become rivals", "…sign a non-aggression pact", "Z joins the war against W"). Knobs `diplo*`. - **Trade & economy (Step 7)** *(done — see `PlanetTrade.cpp`)* — settlements no longer prosper on local food alone. `computeTrade()` (deterministic, no RNG, runs in `rebuildTerritory` **after** `computeTerritory`/`computeCultures`) builds **trade routes** between nearby settlements — a **sea** route when both are coastal (a much longer reach), a **river** route on rivers, else **overland** — with a volume from both populations × proximity × a per-kind bonus × a **political factor** (blockaded to ~0 between realms **at war**, boosted `tradeAllyBonus` between **allies**). Each settlement's **prosperity** = a small base + Σ its link volumes, so a settlement central to many strong routes becomes a rich **hub**. Prosperity **multiplies the carrying capacity** in `stepCivilization` (`K ·= 1 + tradeProsperityWeight· prosperity`), so well-connected coastal/river hubs grow into far bigger cities while isolated/blockaded towns lag. Two light feedbacks close the loop (read last year's economy in `stepConflict`): **trade partners** get a diplomacy attitude nudge (`tradeDiploBonus` — *reward alliance*) and a **wealthy weak neighbour** raises war hostility (`tradeTemptWar` — *rich neighbours tempt war*). Like territory/culture, trade is a **pure function of the (saved) settlements + geography + wars/diplomacy** — **recomputed each sim year, no save state, no version bump**, rewinds for free. Render: a **Wealth heat map** (`wealthColor`, gold hubs) + a **trade-route overlay** (sea cyan / river+land amber) under key **`Z`**, a gold wealth dot per settlement in the **Civ** tab, and a cell-info prosperity/route-count line. Knobs `trade*`. *Next steps (not yet built): cultural **evolution** (spread along borders, drift, assimilation, schism); tribute / vassalage treaties; an accumulated treasury (funding armies / buying peace).* ## Current state Working and verified (logic tested headless): - Icosphere level 5 → 10242 cells, ~223 km/cell, topologically correct (exactly 12 degree-5 vertices, rest degree-6). - Plate flood-fill, drift as rotation on the sphere, boundary-stress uplift. - Relief builds *gradually* toward an isostatic equilibrium (no clamp-rail saturation): after ~40 ticks graded mountain belts (>2000 m) and deep subduction trenches (<-6000 m), 0% of cells pinned to the clamp. - `test_logic.cpp` asserts geometry, plate assignment, non-saturation and graded relief; run it after any `Planet::step()` change (see below). - raylib render: 3D globe (top) + 2D Equal Earth map (bottom strip), orbit camera, 4 color modes. Phase 1 is a **generator**: it paces tectonic ticks toward isostatic equilibrium (watchable, ~3 s), renders live, and **auto-pauses** once the terrain settles (max per-tick change < 2 m). No background thread — that's Phase-2 (continuous drift/erosion) groundwork. - **Phase 2 increment 1 (drift):** after forming settles, the app switches to continuous **drift mode** on a My clock. Each plate has a real speed (1..20 cm/yr); `cflDtMy()` sets the timestep so the fastest plate advances ~half a cell/step. `advect(dt)` moves plate membership + carried crust (plateId, elevation, oceanic, geoAge) by an accumulation scheme: a boundary cell builds `drift` (signed convergence distance) with its dominant other-plate neighbor; +1 cell -> overrun (take that crust; but oceanic can't overrun a buoyant continent -> it subducts under), -1 cell -> new young ridge crust (spreading). Crust type now lives on the **cell** (`Cell.oceanic`), not the plate. The HUD shows elapsed My; `[`/`]` set My/sec; auto-settle still gates Phase 1 -> 2. - **Phase 2 increment 1.5 (plate lifecycle):** without this the count collapsed to 1-3 and the world froze (a giant continental plate that can't shed cells). Every `splitCheckEvery`(10) advect iterations (gated by `driftIter`) `advect()` runs a Wilson-cycle lifecycle: **fission** (a plate over `splitFraction`(20%) of cells splits along a random great circle through its centroid; prob ramps `0.05+0.05*(pct-20)`, the new half gets a random drift — self-regulating, so the count equilibrates ~8-12), **stalemate kick** (a plate whose size barely changed over a window gets a new direction + speed boost to break deadlocks), and **spreading plates** (rift cells become a `Plate.baby` young-ridge strip; `coalesceBabyPlates()` merges connected blobs and dissolves tiny noise ones; a strip past `babyPromoteFrac`(0.7%) is promoted to a real plate with random drift + `volcanicLandFrac` of its interior turned to volcanic-island land). The hard land clamp became a **soft band** (`landBand`) so volcanic land persists; `acquirePlate()` reuses dead plate slots to keep `plates` bounded. Stats panel separates real plates from "Young ridges: N strips"; plate-color mode tints baby cells a uniform ridge grey. Verified by a headless soak. - **Phase 2 increment 1.6 (calmer + cleaner plate map):** kicks were too twitchy ("strange border movement") so a plate must now stall for `stalemateWindows`(4) consecutive check windows (`sStaleStreak`) before a kick. `deleteEnclosedPlates()` absorbs any plate ringed by a single other plate into it (on a mutual pair only the smaller). `fuseMiniPlates()` lets >= `fuseMinPlates`(3) clustered mini plates (non-baby, < `miniPlateCells`) fuse into the largest member and steal one ring of cells from their largest big neighbour (terrane amalgamation). Borders now draw in two colors: real plate borders yellow, young spreading-ridge borders red (3D + 2D, both via `B`). Soak: mini plates ~0, no enclosed slivers, ~8-12 plates. - **Phase 2 increment 2 (erosion + sea level):** `Planet::erode(dtMy)` runs each drift step after advect+step — a mass-conserving, slope-weighted downhill sediment transport (one double-buffered gather pass): the higher cell of each edge gives material to the lower, faster above sea level (`erosionLandRate`) than below (`erosionSeaRate`). Highs wear down toward an uplift<->erosion equilibrium; sediment fills basins and builds coastal shelves/deltas. No river carving (sub-cell at 223 km -> subgrid later). `adjustSeaLevel()` (every `seaLevelEvery` erode calls) eases `cfg.seaLevel` toward the percentile elevation leaving `landFractionTarget` (30%) of cells above water — percentile targeting (`nth_element`) because a proportional nudge oscillates across the flat continental-base elevation. Two "land" notions coexist: crust type (plate buoyancy) vs geographic (elev > seaLevel). Stats headline + water:land are now geographic, with separate "Sea level" and "Crust %" lines. Headless: land -> 30% all seeds, erosion conserves sum(elevation), deterministic. - **Phase 2 increment 3 (gradual sea level + config/save files):** the sea-level controller is now gradual — checked every `seaLevelEvery` (100) erode calls, it nudges sea level by a fixed `seaLevelStep` (100 m) when outside a `seaLevelTol` (2%) deadband, and only if the nudge reduces the error (so it rests near a flat "cliff" instead of oscillating). A human-editable **`planet.cfg`** (key=value) holds all PlanetConfig params (auto-created on first run, `F2` reloads + regenerates); `loadConfig`/`saveConfig` share one `CONFIG_FIELDS` X-macro. A **`planet.save`** binary holds seed + config + full planet state (`Planet::writeState`/`readState`; geometry rebuilt from subdivisions via `buildGeometry()`); `F5` saves, `F9` loads and resumes (deterministic continuation, verified headless). File I/O lives in Planet (raylib-free). - **Phase 2 polish (config validation, QoL, sim cleanups):** `validateConfig()` range-checks every field (+ the cross-rule `oceanBase < continentBase`) on load and `F2`; an invalid `planet.cfg` reverts to safe defaults without overwriting it. Save bumped to **version 2** (now also persists the `[`/`]` drift rate; version-gated reads accept older saves). Viewer adds a **crust-type color mode** (`4`: continental warm brown / oceanic deep blue), `F` **fast-forward** (runs `step()` to settled instantly), `F12` screenshot, `--seed`/`--config` CLI flags, a `P` label per plate on the drift arrows, and a min subdivision level of 1 (level 0 disallowed). Two sim refinements: `step()` re-anchors boundary (source) cells to their original stress each dilation ring so adjacent belts don't cross-inflate (sharper peaks; land flank cells 509->499); and the soft land-band rift/accrete nudge now reads a frozen snapshot of pre-nudge `oceanic` so flipping one cell can't cascade along vertex-index chains into linear "snakes". The earlier drift-direction "inward" fix was reverted (it worsened snaking). - **Phase 2 increment 4 (taller mountains + seafloor aging):** mountains used to cap ~5000 m because collisions were under-weighted and `relax` snapped uplifted crust back to `continentBase` once a migrating front passed. Now `step()` adds a real **continent-continent collision** factor (`colliding[]` = continental cell facing continental, `cfg.collisionFactor`) and an **Andes-class continental arc** factor (`cfg.arcFactor`), and high continental crust gets **isostatic persistence**: `relaxEff = relax*(1 - isostaticPersist*clamp((elev-continentBase) /rootScale,0,1))`, so thick ranges stand and become *erosion-limited* (by the drift-loop `erode()`) instead of relaxing away. These three boosts are **gated on the `Planet::drifting` flag — active only in Phase-2 drift, OFF during Phase-1 forming** (which keeps the original mild factors + full relax). This is deliberate: Phase-1 forming runs `step()` with no erosion, so if the strong uplift + weak relax were active there it would never settle (uplift never balanced) and would rail the clamp — gating to drift, where `erode()` runs every tick, avoids both. main.cpp sets `planet.drifting=true` when forming settles (and in `loadGame` from the saved phase), `false` on reseed/regen. **Seafloor aging->depth:** oceanic crust subsides with `geoAge` via `oceanicBase(age) = max(oceanBase, ridgeDepth - seafloorSubsidence*sqrt(age))` (half-space cooling); `oceanBase` is now the **deep abyssal floor** (-6000 m), `ridgeDepth` the shallow young value (-2500 m), and `seedInitialRelief()` seeds an oceanic age spread (`seafloorSeedAge`) so the starting seafloor already has ridge->abyss variety. Headless: ~half of seeds produce >7000 m ranges that persist (the rest are legitimately low-relief ocean worlds), <2% pinned to the clamp on all seeds, older seafloor markedly deeper, deterministic. Tune the new knobs in `planet.cfg`. - **Phase 3 increment 1 (hydrology: rivers, lakes, fluvial erosion):** `Planet::hydrology(dtMy)` = `routeFlow()` then mass-conserving fluvial erosion, on the fixed grid (Eulerian, raylib-free). `routeFlow()` does priority-flood depression-filling (epsilon tilt so flats drain; ocean cells are outlets) → `sFill`/`sLakeDepth` (a cell with `lakeDepth>0` above sea level is a **lake**), steepest-descent over the filled surface → `sFlowTo`, and flow accumulation in descending-fill order → `sDischarge` (rivers = `discharge>riverThreshold`). The erosion pass walks the network upstream→downstream carrying a sediment load: stream-power incision `K*Q^m*S^n*dt` where under capacity, deposition where over (`cap=riverTransport*Q*S`) — filling lakes, building deltas at mouths, depositing the remainder at ocean sinks so **sum(elevation) is conserved**. Lakes/rivers are **derived from elevation each tick** (no new saved per-cell field; only erosion writes back to `elevation`). Orchestration (main.cpp): after `phase3AfterMy` drift-My the sim **pauses and prompts** ("Continue Phase 2" / "Start Phase 3"); `H` toggles Phase 3 manually. In Phase 3 the drift loop keeps running (advect/step/erode) but at a **finer dt** (`cflDtMy()*phase3DtScale`) plus `hydrology(dt)` — drift never stops, just resolves finer. Lakes shade inland-water blue (`recolor`); rivers draw as a `centroid→downstream` line network (3D + 2D, two widths, `J` toggles). Save bumped to **v3** (+ a `phase3` header flag). Headless: discharge grows downstream and all land rainfall reaches the sinks, mass conserved to ~1e-15, ~10 lake systems + rivers persist, deterministic. (Note: the hydrology phase is now framed in the UI as **Phase 2.5**; the internal `phase3*` names are unchanged.) - **Phase 3 increment 1 (biomes — classify + color):** `Planet::classifyBiomes()` (src/sim/PlanetBiomes.cpp, raylib-free) writes a per-cell `Cell.biome` (enum `Biome`, 13 entries: Ocean, **Ice**, Lake, Beach, Wetland, Grassland, Savanna, Desert, Forest, Taiga, Tundra, Hills, Mountains). A first **rule-based** pass with no real climate yet: temperature = warm-equator curve (super-linear in latitude so cold concentrates at the poles) minus an elevation lapse; moisture = latitudinal rainfall belts (wet equator/mid-lat, dry subtropics→deserts) + river discharge + coastal proximity; classified first-match (ice→ocean→lake→beach→mountains→hills→ lowland-by-temp/moisture). **Polar ice caps** fall out of the temperature test (it also snow-caps high peaks). All thresholds are **tunable in `planet.cfg`** (the `biome*` PlanetConfig fields — `biomeIceTemp`, `biomeMountainElev`, the moisture cutoffs, etc.; only the latitudinal rainfall-belt curve shape stays a fixed helper). The biome is **saved per cell** — save bumped to **v4** (per-cell biome byte appended after `invader`; `readState(is, hasBiome)` reads it for v4, reclassifies for v3, so v3 saves remain loadable). Rendered as color mode `5` (`biomeColor`, src/render/Colors.cpp); re-run each `refreshView`. `lakeColor` changed to bright turquoise so lakes read clearly vs ocean (the "blue speckle" near a clicked cell is just the subgrid detail overlay, where ±250 m value-noise dips below sea level near coasts — not lakes). Headless: every cell valid, both poles Ice, deep equatorial water Ocean, ≥4 land biomes present, deterministic, v4 round-trips biome, v3 loads + reclassifies. - **Phase 3 polish (smaller caps + axial tilt + grid labels):** ice caps trimmed a few points (~17%→~14% of cells) by lowering `ICE_TEMP` in PlanetBiomes.cpp. Added a planetary **`axialTilt`** (obliquity, default 23.44°, in PlanetConfig/`planet.cfg`): the 3D globe + a drawn **spin-axis rod** (through the poles, red/blue pole caps) lean by it via an `rlRotatef` about world Z wrapping all 3D content in `renderGlobe3D`; picking un-rotates the world hit dir by −tilt (`rotateZ`, src/render/Picking.cpp) and 3D plate labels rotate by +tilt so everything stays consistent (the picking sphere is rotation-invariant). Biomes/2D map are unchanged (tilt is visual + groundwork for seasons). The graticule (`G`) now shows **lat/lon degree numbers on the 2D map** edges (`drawGraticuleLabels2D`, plain "60N"/"120W" — the default font has no `°`). `axialTilt` was added to PlanetConfig (`planet.cfg`). Headless: ice ~14%, biome + `axialTilt` round-trip, deterministic. - **Phase 3 polish (biome knobs in config + future-proof save):** the 17 biome classification thresholds moved from constants into PlanetConfig `biome*` fields (tunable in `planet.cfg`, `F2`). To stop config additions from breaking saves each time, the save now stores config as a **self-describing key=value text block** (save **v6**) parsed like `planet.cfg` (`writeConfigFields`/`parseConfigStream` shared); doubles written at `precision(17)` round-trip exactly. Adding/removing config fields no longer breaks saves; v6 just can't load pre-v6 saves (one-time break). Headless: cfg (incl. non-default `biome*`) round-trips exactly, unknown/missing keys handled. - **Phase 3 increment 2 (climate model):** `Planet::computeClimate()` (src/sim/PlanetClimate.cpp, raylib-free, derived/not saved) builds two continuous per-cell fields. **Temperature** `sTemp` (°C) = the latitude curve (`biome*` temp params) − elevation lapse. **Precipitation** `sPrecip`: prevailing winds are zonal by band (tropics/polar easterly, mid-lat westerly); ocean cells are a moisture source and each land cell takes its **upwind** neighbour's moisture, **rains out** more on windward upslopes (orographic) and loses a multiplicative fraction per cell (continentality), so leeward + deep-interior cells dry out. The raw field is near-binary (saturated where the wind hits the sea, ~0 elsewhere), so it's **diffused** `climateMoistureSmooth` passes to create wet→dry transition zones, then normalized to `sMoist` (0..1, **median land → 0.5**, robust to orographic spikes). `classifyBiomes()` now reads `sTemp`/`sMoist` (dropping the old latitude+discharge+coast hack) → rain-shadow/interior **deserts** + a varied, per-world biome spread; wetlands now require adjacency to water (ocean/lake). Color modes `6` (temperature, blue→red) / `7` (precipitation, dry→wet). `computeClimate()` runs before `classifyBiomes()` in `generate()` and `refreshView()`. New `climate*` config knobs (planet.cfg). Headless: equator warm/poles cold, lapse, coastal wetter than interior, deserts present, deterministic. - **Seasons (obliquity):** `axialTilt` (previously visual-only) now drives a per-cell seasonal temperature range. `computeClimate()` adds derived `sTempSummer`/`sTempWinter` (= annual mean `sTemp` ± a half-amplitude `A = seasonAmpMax·tiltFactor·latShape·continentality`), where `tiltFactor = sin(axialTilt)/sin(23.44°)` (0 tilt → no seasons) and **continentality** comes from a multi-source BFS ring-distance from ocean cells (coasts/oceans muted by thermal inertia, interiors swing most). Big swings at high-latitude continental interiors, ~0 at the equatorial coast. `classifyBiomes()` blends **winter** temp into the Tundra/Taiga cold cutoffs via `biomeSeasonWeight` (0 = annual-mean-only/old behaviour, default 0.6) so cold-winter interiors turn boreal/tundra (Siberia effect) — the amplitude is geographically shaped, so this expands cold biomes only where seasons bite. Derived/not-saved (no save bump). Color key `6` now **cycles** mean→summer→winter→seasonality; cell-info shows summer/winter. New `season*` + `biomeSeasonWeight` config knobs. Headless: equator swing ≈1.6 °C vs ≈20 °C at high latitude, interior land ≫ ocean, tilt=0 → no seasons, higher tilt → bigger swing, `biomeSeasonWeight=0` leaves biomes unchanged, cold-biome count rises with seasons, deterministic. - **UI polish (full cell info + view label + framing):** the cell-info panel (`cellInfo`, src/render/Panels.cpp) now shows everything per cell — crust type, **biome** (`biomeName`), **temperature** + **precipitation %**, and **river/lake** when hydrology is on — in addition to the existing cell#, lat/lon, elevation, plate, geoAge. The active color mode is shown **top-center** of the globe ("Biome view", etc., via `colorModeName`), updating with `1`–`7`. The HUD title/status and the hydrology prompt were reworded to drop the rigid "Phase N" labels (now "World Creation: forming / drift & erosion / hydrology"); internal `phase*` names are unchanged. Render/text only — no sim/save/config change. - **Biota (flora/fauna/funga):** the World-Creation stage after biomes. Two layers (src/sim, raylib-free): (1) **density scalars** `sFloraDensity`/`sFaunaDensity`/`sFungaDensity` ∈ [0,1] via `Planet::computeBiotaDensity()` — flora = NPP Liebig-min of temp & moisture (0 on water/Ice), fauna = herbivore capacity ∝ flora with carnivores gated on local prey (`bioCarnPreyMin`), funga = flora-like but moisture/organic-matter-led + cold-tolerant. Derived each tick (like climate), drive color modes `8`/`9`/`0`. (2) A discrete **slot/point population** `Planet::generateBiota()` (key `L`, on a settled world) — each land cell draws broad **archetypes** from a comprehensive table (`biotaArchetypes()`, 49 entries across Flora/Fauna/Funga incl. marine, each with Class/Order/Family/Size + a biome mask + climate tolerance) into a per-kind slot cap + a density-scaled point budget (Tiny=1…Huge=5 cost), weighted by suitability and a **regional bonus** for archetypes already placed in same-biome neighbours (homogeneous regions, variety at boundaries). Organisms are labelled by their **taxonomy** — Family + Size + role (e.g. *Felidae (Big, Carnivore)*, with the full *Class > Order > Family* tree in `organismTaxonomy()`), never an informal common name like "big cat"; generalist families get a biome adjective (*Desert Muridae*). Uses a **separate RNG** seeded from `cfg.seed` so generating biota never perturbs tectonic determinism. Population is **saved** (`sBiota`, save **v7**); densities are derived/not-saved. New files `PlanetBiota.{hpp,cpp}` + `PlanetFlora/Fauna/FungiGen.cpp`; color modes `floraColor`/`faunaColor`/`fungaColor`; cell-info shows density % + the per-kind organism list. `bio*` config knobs. Headless `test_biota.cpp`: density ranges/zeros, fauna≤ capacity, carnivore gating, slot/point budgets, determinism + RNG isolation, v7 round-trip, pre-v7 loads empty. v7 reads v6-and-older (no biota block → empty population; press `L`). - **Biota — marine flora & fauna (life in the ocean):** the biota layers used to be 0 on every water cell (a hard `elevation<=sea` gate + no Ocean-masked archetypes), so the sea read as barren. Now ocean cells (not under polar `Ice`) get a **marine primary productivity** in `computeFloraDensity`: `base + (1-base)·max(shelf, coast)` where `shelf` = shallowness (`1 - depth/bioMarineShelfDepth`, light to the photic floor) and `coast` = a multi-source BFS **ring-distance from land** (nutrient runoff; mirrors the continentality BFS, seeded from land not ocean) — so productivity is rich on sunlit shelves/coasts, lower in the deep open ocean, zero only under ice; `sMoist` (a *land* rainfall field) is **not** used at sea. `computeFaunaDensity` now skips only `Ice` (was all water) so marine fauna = `flora·productivity`, with the existing carnivore prey-gate clustering sharks/seals/squid on rich shelves. **Funga stays land-only.** `generateBiota` populates ocean cells too (skip `Ice`; no marine funga) — `fillFlora`/`fillFauna` are unchanged because their biome-mask filter draws only the **new Ocean-masked archetypes** appended to `biotaArchetypes()`: marine flora **Kelp / Seagrass / Phytoplankton** and marine fauna **Forage fish / Reef fish / Shark / Baleen whale / Seal / Squid** (all `moistMin=0`, SST-zoned; append-only so v7 saves are unaffected — old saves just lack them until `L`). The flora/fauna color views (`8`/`9`) render ocean on a **distinct marine ramp** (`marineFloraColor` deep blue→teal/green bloom, `marineFaunaColor` deep blue→cyan→warm) so the sea still reads as sea; land ramps + the funga view unchanged. New `bioMarineBase`/`bioMarineShelfDepth`/ `bioMarineCoastRings` config knobs (self-describing config → **no save bump**). `test_biota.cpp` updated: zero life under ice, marine flora/fauna present at sea + populate ocean tiles, funga 0 on water, capacity/carnivore-gate/budgets/determinism still hold. - **Live World — clock + day/night + live seasons + snow line:** the first **Live World** stage (the slow real-time arc after World Creation). Engine (`src/sim/PlanetLive.cpp`, raylib-free, derived/not-saved): `computeInsolation(dayOfYear01, timeOfDay01)` → `sInsolation` (0..1 cosine solar incidence; declination `axialTilt·sin(2π·doy)`, sub-solar longitude sweeps once per day; the foundation the future weather sim reads) and `computeLiveSeason(doy)` → `sLiveTemp` (the annual-mean `sTemp` swung toward the existing `summerTemp`/`winterTemp` by the seasonal phase, anti-phased across hemispheres). Viewer: key `W` (settled world) toggles **Live World** — drift freezes and `liveTime` advances at `liveRate` (sim hours/real-second), `[`/`]` ramp it hour→~20 years/sec (`liveRateMax()` = 20 sim-years/s); the HUD shows a `Year/Day/HH:MM` calendar (`dayLengthHours`/`yearLengthDays`). `rebuildLiveOverlay()` builds a per-cell day/night brightness (`illum`, soft terminator, dim night floor) + `shadedColors` (base colour → snow on cold land / sea-ice on cold ocean via `snowTemp`/`seaIceTemp` → day/night dim); both the 3D globe and 2D map draw `displayColors()` (the overlay over **any** colour mode), `N` toggles the terminator, a sun marker sits over the lit hemisphere. Cell-info adds a `live temp / day-night / snow` line. Save **v8** appends the Live World flag + `liveTime` (version-gated; older saves load with it off). New `PlanetLive.cpp` in CMake + the headless list; `test_live.cpp`: insolation range, lit/dark hemispheres, declination tracks `axialTilt` (polar day/night at solstice), live temp within the summer/winter band + anti-phased, snow line advances in winter, determinism. - **Live World — moons, tides & a distant sun:** engine `src/sim/PlanetOcean.cpp` (raylib-free): `generateMoons()` seeds **1–3 moons** (`Moon` struct in PlanetTypes) from a separate RNG (`cfg.seed ^ 0x900D5EED`, tectonic stream untouched); `sunDirection`/`moonDirection`/ `moonOrbitNormal` give model-space sky geometry (one source of truth — `computeInsolation` now calls `sunDirection`). `computeTides(doy,tod,days)` → `sTide` (m), equilibrium two-bulge tide (`Σ w·(cosθ²−⅓)`, moons + sun weighted `tideSunFactor`, scaled `tideAmplitude`); derived/not saved. Viewer: `T` colours the **coastline** (`buildCoastline` dual-contour + `tideColor` diverging amber↔cyan, per-segment in 3D + `drawColoredSegments2D` in 2D), auto-scaled to the tide extent; `stepSim` computes tides + `moonDirs`/`moonNormals` each live frame. 3D render: small **distant sun** (`sunDist`≈9) + halo; **moons** at a visible orbit band with sun-lit **phase** (offset-dark-sphere), faint **orbit rings**, and **eclipses** — solar shadow folded into `rebuildLiveOverlay`'s `illum` near the sub-solar point, lunar dimming (reddish) when a moon is in the planet's shadow. Cell-info adds a tide line; stats shows the moon count. **Save v9** appends the moons block (`writeState`/`readState(...,hasMoons)`; pre-v9 saves synthesize moons from the seed). `test_ocean.cpp`: moon count/determinism + RNG isolation, unit sweeping sky dirs, zero-mean two-bulge tide (high under moon + antipode, low at 90°, moves with time), save v9 round-trip. **Enclosed-sea cap:** `computeTides` flood-fills connected ocean bodies and caps the amplitude of any body under 10 cells to `0.01·cells + 0.03` m (a one-cell sea ≈ 0.04 m, an inland saltwater lake stays calm) — a small closed basin can't build a real tidal range; open oceans (≥10 cells) keep the full equilibrium tide. - **Live World — ocean currents + climate feedback:** `Planet::computeOceanCurrents()` (PlanetOcean.cpp) builds a per-ocean-cell tangent velocity `sCurrent` (derived/not saved): wind stress (`sWind`) rotated by a **Coriolis** deflection (right N / left S about the cell normal), the across-shore component removed at land neighbours so flow **follows coasts** (gyres), then 3 smoothing passes (re-projected to the tangent plane; zero on land). `computeClimate()` calls it right after the wind pass and feeds it back: a **bounded coastal temperature anomaly** = `climateCurrentFactor · (poleward speed / max)` on ocean cells (warm poleward, cold equatorward), smoothed onto the coasts and added to `sTemp` **before** seasons, so summer/winter + biomes shift with it. Render: `buildCurrents` emits subsampled warm/cold **arrows** over the sea (warm = poleward/red, cold = equatorward/blue), key `O` (3D + 2D), built in `refreshView`. New knob `climateCurrentFactor` (4 °C). `test_ocean.cpp` adds: currents tangent + zero on land + widespread, feedback bounded by the knob and produces both warming and cooling, deterministic. Live-World ocean/sky pass complete. - **Live World — dynamic weather (clouds & rain):** `Planet::stepWeather(dtHours)` (PlanetWeather.cpp) advances a per-cell humidity/cloud/rain cycle on the live clock: **evaporate** over warm sunlit ocean (uses `sInsolation`+`sTemp`), **advect** humidity & cloud downwind (upwind differencing along `sWind`/`sUpwind`, `weatherWindKmh`), **condense** the supersaturated air into cloud — saturation `weatherSatBase + weatherSatTempCoef·T`, plus windward **orographic** lift — **rain** out cloud above `weatherRainThresh`, then **dissipate**. `initWeather()` spins the fields up from the moisture climatology; bounded exponential rate forms keep it stable at any timestep. Runs each live frame in `stepSim` (dt = the same sim-hours added to `liveTime`; held when paused). Render: a translucent **cloud shell** (white → dark storm where it rains, alpha = cover) over the 3D globe + a `drawWeather2D` layer on the 2D map, key `K` (default on); cell-info adds cloud/humidity/raining. **Saved v10** (humidity/cloud/rain, flag-gated; older saves spin weather up live). Deterministic (no RNG). `test_weather.cpp`: fields in range, clouds form + rain falls, oceans moister than land, determinism, v10 round-trip. - **Live World — moving weather systems (lows / hurricanes / typhoons):** the base cloud/rain field relaxes to a *static* pattern under fixed forcing, so `stepWeather` now also runs a population of drifting **`WeatherSystem`** agents (PlanetTypes; **saved v11**; separate `sWeatherRng` seeded from `cfg.seed` → tectonic determinism intact). Each step: **spawn** over warm tropical ocean (5–25°, SST ≥ `weatherTropicalSST`) or a mid-latitude (30–62°) ocean low (capped at `weatherSystemMax`, prob ∝ `weatherSpawnRate`); **move** along the steering wind (`sWind` at the nearest cell) + a poleward recurve at `weatherSystemSpeed`; **intensify** over warm sea / **decay+cull** over land/cold; **stamp** a Gaussian cloud/rain shield (`weatherSystemCloud`/`Rain`, scaled by strength × local humidity) — so cloud clusters travel and dissipate behind the system. A tropical system past `weatherHurricaneStr` is a hurricane/typhoon. Render: an animated cyclonic **spiral marker** per system (red + eye for cyclones, blue lows; spins with `liveTime`·hemisphere) in 3D + 2D, HUD system/cyclone counts, and a storm list (basin-named) in the Live info `Weather` tab — all under `K`. `test_weather.cpp` adds: systems spawn, move between steps, thicken cloud, RNG isolation, determinism. - **Live World viewer controls — storm follow-cam, 2D map zoom, clock stepper:** (1) **`Y`** cycles the 3D camera to **follow a storm** (by descending strength, off after the last). Tracked by a stable `WeatherSystem.id` (assigned at spawn from `sStormNextId`; transient, not RNG); each frame `handleInput` points the camera straight at it via `camYaw/camPitch` from `rotateZ(pos,+axialTilt)` (model→world), orbit-drag disabled while following, auto-release if it dissipates. (2) **2D map zoom**: `mapZoom`/`mapPanX`/`mapPanY` + `Viewer::mapViewRect()` (mapRect scaled about its centre + pan); every map projection call routes through it while the **scissor/frame stay `mapRect`** (`drawMapTris` now derives y from the rect, not the fixed `m.pos`). Mouse-wheel over the map zooms toward the cursor (1–8×); drag pans when zoomed, else rotates `mapLon`; 2D picking inverts the same rect. (3) **Clock stepper**: the `stepSim` live body is factored into `Viewer::liveAdvance(dtClock, dtWeather)`; **`.`** steps forward and **`,`** back by `liveRate` hours (both auto-pause, like a video frame-step). Weather is an integrated path (not analytically reversible), so a forward step snapshots the full state — `Planet::captureWeather()`/`restoreWeather()` (humidity/cloud/rain/ storms/RNG) into a bounded `wxUndo` ring — and **`,` restores the previous snapshot**, so the step really reverses *everything* (clouds, rain, **moving storms**) plus the deterministic sky. A manual step **always** records (rate-independent, via `wxPushSnapshot`); a continuous run records a throttled snapshot (~1/sec) so a run is rewindable too. `,` searches the ring by time, the ring drops oldest past `wxUndoMax`. The most recent `wxSaveMax`(40) frames are **persisted in the save (v12)** so a load can rewind storms past the saved moment; a load also drops any stale pre-load history. With no recorded past (e.g. immediately after a pre-v12 load) `,` rewinds the sky only and says so. `S` in Live World aliases the forward step. - **Live World event log:** `liveInfoRect` is a tabbed panel (`Sky`, `Tides`, `Weather`, `Events`). The viewer-owned event journal is saved in v16, capped to the newest 200 entries, and records storm formation/intensification plus volcano dormancy, eruptions and island breaches. Clicking an event selects its cell, releases storm follow-cam, rotates the 3D view to it, and centres the 2D map at the current zoom. - Mouse hover (in either view) shows per-cell info. Clicking a tile opens a right-side detail panel: tile info header + the tile's subgrid drawn as a flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res subgrid patch is also overlaid on the globe for context; the hovered subtile is marked on the globe. `C` closes the panel. ## Architecture The code is split into a **raylib-free engine** (`src/sim`, testable headless) and a **raylib viewer** (`src/render`); `src/main.cpp` is a ~10-line entry point. `Planet` is one class implemented across several `.cpp` files (one per phase/ concern, all sharing `Planet.hpp`); the viewer is one `Viewer` struct whose state + methods are likewise spread across a few render files. CMake adds both folders to the include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`). ``` src/ main.cpp entry point: build Viewer, init(argc,argv), run() sim/ (raylib-free engine -- testable headless) Vec3.hpp double-precision 3D vector math IcoSphere.* geodesic icosphere: fixed vertices + neighbor adjacency Projection.hpp Equal Earth equal-area projection (forward + Newton inverse), dir<->lon/lat helpers. Header-only, raylib-free, testable. PlanetTypes.hpp Cell / Plate / SubGrid / PlanetConfig data structures Planet.hpp the Planet class declaration + config-file func decls Planet.cpp generation, geometry, plate seeding, shared helpers, subgrid PlanetTectonics.cpp step() (Phase 1/2 stress->uplift->relax) PlanetDrift.cpp cflDtMy/advect + plate lifecycle (fission/kick/baby/fuse) PlanetErosion.cpp erode() + adjustSeaLevel() PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3) PlanetClimate.cpp computeClimate() (temperature + orographic precipitation) PlanetLive.cpp computeInsolation/computeLiveSeason (Live World: day/night + live seasons) PlanetOcean.cpp moons (generate/orbit) + computeTides + computeOceanCurrents PlanetWeather.cpp stepWeather (Live World dynamic clouds & rain cycle) PlanetVolcano.cpp placeVolcanoes/stepVolcanoes (Live World volcanoes + islands) PlanetBiomes.cpp classifyBiomes() (per-cell Cell.biome from elev + climate) PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity PlanetFloraGen.cpp computeFloraDensity + fillFlora PlanetFaunaGen.cpp computeFaunaDensity + fillFauna (carnivores gated on prey) PlanetFungiGen.cpp computeFungaDensity + fillFunga (moisture/organic-matter rule) NameGen.* deterministic procedural name generator (syllable banks; reused by civ arc) PlanetGeography.* generateGeography() (named features: continents/oceans/ranges/rivers/lakes) PlanetEcoregions.* generateEcoregions() (named ecological provinces + dominant biota/productivity) PlanetCiv.* computeHabitability/placeSettlements/stepCivilization (settlements; civ Step 2) PlanetNation.* computeTerritory (realms + per-cell ownership + borders; civ Step 3) PlanetCulture.* computeCultures (cultures/ethos/faith per continent + governments; civ Step 4) PlanetConflict.* stepConflict (wars/conquest/revolts + diplomacy/alliances/coalitions; civ Steps 5-6, save v21/v22) PlanetTrade.* computeTrade (trade routes + prosperity/wealth feeding growth; civ Step 7, derived) PlanetIO.cpp config file (text) + binary save/load render/ (raylib viewer) Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota) Map2D.* Equal Earth 2D map: positions + projection/draw helpers Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids Picking.* mouse ray / sphere hit / nearest-cell / angle helpers Panels.* right-column UI: detail panel, hover info, world stats Viewer.{hpp,cpp} Viewer struct: all state + setup + sim orchestration ViewerInput.cpp handleInput(): camera, hover picking, click, keys ViewerRender.cpp renderGlobe3D / renderMap2D / renderPanels / renderHUD / renderPrompt CMakeLists.txt fetches raylib 5.5 via FetchContent; lists src/sim + src/render BUILD.md dependencies + build/run + controls ``` ### Key data structures (src/sim/PlanetTypes.hpp) - `Cell` — `unit` (fixed sphere direction), `elevation` (continuous meters, double, NOT quantized), `plateId`, `geoAge`, `neighbors`, and a `std::shared_ptr subgrid` HOOK (still null on the cell; the viewer builds subgrids on demand via `Planet::makeSubGrid(cell,res)`). - `SubGrid`/`SubCell` — a res*res patch around one macro cell; elevation is an inverse-distance blend of that cell + neighbors plus value-noise detail, and each subcell records `nearestMacro`. Phase-4 preview, generated on click. - `Plate` — `type` (Oceanic/Continental), `driftAxis`, `driftSpeed`. - `PlanetConfig` — `radius` (default 6.371e6 m, Earth), `subdivisions`, `seaLevel`, `plateCount`, `seed`. ### Resolution notes (important, was discussed in design) - **Lateral** resolution = cell spacing = `sqrt(4*pi*R^2 / N)`. Coarse on purpose (~223 km at level 5). Only mountain-range scale, not single hills. - **Vertical** (elevation) resolution is effectively unlimited: it is a `double` in meters. 100 m steps or finer are free. Mount-Everest / Mariana-Trench range is exactly representable. - Climate (phase 3) reads elevation as a smooth continuous function, so no resolution is lost by coarse height bands. - For dense detail later (population/culture), generate a per-cell **subgrid** on demand and mark which edge sub-cells border which neighbor macro-cell, so cross-boundary interaction (rain shadow, transition zones) works. ## Build & run ```bash cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j ./build/planetsim ``` Target OS is Nobara Linux (KDE/Wayland, Intel Arc A770). Dependency install line is in BUILD.md (`dnf install cmake gcc-c++ mesa-libGL-devel ...`). raylib 5.5 is fetched automatically — do not vendor it. ### Quick headless logic test (no display needed) ```bash g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \ src/sim/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \ src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \ src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp \ src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetVolcano.cpp \ src/sim/PlanetBiota.cpp \ src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \ src/sim/NameGen.cpp src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp \ src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp src/sim/PlanetTrade.cpp \ src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t ``` (Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`, `test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp`, `test_civ.cpp`, `test_nation.cpp`, `test_culture.cpp`, `test_conflict.cpp`, `test_diplomacy.cpp` or `test_trade.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions / Civilization / Nation / Culture / Conflict / Diplomacy / Trade suites — same source list. CMake also builds `test_events` for the viewer event journal.) Use this to verify tectonics after changing `Planet::step()` without launching the window (the engine lives in `src/sim` and is raylib-free, so it links without any render code). The `#pragma omp` lines in `step()` are ignored without `-fopenmp`, so this serial build is correct; add `-fopenmp` to benchmark the threaded path. ### Performance / parallelism `Planet::step()` is data-parallel: each pass writes only its own cell index (double-buffered where it reads a field it writes), so the OpenMP `parallel for` loops are **bit-identical for any thread count** (determinism intact). It is memory-bandwidth-bound, so the speedup tops out ~3x (sub-7: ~5.1 -> ~1.5 ms) around 4-8 threads regardless of core count. `step()` also reuses persistent scratch buffers (the `s*` members) so it allocates nothing per tick. Small grids stay serial via `if(n > 20000)`. Control threads with `OMP_NUM_THREADS` (4-8 is the sweet spot; the default uses all cores for no extra gain). OpenMP is optional and auto-detected by CMake. For a bigger leap (or 1M+ cells) the next step is a GPU compute-shader port -- a Phase-2 effort. ## Controls LMB drag orbit · wheel zoom · hover for cell info (3D or map) · click a tile to open its detail panel (subtiles) · `C` close panel · drag the 2D map to pan it east/west · `1`..`0` color by elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga (`8`/`9`/`0` = biota density; `6` **cycles** temperature → summer → winter → seasonality; active mode shown top-center of the globe) · `B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3, all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) · `O` ocean-current arrows (warm/cold) · `K` weather clouds/rain (Live World) · `V` volcano markers (Live World) · `M` place-name labels (the atlas; names the world on first use) · `E` ecoregions colour view (names ecology on first use) · `I` habitability heat map · `U` settlements (the dawn of civilization on first press; toggles markers after) · `P` territory / realms view + political borders (Realms tab lists nations) · `X` culture / faiths view + cultural borders (Cultures tab lists peoples, ethos & religion) · `Z` wealth / trade view + trade routes (sea/river/overland; prosperity feeds city growth) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec → up to ~20 years/sec) · `S` single tick (in **Live World** steps the clock forward) · `.`/`,` step the live clock forward/back by one rate-unit (auto-pauses; `,` steps **everything** back incl. weather/storms via an undo history) · `Y` cycle the 3D camera to **follow a storm** (off after the last) · mouse-wheel **over the 2D map** zooms toward the cursor (drag pans when zoomed) · `F` fast-forward Phase-1 forming to settled · `H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga, on a settled world; re-press regenerates) · `W` enter/leave **Live World** (settled world) · `R` reseed · `+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load · `F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate. Phase 3: after `phase3AfterMy` simulated years a modal prompt asks **Continue Phase 2** / **Start Phase 3**; `H` starts/stops it manually. In Phase 3 drift keeps running at a finer timestep (`cflDtMy()*phase3DtScale`) while rivers, lakes and fluvial erosion evolve. Live World (`W`, on a settled world): geological drift freezes and a slow real-time clock runs (`liveTime` in hours, `liveRate` = sim hours/real-second, ramped hour→~20 years/sec with `[`/`]`). A moving day/night terminator (`N`), a live seasonal temperature cycle and a moving snow/sea-ice line animate over whatever colour mode is active; the HUD shows a `Year/Day/HH:MM` calendar. **1–3 moons** orbit (sun-lit phases, orbit rings, solar/lunar eclipses) and, with the distant sun, raise tides — `T` colours the coastline by the live tide level (amber low ↔ cyan high). `K` shows moving weather (clouds, rain, drifting storms / hurricanes). **Volcanoes** are placed by tectonic context on entry and run a stateful lifecycle — submarine ones can build into new **volcanic islands**; `V` toggles the cone/eruption markers. With settlements placed (`U`) and the Territory view on (`P`), realms **wage war** each year — red war-fronts appear, cities change hands (borders move) or fall to ruins, empires fracture as provinces revolt; realms also form **alliances** (green arcs) and **rivalries** (dark-red arcs), allies **join each other's wars** (coalitions) and never fight each other, and wars end in **peace treaties**; the **Realms** tab lists each realm's allies/ rivals/wars and the **Events** tab logs it all. `Z` shows the **trade economy** — routes (cyan sea / amber land) + a **wealth heat map**; well-connected coastal/river **hubs** grow richer & bigger, a war cuts the routes between belligerents, and the **Civ** tab shows each settlement's wealth. `Y` makes the 3D camera **follow a storm** (cycles by strength, off after the last); `.`/`,` step the clock forward/back by one rate-unit (back rewinds the sky **and** weather/storms/volcano lifecycle **and wars/conquests** via snapshots). Mouse-wheel over the 2D map zooms (drag pans). CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config file (both applied before the initial load/generate). Two files in the working dir: `planet.cfg` (human-editable key=value of every PlanetConfig param, auto-created on first run, reload with `F2`) and `planet.save` (binary: seed + config + full planet state, written/read by `Planet::writeState`/`readState`, resumes deterministically). Config is range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe defaults (without overwriting your `planet.cfg`) and shows a status message. The save header is versioned (currently **22**; v2 adds the `[`/`]` drift rate, v3 a `phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing key=value text block** instead of a raw POD dump, v7 appends the **biota population** block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World** clock — a flag byte + `liveTime`, v9 appends the **moons** block, v10 appends the **weather** block — humidity/cloud/rain, flag-gated, v11 also persists the **weather systems** + RNG so a load resumes active storms, v12 appends the most recent **step-back frames** — `wxSaveMax`(40) weather snapshots — so a load can rewind storms past the saved moment, v13 appends the Live World clock rate, v14 appends the old pure-function **volcanoes** block, v15 replaces it with stateful volcano lifecycle agents plus volcano state in step-back frames, v16 appends the saved **event journal**, v17 appends the **geography/atlas** block — named features + per-cell region indices, v18 a geography reshuffle salt, v19 the **ecoregions** block, v20 the **civilization settlements** block (the fixed settlement set + per-frame populations in the step-back history), v21 the **civilization conflict** block — per-settlement **allegiance** (conquest) + active **wars** + the war RNG, also added to the step-back frames so a load can rewind conquests, and v22 the **diplomacy** block — standing realm **relations** (alliances / rivalries / truces), likewise per-frame; newer-than-supported is rejected. Older saves (no biota block) load fine with an empty population (press `L`); pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10 saves spin weather up live; pre-v11 saves load with no active storms (they respawn); pre-v12 saves load with no step-back history (you can still step forward then back); pre-v13 saves resume with the default live clock rate; pre-v14 saves load with no volcanoes (placed on the next Live World entry); v14 volcanoes are discarded and reseeded as v15 lifecycle agents, with old history skipped; pre-v16 saves load with an empty event journal; pre-v17 saves load with no geography (regenerated on demand via `M`); pre-v19 saves load with no ecoregions (regenerated via `E`); pre-v20 saves load with no settlements (re-seeded via `U`); pre-v21 saves load with no wars (everyone independent; wars begin again as the clock runs); pre-v22 saves load with no diplomacy (relations re-form as the clock runs). A load drops any **stale** pre-load `wxUndo` history and reloads the saved one. **As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved config is parsed like `planet.cfg` (unknown keys ignored, missing keys keep defaults), written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves — a one-time break; a length guard makes that fail gracefully.) `writeConfigFields`/ `parseConfigStream` in PlanetIO.cpp are shared by `planet.cfg` and the save. Layout (1920x1080): left column 70% wide = 3D globe (top, 60% h, RenderTexture 1344x648) + 2D Equal Earth map (bottom, 40% h, **left-aligned**, with the freed space at its right holding the Live-World tabbed info panel — `liveInfoRect`, `renderLiveInfo`: Sky moon phase discs, selected coastal-tile tide phase, active weather systems, and the clickable saved event journal); right column 30% wide = cell info (top 50% h) + subareas (bottom 50% h). 3D hover uses a custom camera ray with the 3D viewport (1344x648 at the origin -- GetScreenToWorldRay assumes the full screen, wrong here); 2D hover uses EqualEarth::inverse (minus the `mapLon` pan). Borders (`B`), drift vectors (`D`) and a lat/lon graticule (`G`) draw in BOTH the 3D globe and the 2D map; drag the 2D map left/right to pan longitude (`mapLon`). The graticule (`G`) also draws **lat/lon degree numbers along the 2D map edges**. The 3D globe (and its spin-axis rod) lean by `cfg.axialTilt` (an `rlRotatef` about world Z around all 3D content in `renderGlobe3D`); 3D picking un-rotates the world hit dir by −tilt and plate labels rotate by +tilt (`rotateZ`, src/render/Picking.cpp) to stay in sync. Borders draw in two colors: real plate boundaries yellow, young spreading-ridge (baby-plate) boundaries red — both via `buildBorders` filling two segment lists. Drift arrows: one cyan arrow per plate at its centroid along the local drift velocity `omega x r`; length scales with speed. Rebuilt per world-gen. Each plate is also tagged with a `P` label drawn just above its centroid — projected by hand in 3D (the project's own `Vec3` camera-basis math, matching `BeginMode3D`'s fovy/aspect, behind-camera points skipped) and via Equal Earth in the 2D map. **Sim model (`Viewer::stepSim`, src/render/Viewer.cpp):** single-threaded. Each frame (while not paused/settled) it advances ~`formRate` (55) ticks/second of `planet.step()` (Phase 1 = pure tectonic forming, no erosion — `planet.drifting` is false) and rebuilds the view live, so you watch the terrain rise. `step()` returns the max per-tick elevation change; after `settleNeed` (3) consecutive ticks below `settleThresh` (2 m) it sets `settled` and stops stepping — no idle CPU. Re-evolve / reseed / subdivision-change clear `settled` and restart the forming pass. Rendering reads the live `cells` directly (safe: nothing else mutates them). Phase 2's *continuous* sim will want a worker thread + a render snapshot (so the 60 fps render never races the sim) — that's the natural place to reintroduce threading. Plate borders are traced once per world-gen as a dual contour through boundary triangles (plates are fixed in phase 1). ## Tuning knobs (expect to adjust these first) - `elevExagg` (src/render/Viewer.hpp) — visual elevation exaggeration; without it the sphere looks smooth. - `axialTilt` (PlanetConfig, `planet.cfg`) — obliquity in degrees (default 23.44). Leans the 3D globe + spin-axis rod; groundwork for seasons. Editable + `F2` to apply. - Biome thresholds (`biome*` in PlanetConfig / `planet.cfg`) — `biomeIceTemp` (lower = smaller polar/snow caps), `biomeMountainElev`/`biomeHillsElev`, the moisture cutoffs (`biomeDesertMoist`/`biomeGrassMoist`/`biomeWetlandMoist`), and the temperature model (`biomeEquatorTemp`/`biomePoleDrop`/`biomeLatExp`/`biomeElevLapse`, shared with climate). Editable + `F2`. - Climate (`climate*` in PlanetConfig / `planet.cfg`) — precipitation model: `climateOrographic` (windward-rain strength), `climateRainEfficiency`, `climateContinentality` (inland drying), `climateMoistureSmooth` (diffusion passes → wet/dry transition zones; raise for smoother, more grassland/forest), `climateOceanMoisture`, `climateOroRefHeight`, `climateWindPasses`. Temperature uses the `biome*` temp params. `climateCurrentFactor` (4 °C) is the max coastal warming/cooling from ocean currents (0 = off; ocean-current arrows toggle with `O`). Current deflection angle + smoothing passes are constants in `computeOceanCurrents()` (PlanetOcean.cpp), not config. - **Weather (`weather*` in PlanetConfig / `planet.cfg`):** the Live World clouds/rain cycle — `weatherEvapRate` (ocean evaporation speed), `weatherWindKmh` (advection speed of humidity/cloud), `weatherSatBase`/`weatherSatTempCoef` (how much moisture the air holds vs temperature — lower base = cloudier), `weatherCondense` (supersaturation→cloud rate), `weatherOrographic` (windward rain on mountains), `weatherRainThresh`/`weatherRainRate` (when/how fast thick cloud rains), `weatherCloudDissip` (cloud clearing). Toggle the overlay with `K`. Cloud render colours (white→storm, alpha) are constants in ViewerRender/Map2D. - **Weather systems (`weather*` storm knobs, `planet.cfg`):** `weatherSystemMax` (concurrent cap), `weatherSpawnRate` (genesis frequency), `weatherSystemSpeed` (km/h drift), `weatherTropicalSST` (min SST for tropical genesis), `weatherSystemRadius` (cloud-shield size), `weatherSystemCloud`/`weatherSystemRain` (stamp strength), `weatherHurricaneStr` (strength to count as a hurricane/typhoon). Markers show under `K`. Tune these for a stormier or calmer world. - Seasons (`season*` + `axialTilt` + `biomeSeasonWeight`, `planet.cfg`) — `axialTilt` is the master driver (0 = no seasons); `seasonAmpMax` (18 °C max seasonal half-range at full tilt/lat/interior), `seasonLatExp` (1.2, push swing toward poles), `seasonContinentRings` (6, ocean-distance to full continentality; lower = coasts go continental sooner), `seasonOceanFactor` (0.15, ocean/coast swing floor). `biomeSeasonWeight` (0.6) sets how much winter temp drives the Tundra/Taiga cutoffs (0 = annual-mean only, restores pre-seasons biomes). - Biota (`bio*` in PlanetConfig / `planet.cfg`) — density: `bioVegTempMin`/`bioVegTempOpt`/ `bioVegMoistRef` (flora temp/moisture limits), `bioFaunaProductivity` (animals per unit flora), `bioCarnPreyMin`/`bioCarnScale` (carnivore prey gate + ramp), `bioFungaMoistRef`/ `bioFungaFloraWeight`/`bioFungaTempMin` (funga moisture/organic-matter/cold rules); **marine flora/fauna**: `bioMarineBase` (0.15, open-ocean baseline density far from land), `bioMarineShelfDepth` (2500 m, depth over which shelf/light productivity fades to the base), `bioMarineCoastRings` (3, ocean rings from land over which coastal-nutrient richness fades to the base — lower = a tighter coastal band, higher = life further offshore); slot/point population: `bioFloraSlots`/`bioFaunaSlots`/`bioFungaSlots` (distinct-type cap), `bioFloraPoints`/`bioFaunaPoints`/`bioFungaPoints` (point budget at full density, scaled by it; Tiny=1…Huge=5), `bioRegionBonus` (how strongly a cell copies same-biome neighbours → homogeneity vs variety). To add organisms (incl. marine — give them a `B::Ocean` biome mask and `moistMin=0`), append to `biotaArchetypes()` in PlanetBiota.cpp (append-only — indices are serialized in v7 saves). - **Live World (`dayLengthHours`/`yearLengthDays`/`snowTemp`/`seaIceTemp`, `planet.cfg`):** `dayLengthHours` (24) sets the day/night period (and the `d/s` rate unit), `yearLengthDays` (365.25) the season period; `axialTilt` drives the seasonal declination (0 = no day/night tilt). `snowTemp` (0 °C) is the land snow line, `seaIceTemp` (-2 °C) the ocean sea-ice line — raise either to grow the white caps. Render constants (night-floor brightness, terminator softness, snow-blend ramp) live in `rebuildLiveOverlay()` (src/render/Viewer.cpp), not config. - **Moons & tides (`tideAmplitude`/`tideSunFactor`, `planet.cfg`):** `tideAmplitude` (0.6 m) scales the equilibrium tide per unit tide-raising weight (raise for a more dramatic coastline swing); `tideSunFactor` (0.46) is the sun's tide weight vs a unit moon. Moon count (1–3) and per-moon orbit/period/inclination/mass are randomized in `generateMoons()` (PlanetOcean.cpp); the 3D sun distance/size, moon orbit-render band and eclipse angles are render constants (ViewerRender.cpp / `rebuildLiveOverlay`), not config. - **Volcanoes (`volcano*` in PlanetConfig / `planet.cfg`):** placement by tectonic context — `volcanoProbRidge` (0.55), `volcanoProbBorder` (0.06), `volcanoProbInterior` (0.003) are the per-cell placement probabilities for young-ridge / plate-border / interior cells (raise for more vents of that kind), `volcanoMaxCount` (60) caps the total (reservoir-sampled so the ratios hold). Lifecycle — `volcanoInitialBuildMax` (2500 m) pre-builds some vents on entry, `volcanoBuildRate` (0.02 m/h at activity 1) grows active vents, `volcanoFreeHeight` (1000 m absolute) protects deep vents from dormancy, and `volcanoMaxHeight` (3200 m built) is the soft height where dormancy becomes certain. Dormancy/explosions — `volcanoDormancyRate` (1/year scale), `volcanoDormantMinYears`/`MaxYears` (120/1200), `volcanoExplodeDropFrac` (0.20), `volcanoActivityDecay` (0.70), and `volcanoDeadActivity` (0.05). Ash FX — `volcanoBlastRadius` (0.09 rad), `volcanoBlastCloud` (1.5), `volcanoAshMinYears`/`MaxYears` (0.5/3), `volcanoAshPuffCellsPerWeek` (2), `volcanoAshCloud` (0.9), and `volcanoAshCooling` (6 °C). Marker sizes/colours are render constants (ViewerRender.cpp). - **Geography / atlas (`geo*` in PlanetConfig / `planet.cfg`):** feature-extraction thresholds — `geoContinentMinCells` (40, land component ≥ this = Continent, else Island), `geoSeaMaxCells` (60, ocean **basin** ≤ this = Sea, else Ocean), `geoMountainElev` (2500 m, min elevation for a mountain-range cell), `geoRangeMinCells` (4, min cells for a named range), `geoRiverMinDischarge` (80, min mouth discharge for a named river), and the label-clutter caps `geoMaxRivers` (40) / `geoMaxPeaks` (40, largest/highest kept). **Ocean basins** — the connected world ocean is split into several named oceans by a distance-from-land watershed: `geoOceanSepRadians` (1.40 rad, min angular gap between basin centres — *raise → fewer oceans*, lower → more) and `geoOceanDeep` (4, min rings from land for a cell to seed a basin); defaults give ~4–6 oceans on an Earth-like world, 1 on a waterworld. Feature names are deduped on the **proper-noun root** (no shared roots across kinds), and a new volcanic island is named on the fly (`Planet::nameNewLand`, joins an adjacent landmass or mints a fresh Island). Name flavour (syllable banks, a "language" per continent) + label fonts/colours are constants in NameGen.cpp / ViewerRender.cpp, not config. - **Civilization / settlements (`civ*` in PlanetConfig / `planet.cfg`):** placement (habitability-weighted + clustered) — `civMaxSettlements` (80, cap), `civClusterExp` (3.0, higher = settlements cluster harder on the best land), `civMinSpacingRadians` (0.06 rad, soft suppression scale around each pick — lower = tighter clusters), `civMinHabitability` (0.22, don't place below this). Habitability blend — `civHabWaterWeight` (0.45), `civHabFoodWeight` (0.40, the rest is temperature comfort), `civHabTempOpt` (18 °C, most comfortable mean), `civHabElevPenalty` (2500 m, high terrain steeply penalised above this). Population — `civSeedPopulation` (250, initial village), `civGrowthRate` (0.02/yr logistic rate), `civMaxPopulation` (2e6, the carrying capacity at habitability 1), tier thresholds `civTownPop` (5000) / `civCityPop` (100000), `civAbandonPop` (50, below = abandoned/ruins but can revive). **Dynamics** — `civSiteVariety` (1.0; 0 = flat capacities, higher = big rivers/coasts host far larger cities → wide size spread), `civGrowthMin` (0.25, growth-rate floor at habitability 0), `civHarvestVar` (0.25, year-to-year harvest swing, scaled by continentality), `civDroughtStrength` (0.70) / `civDroughtPeriod` (8 yr) / `civDroughtThresh` (−0.15) / `civDroughtArid` (0.50, drought-proneness in arid regions), `civColdYearStrength` (0.50), `civFloodBonus` (0.25, river silt), `civFamineRate` (0.15, accelerated loss when food < population), `civStormDeathRate` (0.50) / `civHurricaneDeathMult` (3.0, deaths from a storm/hurricane over a town). Droughts/harvests are deterministic per (~20° region, year, seed); an active volcano's ash within ~1.5× its blast radius also cuts capacity. Marker sizes/colours + hardship tint are render constants (ViewerRender.cpp). - **Territory & nations (`civTerritory*`/`civVassal*`/`civEmpire*`, `planet.cfg`; key `P`):** `civTerritoryBase` (0.035 rad, a village's reach), `civTerritoryScale` (0.05 rad per log10 of population/seed — big cities reach far), `civTerritoryMax` (0.35 rad cap); `civVassalRange` (1.5 × a capital's range = its annexation reach for vassal towns → bigger = larger kingdoms); empire threshold `civEmpireMinMembers` (5 settlements) / `civEmpirePop` (5e6 total). Territory + realms are **derived** (recomputed each sim year, not saved). Realm colours/border colour/labels are render constants (Colors.cpp / ViewerRender.cpp). - **Culture, beliefs & governments (civ Step 4, key `X`):** **no config knobs** — the ethos/faith/ government rules are internal constants in `PlanetCulture.cpp` (ethos-signal threshold 0.34, the biome→faith map, the tier→government picks) and the culture colours/border colour are render constants. Cultures are derived (recomputed with territory, not saved). To retune, edit `computeCultures()`. - **Conflict & war (`war*` in PlanetConfig / `planet.cfg`; civ Step 5, save v21):** `warMaxConcurrent` (6, simultaneous wars), `warDeclareRate` (0.12, war-declaration chance × hostility), the hostility weights `warAmbition` (1.0, size gap) / `warIdeology` (0.8, culture+faith difference) / `warBorder` (0.5, contested frontier), `warWarlikeMult` (1.4, Warlike-ethos strength bonus), `warCasualtyRate` (0.06, per-year frontier-city population loss, loser more), `warConquerScore` (0.6, |warscore| to take a city), `warSackChance` (0.3, raze vs flip a taken city), `warExhaustion` (1.5, |warscore| to end in peace — also a hard 60-battle-year cap), `warRevoltRate` (0.04, per-year base revolt of a held foreign/ distant city), `warMinRealmPop` (2000, realms below this don't start wars). Adjacency is by settlement proximity (`civTerritoryMax·1.5`); the red war-front/marker colours are render constants. War runs on the yearly tick and is **saved** (allegiance + wars + war RNG) — tune for a warlike or peaceable world. - **Diplomacy (`diplo*` in PlanetConfig / `planet.cfg`; civ Step 6, save v22):** realm-pair attitude drift → alliances / rivalries / coalitions. `diploDriftRate` (0.06, how fast attitudes move), `diploAffinity` (1.0, pull from shared culture + faith vs difference), `diploWarPenalty` (0.5, extra attitude drop while at war), `diploTruceYears` (12, post-war truce length), `diploWarGrudge` (0.4, one-off attitude drop when a war ends), and the kind thresholds `diploAllyThreshold` (0.5) / `diploNonAggThreshold` (0.2) / `diploRivalThreshold` (−0.5). Ally/rival arc colours are render constants (ViewerRender/Overlays). Runs in the same yearly `stepConflict` tick; raise `diploDriftRate`/`diploAffinity` for a more alliance-heavy world, lower the thresholds' spread for touchier politics. - **Trade & economy (`trade*` in PlanetConfig / `planet.cfg`; civ Step 7, key `Z`, derived → no save bump):** route reach — `tradeLandRange` (0.10 rad overland), `tradeSeaRange` (0.30, extra when both coastal), `tradeRiverBonus` (0.06, on rivers); `tradeMinVolume` (0.05, link cutoff); economy — `tradeProsperityWeight` (0.8, how much a hub's prosperity multiplies its carrying capacity → bigger cities), `tradeWarBlock` (0.0, trade-volume factor between realms at war), `tradeAllyBonus` (1.5, between allies); feedback — `tradeDiploBonus` (0.1, attitude/yr between trade partners), `tradeTemptWar` (0.3, war-hostility from a wealthy weak target). Prosperity/routes are recomputed each sim year in `rebuildTerritory` (no RNG, not saved). Wealth-ramp + route colours are render constants (Colors.cpp/Overlays.cpp). Raise `tradeProsperityWeight` for a more hub-dominated world, `tradeSeaRange` for more maritime trade. - `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main knob for how fast/high relief builds. - `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks asymptote at `base + perTickUplift/relax`, so raising it lowers/flattens the equilibrium; lowering it makes relief taller and slower to settle. - `beltWidth` (PlanetConfig) — how many cell-rings a mountain belt spreads inland (belt width / flank extent). - `trenchFactor` (Planet::step) — depth multiplier for subduction trenches. - `driftSpeed` (Planet::assignPlates) — how fast boundary stress builds. - **Plate lifecycle (PlanetConfig, Phase 2 inc. 1.5):** `splitFraction` (0.20, plate share that may rift), `splitProbBase`/`splitProbSlope` (fission prob ramp), `splitCheckEvery` (10, iters between periodic checks), `stalemateEps` / `stalemateBoost` (deadlock detection + kick), `stalemateWindows` (4, consecutive stuck windows before a kick — raise to calm border jitter), `babyMinCells` (4, baby blobs smaller than this dissolve as noise), `babyPromoteFrac` (0.7%, ridge-strip size to become a real plate), `volcanicLandFrac` / `volcanicElev` (land grown on promotion), `landBand` (0.10, soft land-conservation band), `miniPlateCells` (50, below = "mini") + `fuseMinPlates` (3, cluster size to fuse + steal). Tune these to control how lively / fragmented the plate map stays. - **Erosion + sea level (PlanetConfig, Phase 2 inc. 2):** `erosionLandRate` (0.08) / `erosionSeaRate` (0.02) — fraction/My worn off above / below sea level (raise for faster, flatter terrain); `landFractionTarget` (0.30) — geographic land goal; `seaLevelStep` (100 m) — fixed nudge per adjustment + `seaLevelTol` (0.02) — deadband where sea level rests (raise step or shrink tol for a tighter 30/70, but a big step can overshoot a flat "cliff"); `seaLevelEvery` (100) — iterations between sea-level updates (higher = more gradual). All editable in `planet.cfg`. - **Orogeny — taller mountains (PlanetConfig, Phase 2 inc. 4):** `collisionFactor` (1.8, continent-continent uplift, Himalaya) and `arcFactor` (1.4, continental subduction-arc uplift, Andes) — raise either to make ranges taller/more reliable across seeds; `isostaticPersist` (0.85, how strongly high crust resists relax — toward 1 = ranges barely erode and continents stay high; lower = more dynamic rise/erode) saturating at `rootScale` (2500 m above `continentBase`). These three boosts are **drift-only** (gated on `Planet::drifting`); in Phase-2 drift the per-tick `erode()` is what limits their height, so they don't rail the clamp. - **Soft peak cap (drift-only) — no 9000 m plateau:** previously the hard `[-11000, 9000]` clamp (in `step()`, `erode()` *and* `hydrology()`) flattened many drift-time peaks into a 9000 m plateau. Now growth is **probabilistic** above `peakSoftCapStart` (7000 m): the chance a tick's positive uplift "takes" falls linearly to 0 at `peakSoftCapEnd` (12000 m), so peaks spread smoothly across a height band instead of railing at one ceiling. A **lost** grow roll forfeits that tick's uplift *and* shaves a random `0..peakFailDrop` (200 m) off, so a peak hovers near its own height (height now tracks orogeny strength: strong seeds reach 10–11 km, most cluster lower). The hard clamp's **upper bound now tracks `peakSoftCapEnd`** in all three places (just a safety rail; lower −11000 m unchanged). The roll is a **pure hash of `(cellIndex, erodeIter, seed)`** — never touches `rngState`, stays bit-identical across OpenMP thread counts, and since `erodeIter` is saved (step/erode run 1:1 in drift) F5/F9 resumes **bit-identical** (no save-version bump). **Drift-only** (gated on `drifting`) so Phase-1 forming still auto-settles. Tune `peakSoftCapStart` / `peakSoftCapEnd` / `peakFailDrop` in `planet.cfg`. Verified headless: smooth 8.5→10.5 km taper, 0 cells pinned at the ceiling, determinism + exact resume intact. - **Seafloor aging->depth (PlanetConfig, Phase 2 inc. 4):** `oceanBase` is now the **deep abyssal floor** (-6000 m, not a flat ocean base) and `ridgeDepth` the shallow young value (-2500 m); `seafloorSubsidence` (280 m per sqrt(My)) sets how fast oceanic crust deepens with `geoAge` (half-space cooling), and `seafloorSeedAge` (80 My) is the initial oceanic age spread at generation so the starting seafloor already has ridge->abyss variety. - **Hydrology (PlanetConfig, Phase 3 inc. 1):** `phase3AfterMy` (300) — drift-My before the Phase-3 prompt; `phase3DtScale` (0.2) — Phase-3 timestep = `cflDtMy()*this` (smaller = finer carving, slower drift per step); `rainfall` (1.0) — uniform precip per cell (drainage-area unit; orographic precip is a later climate add-on); `riverThreshold` (25) — discharge above which a cell is a river (also the river-render threshold); `riverIncision` K (0.02) + `riverDischargeExp` m (0.5) + `riverSlopeExp` n (1.0) — stream-power incision `K*Q^m*S^n*dt` (raise K for faster valley carving); `riverTransport` (0.1) — transport capacity `cap=this*Q*S` and `depFrac` (0.25) — deposition rate of excess load (raise both for more deltas / faster lake infill). All editable in `planet.cfg`. ## Conventions - All code, identifiers, comments and filenames in **English**. - Before generating code, summarize the approach and ask whether to proceed. - Prefer **inline code in chat** over attachments; give exact file contents rather than long explanations. - Direct, concrete answers. Metric system throughout. - C++17, multi-file CMake. Keep simulation logic free of raylib so it stays testable headless: engine in `src/sim` (no raylib), all rendering in `src/render`. - Geometry stays fixed — never make cells move; add new per-cell properties and flow them over the existing grid + neighbor adjacency.