planetsim/CLAUDE.md
Jonas Reith ca844a5d3f Live World weather: moving systems (lows, hurricanes & typhoons)
The base cloud/rain field relaxes to a static pattern under fixed forcing, so it looked
frozen. stepWeather now also runs a population of drifting WeatherSystem agents (world
objects, not cells; transient/not saved; separate sWeatherRng seeded from cfg.seed so
tectonic determinism is intact): spawn over warm tropical ocean (5-25 deg, SST gate) or a
mid-latitude ocean low (capped at weatherSystemMax); move along the steering wind (sWind at
the nearest cell) plus a poleward recurve at weatherSystemSpeed; intensify over warm sea,
decay and cull over land/cold; and stamp a Gaussian cloud/rain shield onto the grid so cloud
clusters travel and dissipate behind them. 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 by hemisphere) in 3D + 2D, HUD system/cyclone counts, and a basin-named
storm list in the Sky & tides panel -- all under K. New weather* storm knobs. test_weather.cpp
adds: systems spawn, move between steps, thicken cloud, RNG isolation, determinism. All five
suites pass; GUI build clean. Docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:40:58 +02:00

769 lines
57 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 → weeks/months, `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`)***13 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).
Transient (not saved; respawn from the seed). This makes the weather visibly move (the base field
alone relaxes to a static pattern).
## 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<id>` 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 upstreamdownstream 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 subtropicsdeserts) + river discharge +
coastal proximity; classified first-match (iceoceanlakebeachmountainshills
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 wetdry 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, bluered) / `7` (precipitation, drywet). `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** meansummerwinterseasonality; 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()`, 36 entries
across Flora/Fauna/Funga, 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`).
- **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
hourmonth; 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 **13 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 ambercyan, 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; transient, not saved; separate
`sWeatherRng` seeded from `cfg.seed` tectonic determinism intact). Each step: **spawn** over
warm tropical ocean (525°, SST `weatherTropicalSST`) or a mid-latitude (3062°) 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 Sky & tides panel all under `K`. `test_weather.cpp`
adds: systems spawn, move between steps, thicken cloud, RNG isolation, determinism.
- 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)
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)
PlanetIO.cpp config file (text) + binary save/load
render/ (raylib viewer)
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
Map2D.* Equal Earth 2D map: positions + projection/draw helpers
Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
Panels.* right-column UI: detail panel, hover info, world stats
Viewer.{hpp,cpp} Viewer struct: all state + setup + sim orchestration
ViewerInput.cpp handleInput(): camera, hover picking, click, keys
ViewerRender.cpp renderGlobe3D / renderMap2D / renderPanels / renderHUD / renderPrompt
CMakeLists.txt fetches raylib 5.5 via FetchContent; lists src/sim + src/render
BUILD.md dependencies + build/run + controls
```
### Key data structures (src/sim/PlanetTypes.hpp)
- `Cell` `unit` (fixed sphere direction), `elevation` (continuous meters,
double, NOT quantized), `plateId`, `geoAge`, `neighbors`, and a
`std::shared_ptr<SubGrid> subgrid` HOOK (still null on the cell; the viewer
builds subgrids on demand via `Planet::makeSubGrid(cell,res)`).
- `SubGrid`/`SubCell` a res*res patch around one macro cell; elevation is an
inverse-distance blend of that cell + neighbors plus value-noise detail, and
each subcell records `nearestMacro`. Phase-4 preview, generated on click.
- `Plate` `type` (Oceanic/Continental), `driftAxis`, `driftSpeed`.
- `PlanetConfig` `radius` (default 6.371e6 m, Earth), `subdivisions`,
`seaLevel`, `plateCount`, `seed`.
### Resolution notes (important, was discussed in design)
- **Lateral** resolution = cell spacing = `sqrt(4*pi*R^2 / N)`. Coarse on
purpose (~223 km at level 5). Only mountain-range scale, not single hills.
- **Vertical** (elevation) resolution is effectively unlimited: it is a
`double` in meters. 100 m steps or finer are free. Mount-Everest /
Mariana-Trench range is exactly representable.
- Climate (phase 3) reads elevation as a smooth continuous function, so no
resolution is lost by coarse height bands.
- For dense detail later (population/culture), generate a per-cell **subgrid**
on demand and mark which edge sub-cells border which neighbor macro-cell,
so cross-boundary interaction (rain shadow, transition zones) works.
## Build & run
```bash
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/planetsim
```
Target OS is Nobara Linux (KDE/Wayland, Intel Arc A770). Dependency install
line is in BUILD.md (`dnf install cmake gcc-c++ mesa-libGL-devel ...`).
raylib 5.5 is fetched automatically do not vendor it.
### Quick headless logic test (no display needed)
```bash
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
src/sim/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp \
src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetBiota.cpp \
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
```
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp` or
`test_weather.cpp` to run the Biota / Live World / Ocean / Weather suites same source list.)
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) ·
`SPACE` or on-screen button pause ·
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) ·
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
on a settled world; re-press regenerates) · `W` enter/leave **Live World** (settled world) ·
`R` reseed ·
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
Phase 3: after `phase3AfterMy` simulated years a modal prompt asks **Continue
Phase 2** / **Start Phase 3**; `H` starts/stops it manually. In Phase 3 drift
keeps running at a finer timestep (`cflDtMy()*phase3DtScale`) while rivers, lakes
and fluvial erosion evolve.
Live World (`W`, on a settled world): geological drift freezes and a slow real-time clock runs
(`liveTime` in hours, `liveRate` = sim hours/real-second, ramped hour→month 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.
**13 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).
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 **9**; 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); 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.
**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 **"Sky & tides" panel** — `liveInfoRect`, `renderLiveInfo`:
per-moon phase discs + a selected coastal tile's tidal phase); 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<id>` 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);
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, 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 (13) 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.
- `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
1011 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.