# Design notes (durable context) These are the non-obvious decisions/conventions that were previously only in Claude's auto-memory (which lives under `~/.claude/` and does **not** travel with the repo). Captured here so the context survives a move to another machine/server. `CLAUDE.md` has the authoritative current-state changelog; this is the "why / where things live" summary. ## Framing: World Creation → Live World The roadmap is no longer rigid numbered "phases". **World Creation** is a set of continuous, overlapping stages on a geological clock (My): tectonics → continental drift & erosion → hydrology → climate → biomes → (fauna & flora, next). The long-term goal is a separate **Live World** mode that runs the *finished* planet at a much slower real-time clock (hours/days/weeks/months) with dynamic weather (clouds, rain, storms) and living ecosystems/civilization. **Internal code still uses `phase*` names** (`Planet::drifting`, the `phase3` flag, `phase3AfterMy`/`phase3DtScale` config keys) for save/config compatibility — only display strings and docs use the new framing. ## Code module layout Split into a raylib-free **engine** (`src/sim/`, headless-testable) and a raylib **viewer** (`src/render/`); `src/main.cpp` is a ~10-line entry point. CMake adds both dirs to the include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`). `Planet` is **one class implemented across several .cpp files** (all share `Planet.hpp`): - `PlanetTypes.hpp` — `Cell` / `Plate` / `SubGrid` / `Biome` enum / `PlanetConfig`. - `Planet.cpp` — generation, geometry, plate seeding, RNG + shared helpers, subgrid, min/max. - `PlanetTectonics.cpp` — `step()` (stress→uplift→relax; orogeny boosts gated on `drifting`). - `PlanetDrift.cpp` — `cflDtMy`/`advect` + plate lifecycle (fission/kick/baby/fuse/enclosed). - `PlanetErosion.cpp` — `erode` + `adjustSeaLevel`. - `PlanetHydrology.cpp` — `routeFlow`/`computeHydrology`/`hydrology` (depression-fill→lakes, steepest-descent→rivers, mass-conserving stream-power incision). - `PlanetClimate.cpp` — `computeClimate()` (temperature + orographic precipitation). - `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate). - `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw + `computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga). - `PlanetFloraGen.cpp` / `PlanetFaunaGen.cpp` / `PlanetFungiGen.cpp` — per-kind density + per-cell `fill*` (fauna gates carnivores on local prey; funga is moisture/organic-led). - `PlanetIO.cpp` — text config + binary save/load. The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration), `ViewerInput.cpp` (camera/picking/keys), `ViewerRender.cpp` (globe/map/panels/HUD/prompt), plus topical helpers `Colors` / `Map2D` / `Overlays` / `Picking` / `Panels`. Per-tick order in `Viewer::refreshView()`: `computeHydrology()` (if hydrology on) → `computeClimate()` → `classifyBiomes()` → `computeBiotaDensity()` → `recolor()`. The discrete biota *population* (`generateBiota()`) is NOT in this per-tick path — it's on-demand (key `L`). ## Core principle (do not violate) Geometry is **fixed** — cells (icosphere vertices) never move. Only per-cell *properties* flow over the fixed grid + neighbor adjacency (Eulerian). New phenomena = new per-cell fields flowed over the grid, never moving cells. ## Axial tilt render convention (non-obvious) The 3D globe is rendered leaned by `cfg.axialTilt` via `rlRotatef(tilt,0,0,1)` wrapping all 3D content in `renderGlobe3D`. Because that rotation isn't in the data, anything mapping between world and model space must compensate with `rotateZ(v, ±tilt)` (src/render/ Picking.cpp): 3D picking un-rotates the ray hit by `−tilt` before `nearestCell`; 3D plate labels rotate by `+tilt` before projecting. The 2D map + biome/climate are tilt-independent. ## Save format (v7) — self-describing config + biota population `planet.save` stores `PlanetConfig` as a **self-describing key=value text block** (not a raw POD dump), parsed like `planet.cfg` (`writeConfigFields`/`parseConfigStream` shared in PlanetIO.cpp), written at `precision(17)` so doubles round-trip exactly. Consequence: **adding/removing PlanetConfig fields no longer breaks saves** (unknown keys ignored, missing keys keep defaults). v6 cannot load pre-v6 saves (one-time break; a length guard fails it gracefully). Per-cell `Cell.biome` is saved (a byte appended after `invader`). **v7** appends the **biota population** (`sBiota`): a flag byte, then three `Organism{uint16 archetype, uint8 biome}` lists per cell. Densities are derived (not saved). Older saves without the block load fine with an empty population (`readState(is, hasBiome, hasBiota)`; `hasBiota = ver>=7`). ## Biota (flora / fauna / funga) — density + slot/point population Two layers (`PlanetBiota.cpp` + the three `*Gen.cpp`): (1) derived per-cell **density** scalars (0..1) recomputed each tick like climate — flora = Liebig-min(temp, moisture), fauna ∝ flora (carnivores gated on neighbourhood prey ≥ `bioCarnPreyMin`), funga = moisture/organic-matter-led + cold-tolerant; 0 on water/Ice. (2) On-demand discrete **population** `generateBiota()`: each land cell draws broad archetypes from the comprehensive append-only `biotaArchetypes()` table into a per-kind slot cap + density-scaled point budget (size → cost Tiny=1…Huge=5), weighted by biome/climate suitability and a **regional bonus** for archetypes already in same-biome neighbours (single index-ordered pass → homogeneous regions, boundary variety). Organisms are labelled by **taxonomy** — Family + Size + role (full `Class > Order > Family` in `organismTaxonomy()`), never informal common names ("Felidae", not "cat"); generalist families get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seeded from `cfg.seed`** (not `Planet::rngState`) so populating biota never perturbs tectonic determinism — asserted in `test_biota.cpp`. The archetype table is **append-only** (indices are serialized in v7 saves). ## Climate + biome model (derived, not saved) `computeClimate()` builds two derived per-cell fields: - **Temperature** (°C) = latitude curve (`biomeEquatorTemp/PoleDrop/LatExp`, super-linear so cold concentrates at poles) − `biomeElevLapse` × elevation. This is the **annual mean**; the **Seasons** pass adds derived `sTempSummer`/`sTempWinter` = mean ± `A`, where the seasonal half-amplitude `A = seasonAmpMax · sin(axialTilt)/sin(23.44°) · latShape · continentality`. Continentality is a multi-source-BFS ring distance from ocean cells (oceans/coasts muted by thermal inertia; interiors swing most). `classifyBiomes()` blends winter temp into the Tundra/Taiga cold cutoffs via `biomeSeasonWeight` (0 = mean only → unchanged biomes), so cold-winter continental interiors turn boreal/tundra. Seasonal fields are derived/not-saved. - **Precipitation**: zonal prevailing winds (easterly tropics/poles, westerly mid-lat); ocean cells are a moisture source; each land cell takes its **upwind** neighbour's moisture, rains out more on windward upslopes (orographic), loses a multiplicative fraction per cell (continentality) → leeward/interior drying. The raw field is near-binary, so it's **diffused** (`climateMoistureSmooth` passes) into transition zones, then normalized to `sMoist∈[0,1]` by anchoring the **median land precip → 0.5** (robust to orographic spikes). `classifyBiomes()` reads `sTemp` + `sMoist` (not a latitude hack) → rain-shadow/interior deserts emerge; 13 biomes incl. polar Ice; wetlands require water adjacency. All biome & climate thresholds are tunable `biome*` / `climate*` keys in `planet.cfg`. ## Headless testing Engine is raylib-free, so logic is tested without a display. Build/run: ``` 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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \ src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t # test_biota.cpp uses the same source list (Biota suite). ``` (add new `src/sim/*.cpp` to that list as stages are added). `Planet::step()` passes are data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).