# 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). **Big-city demography** (the growth rebalance — cities used to climb a huge stacked K exponentially for millennia, "endless growth"): an **urban crowding** mortality rises with the square of city size (`civCrowdingLoss·(P/civMetropolisPop)²`), so the best trade hubs **plateau at a historical metropolis scale (~1–2M)** instead of chasing K; the good-year `cond` upside in the K *target* is capped (`civCondBoomCap` — no boom-year ratchet; droughts still bite uncapped); and rare deterministic **plagues** (`civPlague*`, pure hash of (id, year, seed) — a 1–3-year wave killing 20–40% at full exposure) strike **cities** (exposure 0 below ~30k) harder when **trade-connected** (contagion travels the routes — the cost of being a hub; derived `sCivPlague`, a cell-info `PLAGUE` line + "Plague ravages X" / "Plague shrinks X" kind=3 events). 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*`. - **Colonization (kingdoms found colonies + islands)** *(done — see `PlanetCiv.cpp` `stepColonization`)* — the settlement set is no longer fixed after `U`. Once per sim year a **kingdom+** realm (`totalPop ≥ civColonyMinPop`) may **found a new settlement** on the best **unclaimed** (wilderness) high-habitability, well-spaced cell within reach of a member — **overland** (`civColonyReach`) or **across water** from a **coastal** member (`civColonySeaReach`, so **islands / other continents** are colonised). The colony is **appended** to `settlements`, bound to the founder's realm by **allegiance** (Step 5, so it joins the founder's realm even across cultures/water) and given a **colonial supply trade link** to the founder (Step 7, `civColonySupply`) so its prosperity keeps it alive on marginal land — **as long as the founder is alive** (when the founder's capital falls, Step-5 pruning clears the allegiance → the supply ends → the colony subsists on local food/trade and may wither). Deterministic (pure hashes, no RNG); the set **grows by appending** (saved as-is — the v20 settlement block is variable-length; **no save-version bump**), and **step-back truncates** it (`restoreWeather` resizes to the snapshot's settlement count → a colony re-founds identically on replay). Founding logs a **kind=3** event ("The Kingdom of X founds the colony of Y"). Knobs `civColon*`. *(Also fixed: pressing `R` (reseed) after civilization existed left stale territory/culture/war/diplomacy/trade overlay lines on the new world — `regenWorld()` now clears the civ overlay vectors + toggles.)* - **Cultural evolution (Step 8)** *(done — see `PlanetCulture.cpp` `stepCulture`, save v23)* — cultures stop being static one-per-continent blocs. Culture identities are now **stateful**: the list is **append-only** (seeded once at the dawn — the same one-per-continent peoples as before — with schism children appended later; a record is **frozen after creation** so indices/colours stay stable and the ethos/faith no longer silently re-derive) and the **per-settlement culture is mutable state**. `stepCulture(year)` runs in the yearly tick (between `stepConflict` and `stepColonization`), all **pure hashes** of (stable id, year, seed) — no RNG: **assimilation** (a settlement held by a foreign-culture overlord adopts its ruler's culture, `cultAssimRate` — which drops the Step-5 revolt `cultBonus` on its own, so assimilation pacifies provinces), **border conversion** (a settlement dwarfed by a nearby foreign culture's weight = population × trade prestige converts toward it, `cultConvert*`/`cultSpreadRange`/ `cultPrestigeWeight`; **realm capitals are exempt** — they anchor identity), and **schism** (a large culture's far-flung coherent cluster — typically overseas colonies — breaks away as a **new people**, `cultSchism*`: a fresh NameGen name from the local language bank, ethos/faith **re-derived from its own lands** — keeping the parent's faith name if the faith is unchanged — parent id + founding year recorded). **Colonies now inherit the founder's culture** at founding (the seed for later colonial schisms), and the Step-3 mono-cultural vassalage rule is **culture-matched** (regionId fallback before the dawn / for -1 entries) — a schism cluster stops vassalizing to the old capital, founds its own realm, and the culture/faith diplomacy affinity drifts parent and child toward rivalry → **colonial independence wars emerge**. The per-cell culture view (`X`) now colours by the owning **settlement's** culture, so a conquered city keeps its people's colour until it assimilates. `computeCultures()` became a pure derived refresh (tallies/governments/cell view; seeds only when the list is empty — the dawn / pre-v23-load path); the state — culture identities + `sSettleCulture` (+ a next-id counter) — is **saved (v23)** and **snapshotted via truncate-and-replay**: the list is append-only and `stepCulture` is a pure function, so a step-back frame stores just the per-settlement vector + the list *length*, `,` truncates schism children and a replay re-creates them identically (the colony-truncation precedent). **Kind=7** `WorldEvent`s ("X adopts the culture of the Velmar", "X embraces the ways of the Nharos", "The Nharos break away from the Velmar"); the **Cultures** tab hides extinct peoples (members 0 — slots persist) and shows a schism child's founding year. Knobs `cult*`. *Next steps (not yet built): tribute / vassalage treaties; an accumulated treasury (funding armies / buying peace).* - **Edit mode (manual world editing)** *(done — see `PlanetEdit.cpp`, save v24)* — the "between phases the user can edit the world" hook from the project's roadmap. Key **`F3`** (a settled world; auto-pauses) toggles it; click a tile to open an **Edit Mode** panel (replaces the detail panel) tabbed **Geology** (elevation, plate, crust type, geological age, biome) / **Climate** (temperature, moisture) / **Biota** (flora/fauna/funga density + the discrete population list, add/remove organisms) / **Settlement** (population, allegiance, culture — only shown when the tile hosts one). Each row is a `Planet::setXxx()` call (`PlanetEdit.cpp`) that writes straight into the field the sim already reads next tick — click a numeric value to type a new one (Enter commits, Escape cancels) or scroll the wheel over it to nudge; click a discrete field (plate/biome/crust/allegiance/culture) to cycle or pick from a list; click **+ add** to insert a biota organism from the archetype table. Every field also has a **lock** button — unlocked, an edit is a one-off "nudge" the simulation keeps evolving afterward (like a meteor impact); locked, a new per-cell `EditLock` bitmask (`PlanetTypes.hpp`) exempts that field from its normal per-tick recompute (`PlanetTectonics`/`Erosion`/`Hydrology`/`Drift`/`Biomes.cpp` skip a locked cell's write; `PlanetClimate`/`FloraGen`/`FaunaGen`/`FungiGen`/`Civ.cpp` re-apply a pinned value from a small sparse map after their normal recompute, since those fields have no other persistent storage of their own) until unlocked again. Settlement locks are keyed by the settlement's home cell and gate `stepCivilization()` / `stepConflict()` / `stepCulture()` for it. Elevation/plate/crust/geoAge/biome locks are a bare bit (no extra storage — the Cell field itself is already the save-format source of truth); a plate **fission/ merge** event (whole-plate cell reassignment) is not individually lock-checked. **Save v24** appends the per-cell `editLock` bitmask + the sparse locked-value maps (climate/biota-density/habitability); older saves load with no locks. Headless `test_edit.cpp`: nudge-vs-lock behaviour for every field, organism add/remove, save v24 round-trip, corrupt-stream rejection, pre-v24 compatibility. - **Toolbar (clickable menu)** *(done — see `src/render/Toolbar.{hpp,cpp}`)* — the app was previously 100% keyboard-driven (~40 single-key shortcuts dumped as a dense text list in the HUD); this adds a discoverable, mouse-friendly **sidebar menu** overlaying the top-right of the 3D globe, built with **raygui** (raylib's official header-only widget library, fetched via CMake `FetchContent` `DOWNLOAD_ONLY` like raylib itself — `src/render/RayGuiImpl.cpp` holds the one `RAYGUI_IMPLEMENTATION` translation unit, re-skinned dark via `setupToolbarStyle()` to match the app's palette). Every existing keyboard shortcut still works **unchanged** — the toolbar is purely additive, and its widgets call the exact same `Viewer::` methods the keys do (16 key-handler bodies — `P`/`X`/`Z`/`U`/`I`/`E`/`L`/`H`/`W`/`Y`/`M`/`Shift+M`/`R`/`S`/`F`/`F3` — were extracted from `ViewerInput.cpp` into named methods in `Viewer.cpp` for exactly this sharing, guard included, so keyboard and toolbar can never diverge in behaviour). Sections: a **View Mode** dropdown (the 13 plain colour modes, replacing keys `1`-`0` and expanding the old `6`-key cycle into 4 explicit entries), **Overlays** (checkboxes for `B`/`D`/`G`/`J`/`N`/`T`/`O`/`K`/`V`/`M`), **World/Time** (play/pause, step, fast-forward, a log-scaled speed slider over `driftRate`/`liveRate`, hydrology, reseed, save/load), **Civilization & Ecology** (`E`/`I`/`L`/`U`/`P`/`X`/`Z`, grey until `settled`), **Live World** (`W`/`Y`/`.`/`,`) and **Edit Mode** (`F3`). Key **`F1`** collapses/expands the sidebar (default expanded, for first-run discoverability); a `bool inToolbar` gate in `handleInput()` (mirrors the existing `inPanel`/`inLiveInfo` pattern) keeps interacting with it from also orbiting the camera or picking a globe tile underneath, and the whole panel is `GuiDisable()`d while the Phase-3 modal prompt is up (same exclusivity the rest of the input already has). One real bug found + fixed while building this: `GuiCheckBox` treats its `bounds` as the checkbox glyph itself (raygui auto-positions the label outside it using the label's own width) rather than as a whole clickable row — a naive full-row-width checkbox call just drew an oversized glyph with the label clipped off-panel; fixed by a small `guiCheckRow()` helper (`Toolbar.cpp`) that passes a compact glyph rect and draws the label itself at a known position. Purely `src/render` — no save-format or `src/sim` change. - **High-resolution map export** *(done — see `Viewer::exportMapImage()`, `src/render/ViewerRender.cpp`)* — key **`F11`** (also a toolbar "Export Map" button, `World / Time` section) renders the full (unzoomed) 2D Equal Earth map into an off-screen `RenderTexture2D` at **~4K** (3840 px wide, height from `mapRect`'s current aspect ratio) and saves it as a timestamped `map_export_YYYY-MM-DD_HHMMSS.png` — a "poster" export independent of the on-screen window size/zoom. Keeps the current colour mode and longitude pan and respects every overlay toggle (borders/rivers/drift/tides/currents/clouds/volcanoes/ settlements/names — "exports what you're looking at, just bigger"), except minor place-name labels (peaks/rivers/lakes/seas/small islands) are always shown regardless of on-screen zoom, since a 4K canvas has the room. Enabled by a refactor: the ~90-line overlay-drawing body of `renderMap2D()` was extracted into a shared `Viewer::drawMapOverlays(Rectangle vr, double lonOffset, float scale, float markerZoom, bool showMinorLabels)` (line widths × `scale`, font sizes × `scale`, marker radii × `markerZoom`) — the on-screen path passes `scale=1`/`markerZoom=min(2,mapZoom)` (pixel-identical to before), the export path passes the export/on-screen width ratio for both, so the two callers can never diverge. This works because every `Map2D`/`Overlays` draw function was already parameterized by an arbitrary target `Rectangle`, not the window size — the existing `map2D` member (built at on-screen resolution) is reused as-is for the export, since only its per-cell `lon`/`lat` are read (screen position is recomputed per-rect by `drawMapTris`/`projLonLat`, so it isn't tied to the rect it was built with). One real bug found + fixed while building this: the toolbar button initially called `exportMapImage()` **synchronously** from inside its `GuiButton` click check — but `drawToolbar()` wraps its whole scrollable content in a `BeginScissorMode`/`EndScissorMode` pair (for the scroll-panel clip), so the export's render-texture pass inherited the toolbar's small on-screen clip rect and produced an almost-empty image. Fixed by deferring: the button just sets `Viewer::pendingMapExport`, and `renderFrame()` calls `exportMapImage()` right after `drawToolbar()` returns (scissor mode guaranteed closed by then) — the `F11` key path was never affected (input handling runs before `BeginDrawing()`, outside any scissor state). Purely `src/render` — no save-format or `src/sim` change. ## 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/stepColonization (settlements + colonies; civ Step 2) PlanetNation.* computeTerritory (realms + per-cell ownership + borders; civ Step 3) PlanetCulture.* computeCultures (derived refresh + one-time seeding; civ Step 4) + stepCulture (assimilation/conversion/schism; civ Step 8, save v23) 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) PlanetEdit.cpp manual cell/settlement edits + the EditLock lock mechanism (Edit Mode, save v24) 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, edit-mode panel Toolbar.* clickable raygui sidebar menu (mouse-friendly counterpart to the key list) RayGuiImpl.cpp the one RAYGUI_IMPLEMENTATION translation unit (raygui.h is declaration-only elsewhere) 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. raygui 5.0 (the toolbar's widget library) is likewise fetched via `FetchContent` (`DOWNLOAD_ONLY` — it's a single header, no build step of its own). ### 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/PlanetEdit.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`, `test_trade.cpp`, `test_colony.cpp`, `test_cultevo.cpp` or `test_edit.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions / Civilization / Nation / Culture / Conflict / Diplomacy / Trade / Colony / Cultural-evolution / Edit-mode 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 · `F3` **edit mode** (a settled world; click a tile to edit every property — geology/climate/biota/ settlement — with per-field lock against automatic recompute; see below) · `F1` toggle the clickable **toolbar menu** (top-right of the 3D globe; every key above also has a button/checkbox/dropdown there — see below) · `+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load · `F12` screenshot (`screenshot.png`) · `F11` export the 2D map as a high-resolution (~4K) `map_export_.png` (also a toolbar button) · `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. **Cultures evolve** on the same yearly tick (`X` view) — conquered cities keep their people's colour until they **assimilate** into the ruler's culture, border towns **convert** under a dominant neighbour's cultural weight, and a far-flung colony cluster can **schism** into a new people (a new name/colour) that founds its own realm — colonial independence wars follow. `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 and cultural shifts** via snapshots). Mouse-wheel over the 2D map zooms (drag pans). Edit mode (`F3`, a settled world; auto-pauses): click a tile to open an **Edit Mode** panel in place of the detail panel, tabbed **Geology** / **Climate** / **Biota** / **Settlement** (the last only when the tile hosts one). Click a numeric value to type a new one (**Enter** commits, **Escape** cancels) or scroll the mouse wheel over it to nudge; click a discrete field (plate/biome/crust type/ allegiance/culture) to pick from a list; click **+ add** to place a biota organism, **×** to remove one. Every field has its own **lock** button — unlocked, an edit is a one-off nudge the simulation keeps evolving afterward (like a meteor impact); locked, that field is exempted from its normal automatic recompute until unlocked again (elevation resists erosion/relaxation, biome/climate/biota- density/habitability stay pinned, a settlement's population/allegiance/culture stop changing on their own). Saved (v24). Toolbar (`F1`, default expanded): a clickable raygui sidebar over the top-right of the 3D globe with a button/checkbox/dropdown for essentially every key above, grouped into View Mode / Overlays / World & Time / Civilization & Ecology / Live World / Edit Mode. Purely additive — every keyboard shortcut keeps working exactly as documented above; the sidebar is just a more discoverable way to reach the same actions. Grey/disabled rows mean the action needs a settled world (or Live World); the panel scrolls (mouse wheel) past the World/Time controls to reach Civilization/Live World/Edit Mode. Locked out (`GuiDisable`d) while the Phase-3 "start hydrology?" prompt is up, same as everything else. 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 **24**; 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, v22 the **diplomacy** block — standing realm **relations** (alliances / rivalries / truces), likewise per-frame, and v23 the **culture** block — stateful culture identities (append-only; schism children) + the per-settlement culture + a next-id counter (cultural evolution), likewise per-frame (per-settlement vector + the list *length*; a rewind truncates, a replay re-creates), and v24 the **edit mode** block — a per-cell `editLock` bitmask (manual-edit locks, `F3`) as a trailing vector plus sparse locked-value maps for the climate/biota- density/habitability fields (which have no other persistent storage — the map only holds an entry while its lock bit is set); 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); pre-v23 saves load with no culture state (re-seeded one-per-continent on the next refresh — the same peoples as before; evolution starts from there); pre-v24 saves load with no manual edits/locks. 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` (1e6, the carrying-capacity **scale** — multiplied by habitability/siteQuality/trade, a top hub's K is several × this; the real metropolis ceiling is the crowding below), 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). **Big-city plateau** — `civMetropolisPop` (1.5e6, crowding scale: mortality = `civCrowdingLoss·(P/this)²`/yr; the best hubs plateau near `this·sqrt(growth/civCrowdingLoss)` ≈ 1–1.5M), `civCrowdingLoss` (0.02/yr at P = civMetropolisPop; **0 = the old unbounded behaviour**), `civCondBoomCap` (1.25, cap on the good-year condition upside in the K target — droughts uncapped). **Plagues** — `civPlagueRate` (0.01/yr outbreak chance per settlement), `civPlagueDeathMin`/`Max` (0.20/0.40, total wave kill at full exposure over 1–3 years), `civPlagueTradeWeight` (0.5, exposure share from trade connectivity vs pure size; exposure is 0 below ~30k → villages never plague). Droughts/harvests/plagues are deterministic per (region/settlement, 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` (2.5e6 total, retuned for the plateaued city sizes). 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. Since Step 8 identities are stateful (seeded once, saved v23); to retune the rules, edit `envPick()`/`seedCultures()` in `PlanetCulture.cpp` (a reseed applies them). - **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. - **Colonization (`civColon*` / `civMaxColonies` in PlanetConfig / `planet.cfg`; derived, no save bump):** `civColonizeRate` (0.15, per-eligible-realm per-year chance to found a colony), `civColonyMinPop` (2e5, a realm must reach this to colonize — "kingdoms"), `civColonyMinHab` (0.30, min site habitability), `civColonyReach` (0.12 rad, overland reach) / `civColonySeaReach` (0.35, extra reach over water from a coastal member → islands), `civColonySpacing` (0.05 rad, min gap from existing settlements), `civMaxColonies` (120, cap beyond `civMaxSettlements`), `civColonySupply` (2.0, prosperity from the overlord supply link that keeps colonies alive). Runs in the yearly `stepColonization` tick; raise the rate/reach for aggressive colonial empires. - **Cultural evolution (`cult*` in PlanetConfig / `planet.cfg`; civ Step 8, save v23):** assimilation — `cultAssimRate` (0.03, per-year chance a conquered settlement adopts its ruler's culture); border conversion — `cultConvertRate` (0.02, per-year chance scale), `cultConvertDominance` (2.5, foreign pressure must exceed this × own support — raise to make cultures stickier), `cultSpreadRange` (0.25 rad, how far a settlement projects cultural pressure), `cultPrestigeWeight` (0.5, trade prosperity's boost to cultural weight — rich hubs radiate culture); schism — `cultSchismMinMembers` (6, min living settlements to schism), `cultSchismRange` (0.55 rad from the population centroid past which members are "distant" — lower = easier colonial breakaways), `cultSchismMinCluster` (2, distant settlements needed to break away together), `cultSchismRate` (0.08, per-year chance per qualifying culture). All rates 0 = the static pre-Step-8 world. Runs in the yearly `stepCulture` tick (pure hashes, no RNG); the culture list is capped at 64 peoples. - `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.