A second, reference-style export alongside exportMapImage(): renders at ~8K and labels every named geography feature and every living settlement (settlement names weren't drawn as text anywhere before this), using a greedy priority-based placement pass that only draws a label if its box doesn't collide with one already placed -- so text never overlaps regardless of how densely settlements cluster. Keeps label font size well below the resolution scale, since scaling text 1:1 with a bigger canvas would just reproduce the same crowding at a bigger size. Also uses the plain colour-mode vcolors rather than displayColors(), since in Live World the latter is day/night-dimmed -- a reference atlas should read the same everywhere, not have an arbitrary half of it darkened by whatever moment it was exported.
1327 lines
111 KiB
Markdown
1327 lines
111 KiB
Markdown
# 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 <name> 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.
|
||
- **Labeled atlas export** *(done — see `Viewer::exportAtlasImage()`, `src/render/ViewerRender.cpp`)* —
|
||
a second, reference-style export for "give me every name, readably" (as opposed to `exportMapImage()`'s
|
||
"what I'm currently looking at, just bigger"). Key **`Shift+F11`** (also a toolbar "Export Atlas"
|
||
button under "Export Map") renders at **~8K** (7680 px wide) and always labels **every** named
|
||
geography feature (continent/ocean/sea/island/mountain range/peak/river/lake, ignoring the on-screen
|
||
minor-feature/zoom filter) **and every living settlement** (village/town/city, reading `Settlement.name`
|
||
— `exportMapImage()`/the on-screen map never draw settlement name text, only tier-coloured dot
|
||
markers) — with **guaranteed non-overlapping text**. Two things make that guarantee possible: (1)
|
||
label **font size is deliberately capped well below the resolution scale** (a fixed `labelScale=2.2`,
|
||
not the `~9×` `exportMapImage()` would use at this resolution) — scaling text 1:1 with an ever-bigger
|
||
canvas would just reproduce the same on-screen crowding at a bigger size, so keeping labels a modest
|
||
fixed size relative to the huge canvas is what actually buys the decluttering pass room to work; (2) a
|
||
**greedy priority placement pass**: every candidate label (continents/oceans first, then cities, then
|
||
seas/mountain ranges, towns, islands, villages, then peaks/rivers/lakes last, ties broken by feature
|
||
`size`/settlement population) computes its screen-space text box and is **drawn only if that box
|
||
doesn't collide with one already placed**, else it's silently dropped — so in a dense settlement
|
||
cluster only as many names as legibly fit are shown (major ones win), never overlapping glyphs. Reuses
|
||
`drawMapOverlays(..., drawLabels=false)` for base terrain/borders/rivers/settlement markers (forcing
|
||
`showSettlements` on for the call, since marking cities is the point) — a new `drawLabels` parameter on
|
||
`drawMapOverlays()` lets it suppress the plate-ID and place-name text `exportMapImage()`/the on-screen
|
||
map draw, since this function does its own decluttered pass instead. Uses the plain colour-mode
|
||
`vcolors`, **not** `displayColors()` — in Live World the latter is `shadedColors` (day/night-dimmed),
|
||
which would leave an arbitrary half of a reference map darkened depending on the moment it was
|
||
exported; a gazetteer should read the same everywhere. Saves a timestamped
|
||
`atlas_export_YYYY-MM-DD_HHMMSS.png`; the status line reports how many labels made it in. 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<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 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> 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_<timestamp>.png` (also a toolbar button) · `Shift+F11` export a labeled reference atlas
|
||
(~8K, every geography feature + settlement name, no overlapping text — 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<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); **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.
|