A follow-up to the dominance-schism trigger: each row now shows the culture's share of the world's living population, computed the same way cultDominanceShare reads it, so a culture nearing the split threshold is visible before it actually splits. A culture at/past the threshold draws in an orange warning tint. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
1589 lines
136 KiB
Markdown
1589 lines
136 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.
|
||
- **Bugfix: Territory/Culture/Wealth (`P`/`X`/`Z`) toggles didn't stick, and their border/route
|
||
overlays never turned off** — two independent bugs, both in `src/render`, found by reading the
|
||
actual raygui source rather than trusting live clicks (this session's X11 input became severely
|
||
delayed/queued after enough interaction; see the `gui-verification-xdotool` memory). (1) The
|
||
toolbar's view-mode `GuiDropdownBox` sync code re-derived `v.mode` from `toolbarModeActive` **every
|
||
frame**, not just on a real pick — `Territory`/`Culture`/`Wealth`/`Ecoregion`/`Habitability` aren't
|
||
in the dropdown's 13-entry list, so whenever `mode` was one of those, the code found no match, left
|
||
`toolbarModeActive` at its last plain-mode index, and then unconditionally forced `v.mode` back to
|
||
that stale plain mode and called `recolor()` — reverting the just-toggled view **within the same
|
||
frame**, independent of pause state (`drawToolbar()` runs every render frame regardless). Fixed by
|
||
only applying the picked mode when `toolbarModeActive` actually changed as a result of *this* call
|
||
(before/after compare, confirmed against raygui's `GuiDropdownBox` source: it only writes `*active`
|
||
on a genuine item click). (2) `showNationBorders`/`showCultureBorders`/`showTradeRoutes` were
|
||
separate `bool` fields set `true` only inside their own P/X/Z toggle method and never reset when
|
||
switching away by any *other* means (a number key, the dropdown, another civ toggle) — so realm
|
||
borders / war fronts / alliance arcs / trade routes, once ever turned on, stayed drawn over every
|
||
subsequent view forever. Fixed by making all three **derived** from `mode` (`showNationBorders()`
|
||
etc., now zero-arg methods on `Viewer`, e.g. `mode == ColorMode::Territory`) instead of stored,
|
||
independently-mutable state — the two can no longer drift apart because there's only one source of
|
||
truth. `src/render` only, no save-format or `src/sim` change; ctest unaffected (`src/sim` untouched).
|
||
- **Realm names in the 2D map/exports + a diplomacy-arc toggle independent of Territory** *(done)* —
|
||
two follow-up requests after the above fix. (1) Kingdom/empire capital labels ("Kingdom of X") were
|
||
3D-globe-only (`renderFrame()`'s "3D realm labels" block); neither the on-screen 2D map nor
|
||
`exportMapImage()`/`exportAtlasImage()` ever drew them. `drawMapOverlays()` now has a matching 2D
|
||
block (same `showNationBorders() && drawLabels` gating, `projLonLat` instead of the 3D camera
|
||
projection; city-states skipped same as the 3D version) — fixes the on-screen map and
|
||
`exportMapImage()` in one place. `exportAtlasImage()` separately adds nation names as label
|
||
candidates in its decluttered pass (priority tier alongside cities; skipped for city-states), offset
|
||
above the capital's own settlement-name anchor so the two don't fight over one box; shown regardless
|
||
of the current colour mode, consistent with the atlas being a reference rather than "what you're
|
||
looking at". (2) `showNationBorders()` (Territory view) used to unconditionally show the political
|
||
borders **and** the Step-6 alliance/rivalry arcs together — no way to see realm borders without the
|
||
diplomacy lines. Added an independent `showDiplomacy` bool (default on, key **`A`**, a toolbar
|
||
"Alliances" checkbox in Overlays), `&&`-ed into just the ally/rival arc draw calls (3D + 2D) so
|
||
political borders/war fronts still show under Territory regardless — Territory now toggles cleanly
|
||
with or without the arcs. `src/render` only, no save-format change.
|
||
- **Realm borders + culture annotation over any colour mode** *(done)* — `showNationBorders()`
|
||
(borders/realm-name labels/war fronts/diplomacy arcs) was Territory-mode-only, so there was no way
|
||
to see political borders and realm names layered over e.g. the **Biome** view without switching
|
||
colour modes and losing the terrain-type coloring the user actually wanted to look at. Added an
|
||
independent `showRealms` bool (default off, key **`Q`**, a "Realm borders" toggle in Civilization &
|
||
Ecology — like Territory/Culture/Trade it needs settlement/territory data built first, hence
|
||
shadow+method not a plain checkbox) that's `||`-ed with `showNationBorders()` at just the
|
||
political-border-segment and realm-name-label draw sites (3D, 2D, and `exportMapImage()` which
|
||
shares the 2D path) — `mode`/`vcolors`/`recolor()` are never touched, so this overlay draws lines
|
||
and text *over* whatever colour mode is active without tinting a single cell. War fronts and
|
||
diplomacy arcs deliberately stay `showNationBorders()`-only so the combined view doesn't also drag
|
||
in Territory-only clutter. Realm-name labels everywhere they're drawn (Territory view, this new
|
||
toggle, and the always-on atlas) now also show the realm's dominant culture as a smaller
|
||
`(the Velmar)`-style line stacked directly above the capital marker, via `Nation.cultureId →
|
||
planet.cultureList()` (skipped gracefully when absent). `exportAtlasImage()` forces `showRealms` on
|
||
for the duration of its export (same save/restore pattern already used for `showSettlements`) and
|
||
adds the culture line as its own lower-priority label candidate in the decluttered pass, anchored
|
||
just below each realm's own label so it drops out gracefully on a dense world via the existing
|
||
collision check — no special-casing needed. `AtlasLabel::text` changed from `const std::string*` to
|
||
an owned `std::string` since the culture strings are built on the fly rather than backed by an
|
||
existing stable container. Deliberately **not** reset in `regenWorld()` — like `showDiplomacy`, its
|
||
underlying segment/nation lists are already cleared on reseed, so nothing stale can render; only
|
||
`mode` needs resetting there. `src/render` only, no save-format or `src/sim` change; ctest
|
||
unaffected.
|
||
- **Toolbar: a direct one-click "Biome view" shortcut** *(done)* — during Tectonics/Hydrology (before
|
||
any Civilization & Ecology feature is even relevant) the only way to see the world's actual terrain
|
||
type (deserts/forests/tundra/etc., colour mode `Biome`) was to open the 13-entry View Mode dropdown
|
||
and pick it out, or reach it as an accidental side effect of toggling `Ecoregions (E)` off (its
|
||
toggle lands on `Biome` specifically). Added a **"Biome view (5)"** toggle-styled button directly
|
||
under the dropdown — outside the scrollable body, so it's visible immediately without scrolling —
|
||
that just sets `mode = ColorMode::Biome` (matching the plain `5` key, no side effects, works from
|
||
the very start of World Creation). Purely additive; the dropdown entry and `5` key are unchanged.
|
||
`src/render` only (`Toolbar.cpp`), no save-format change.
|
||
- **Independent realm names, a capital-label collision fix, colony/culture gating, and an explicit
|
||
"cultureless" state** *(done)* — a cluster of follow-ups after the realm-borders overlay above.
|
||
(1) **Atlas capital labels were colliding**: a capital's own settlement-name label was
|
||
systematically lost to the (higher-priority) realm-name/culture block at the same point, since
|
||
their offsets weren't real clearance, just a small nudge — fixed with an explicit bottom-up stack
|
||
(settlement's own box → culture line → realm name, each strictly above the one below it with a
|
||
real gap, sized off a conservative worst-case settlement font) so a capital's own name always
|
||
survives the decluttering pass alongside its realm's. (2) **Realm names are now independently
|
||
generated**, not literally "Kingdom of " + the capital's own name — `computeCultures()`
|
||
(`PlanetCulture.cpp`) derives a separate "place name" via `namegen::makeName()`, keyed on the
|
||
capital *settlement's* permanent id (stable across yearly recomputes, unlike `Nation.id` which is
|
||
just that pass's insertion order) and its own language bank, deduped against other realms named
|
||
the same pass — same retry pattern already used for settlement/colony naming. A kingdom and the
|
||
city it's ruled from now read as two distinct places. (3) **`stepColonization()` now requires
|
||
`nat.cultureId >= 0`** — a culture-less realm has nothing to pass on, and this stops it from
|
||
propagating that gap to new colonies (colonies inherit their founder's culture at founding).
|
||
(4) **Realm labels always show a culture line now** (3D, 2D, atlas) — either the real
|
||
`(the Velmar)` or an explicit `(cultureless)` in a muted colour, instead of silently omitting the
|
||
line when `Nation.cultureId` is unset (which can still arise from older conquest/schism edge cases
|
||
predating fix 3) — makes it a readable, intentional-looking state rather than a gap that looks like
|
||
a bug. `test_culture.cpp` updated to rebuild the new deterministic name formula and assert it
|
||
differs from the capital's own name. Touches `src/sim` (`PlanetCulture.cpp`, `PlanetCiv.cpp`) as
|
||
well as `src/render` — `Nation.name`/`cultureId` are derived/not saved (per the Territory & Culture
|
||
entries above), so no save-format version bump.
|
||
- **Multiple cultures per continent (was always exactly one)** *(done — see `Planet::seedCultures()`,
|
||
`src/sim/PlanetCulture.cpp`)* — a continent seeding only ever one culture at the dawn, combined with
|
||
the existing border-conversion/backfill dynamics, eroded into one super-dominant culture covering
|
||
the whole world over a long run. `seedCultures()` now splits each continent into
|
||
**1..`cultDawnMaxPerContinent`** initial peoples via deterministic farthest-point seeding (each
|
||
settlement joins its nearest seed point — geographically coherent, Voronoi-like regions, not a
|
||
checkerboard); the target count scales with the continent's settlement population via
|
||
`cultDawnSettlementsPerCulture` (both new `planet.cfg` knobs). Per-culture ethos/faith/name hashes
|
||
now fold in `Culture.id` so same-continent cultures (sharing `regionId`/`bank`) get distinct
|
||
identities instead of colliding. This surfaced a real, previously-latent **ordering bug**:
|
||
`computeTerritory()` (realm/vassalage grouping, which prefers matching by actual culture and only
|
||
falls back to same-continent when culture data doesn't exist yet) ran *before*
|
||
`computeCultures()` populated per-settlement culture — harmless when one culture ≡ one continent,
|
||
but would leave temporarily multi-cultural realms now. Fixed at the root: `computeCultures()` split
|
||
into `refreshSettlementCultures()` (the settlement-level seed/backfill/tally slice, no dependency on
|
||
`nations`) which `computeTerritory()` now calls on itself before grouping — every caller (the viewer
|
||
and the dozens of existing test call sites) gets correct behaviour automatically. `test_culture.cpp`/
|
||
`test_cultevo.cpp` updated to assert the new bounded invariant (a continent has 1..N cultures; a
|
||
single culture never itself spans two continents — still enforced) instead of exact equality.
|
||
Verified live: one continent now shows six distinct peoples instead of a single dominant culture,
|
||
with realms still correctly mono-cultural. `src/sim` + `src/render`; cultures are derived/not saved,
|
||
so no save-format version bump.
|
||
- **Main Menu + a proper save browser (was: always auto-generate, one fixed save slot)** *(done — see
|
||
`src/render/MainMenu.{hpp,cpp}`, `Viewer::startNewWorld()`, save v25)* — the app used to always
|
||
generate a world straight from `planet.cfg` on launch, and `F5` always overwrote the same
|
||
`planet.save`. Now it opens on a full-screen **Main Menu** (`AppScreen::MainMenu`, a complete early
|
||
return in `renderFrame()`/`handleInput()` — not a dim overlay like the Phase-3 modal, since the menu
|
||
must render before `planet.generate()` has ever run): a **Home** tab (New World / Back to World —
|
||
the latter only once a world exists — a prominent Seed field + Randomize, and status text), a
|
||
**Settings** tab editing **every one of the ~240 `PlanetConfig` fields**, and a **Load World** tab
|
||
listing every `*.save` file. Reachable again later via a toolbar "Main Menu" button (World/Time
|
||
section) without disturbing a running world — "Back to World" is a pure screen-flag flip. **Saves
|
||
are now timestamped**: `F5`/"Save" always writes a fresh `world_<timestamp>.save`
|
||
(`makeTimestampedSavePath()`, the same `strftime` idiom as the map/atlas export); `F9`/"Load" picks
|
||
the newest via `listSaveFiles()` (`LoadDirectoryFilesEx(".", ".save", false)`); the fixed
|
||
`SAVE_PATH` member is gone. **A generic settings editor drives all 240 fields with no per-field
|
||
code**: `Planet::configFieldTable()` (`src/sim/PlanetIO.cpp`) reuses the existing
|
||
`CONFIG_FIELDS(D, I, U)` X-macro a third time to build a runtime `vector<ConfigFieldDesc>` (name +
|
||
category + type-erased get/set lambdas) — the same "add a field, everything downstream keeps
|
||
working" property `writeConfigFields()`/`parseConfigStream()` already give `planet.cfg`/saves.
|
||
Fields are grouped into **25 categories** via an explicit `name → category` lookup table
|
||
(`kConfigFieldCategory`, hand-verified against `PlanetConfig`'s actual declaration order in
|
||
`PlanetTypes.hpp`) — **not** a "first field of each section" boundary scan over
|
||
`configFieldTable()`'s own iteration order, which was tried first and is a real trap: `CONFIG_FIELDS`
|
||
expands to all `D()` fields, then all `I()` fields, then `U(seed)`, so a category whose fields mix
|
||
types (nearly all of them — e.g. "Plate lifecycle" has 9 doubles early and 5 ints in the separate
|
||
int run) is **not contiguous** in that order; a boundary scan silently merged everything after the
|
||
first non-contiguous category into one bucket. `configCategoryList()` returns the 25 labels in
|
||
display order, derived from `kConfigFieldCategory`'s own (genuinely contiguous, hand-written) block
|
||
order. The Settings tab is a left category rail + a `GuiScrollPanel` field list (the exact
|
||
scroll/scissor pattern from `Toolbar.cpp`); each row is a generic `drawConfigFieldRow()` modeled on
|
||
Edit Mode's `editRow()` (`Panels.cpp`) minus the lock column, and `handleMainMenuInput()`'s
|
||
click-to-focus/type/Enter-commits/Escape-cancels loop mirrors Edit Mode's typed-numeric-entry
|
||
handling (`ViewerInput.cpp`) exactly — click a field, type a new value, Enter calls
|
||
`Viewer::commitMenuField()` (a `std::stod` in a `try/catch`, silently discarding on a bad number,
|
||
same as `commitEditField()`). No live bounds-checking while typing: `validateConfig()` only runs at
|
||
"New World" time (same as the existing `F2` behaviour), reverting to defaults on failure. **Save v25**
|
||
appends the viewer's **colour mode + all overlay toggles** (`showBorders`/`showDrift`/.../
|
||
`showRealms`, 13 bits) so a loaded world shows what it looked like when saved, not just its
|
||
simulation state — deliberately kept **out of** `Planet::writeState`/`readState`'s 15-`bool hasX`
|
||
signature (same idiom already used there for `settled`/`phase3`/`liveWorld`/`liveRate`) so this
|
||
purely-viewer-side addition touches zero of the ~18 `test_*.cpp` files that call `readState`
|
||
directly; instead it's written/read directly in `Viewer::saveGame()`/`loadGame()`, positioned after
|
||
the existing event-journal block (a live bug caught the position mismatch: the read was first
|
||
wired in right after the header fields while the write happens at the very end of the file, which
|
||
silently desynced every read after it and broke the *unrelated* `test_events` suite too — moving
|
||
the read to match the write's actual position fixed both). Camera angle, map zoom/pan, selected
|
||
cell and the Live-info tab stay session-local, matching the user's explicit scope call. New headless
|
||
`test_viewerstate.cpp` (modeled on `test_events.cpp`): the v25 round-trip, plus a version-patched
|
||
negative test confirming a pre-v25 save loads without crashing and leaves mode/overlays untouched.
|
||
`src/sim` (`Planet.hpp`, `PlanetIO.cpp`) + `src/render` (`Viewer.{hpp,cpp}`, `ViewerInput.cpp`,
|
||
`ViewerRender.cpp`, `Toolbar.cpp`, new `MainMenu.{hpp,cpp}`); `.gitignore`'s `planet.save` line
|
||
became `*.save`.
|
||
- **Bugfix: sea level could sink without limit on a long run (`seaLevel` at -2900 m after ~1340 My,
|
||
"land" cells sitting at -2000 m next to "ocean" at -4000 m)** *(done — see `Planet::adjustSeaLevel()`,
|
||
`src/sim/PlanetErosion.cpp`)* — reported after a real long-running world. Root cause: two
|
||
independent notions of "land" (documented since Phase 2 inc. 2, see the Architecture section) drift
|
||
apart on a long run. `adjustSeaLevel()`'s **geographic** target (`landFractionTarget`, elevation
|
||
above `seaLevel`) is a fixed 30%, but the **crust-type** land conservation in `advect()`
|
||
(`targetLand`/`landBand`, Phase 2 inc. 1.5) only holds continental crust area near whatever
|
||
Phase-1 forming happened to settle at for that seed — nothing guarantees the two agree (this
|
||
world's crust settled at ~26%). With genuine continental land structurally short of the 30%
|
||
target, `adjustSeaLevel()` had **no lower bound**: every check found the error still outside the
|
||
deadband and kept nudging `seaLevel` down by `seaLevelStep`, misclassifying progressively older,
|
||
deeper **oceanic** crust as "geographic land" to make up the shortfall. Because oceanic crust keeps
|
||
*aging and deepening* (`oceanicBase(age)`, seafloor subsidence) even at a fixed `seaLevel`, cells
|
||
counted as land kept sinking out of range under the controller — a moving target with no floor, so
|
||
it never reached equilibrium and just kept sinking, confirmed on the user's save (`cfg.seaLevel`
|
||
-2900 m, continental crust only 26.2% of cells vs. 30% target, 9.4% of "geographic land" was
|
||
actually aging oceanic crust). Fixed with two new hard bounds, **`seaLevelMin`** (-3000 m) /
|
||
**`seaLevelMax`** (+3000 m): `adjustSeaLevel()` clamps its candidate nudge to this range, so once
|
||
pinned at a bound the clamped candidate equals the current value, the error doesn't improve, and
|
||
the existing "only nudge if it helps" check stops it there — a world that structurally can't reach
|
||
the target settles at a plausible coastal offset with an imperfect land fraction instead of an
|
||
unbounded, physically nonsensical one. New headless `test_sealevel.cpp`: two synthetic worlds
|
||
(crust short of / in excess of the target, continuous depth distributions so every step genuinely
|
||
helps, reproducing the real pathology rather than getting stuck on a synthetic cliff) confirm the
|
||
controller still engages and moves, but never crosses either bound; a third confirms a normal world
|
||
never approaches the bounds. `src/sim` only (`PlanetTypes.hpp`, `PlanetErosion.cpp`, `PlanetIO.cpp`
|
||
for the two new config fields + range checks); config is the self-describing text block (v6+), so
|
||
**no save-version bump**. Note for already-affected saves: the fix stops further sinking but
|
||
doesn't retroactively restore a sunk `seaLevel` on load (config-field edits, whether via
|
||
`planet.cfg`+`F2` or the Main Menu's Settings tab, only take effect through a regenerate — there is
|
||
currently no way to push a single global field like `seaLevel` into an already-running world
|
||
without starting over); the practical fix for an affected world is a New World.
|
||
- **Two more culture-schism triggers: world dominance + war between kin (was: only a distant-cluster
|
||
schism)** *(done — see `Planet::stepCulture()`, `src/sim/PlanetCulture.cpp`)* — reported after a
|
||
long run left the world under a single "world-dominant" culture (the existing schism only fires for
|
||
a geographically distant cluster, e.g. overseas colonies — a single unbroken landmass never
|
||
qualified, so one culture could quietly absorb the whole world via border conversion with nothing
|
||
to check it). Two new independent triggers, both sharing the existing schism machinery (now
|
||
factored into a `doSchism()` lambda so all three read identically): **dominance schism** — once a
|
||
culture exceeds `cultDominanceShare` (80%) of the world's living population, a per-year chance
|
||
rising from `cultDominanceProbMin` (1%) at the threshold to `cultDominanceProbMax` (99%) at 100%
|
||
share that it fractures on its own, **no geography required** — a coin flip picks **half or a
|
||
fifth** of its own members, the ones farthest from its own population-weighted cultural core, to
|
||
found a new people; **war schism** — a war between two realms that happen to share a culture (a
|
||
"civil war") has `cultWarSchismChance` (50%) odds, rolled once right at declaration, that one whole
|
||
side (a coin flip between attacker/defender) renounces the shared identity, its entire realm's
|
||
settlements becoming a new people — checked against wars with `startYear` equal to the current
|
||
year, so it only ever looks at a war on the exact year it was declared. Both are pure hashes of
|
||
(stable id, year, seed) like the rest of Step 8, so step-back replays them exactly. New config
|
||
fields `cultDominanceShare`/`cultDominanceProbMin`/`cultDominanceProbMax`/`cultWarSchismChance`
|
||
(self-describing config, **no save-version bump**). One design pitfall found while adding these:
|
||
an initial `validateConfig()` cross-rule requiring `cultDominanceProbMin <= cultDominanceProbMax`
|
||
was removed again — the interpolation tolerates the reverse fine (the whole mechanic is already
|
||
gated on `cultDominanceProbMax > 0`), and the rule would have rejected the very natural "disable by
|
||
setting Max to 0" pattern (e.g. from the Main Menu's Settings tab) whenever `Min` was left at its
|
||
nonzero default, silently reverting a player's *entire* config to defaults on New World — a bad
|
||
surprise for an edit that looks perfectly reasonable. `test_cultevo.cpp` gained two new scenarios
|
||
(force every settlement onto one culture and watch it split near-deterministically; inject a
|
||
same-culture war and check the split) alongside the existing distant-cluster one.
|
||
- **Cultures tab: show each people's world population share** *(done — see `Viewer::renderLiveInfo()`,
|
||
`src/render/ViewerRender.cpp`)* — a follow-up to the dominance-schism trigger above: each row in the
|
||
Live-info **Culture** tab now shows its share of the world's living population (`"the Velmar (34%)"`),
|
||
computed the same way `cultDominanceShare` is (a culture's `totalPop` over the sum across all
|
||
cultures) so a culture nearing the split threshold is visible before it actually splits; a culture
|
||
at/past `cultDominanceShare` draws in an orange warning tint instead of its usual `cultureColor`.
|
||
`src/render` only, 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)
|
||
MainMenu.* full-screen Main Menu: generic settings editor (all PlanetConfig fields), seed,
|
||
New World / Load World / Back to World (AppScreen::MainMenu vs. World)
|
||
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 + realm-name labels (Realms tab lists nations) ·
|
||
`A` toggle the alliance/rivalry diplomacy arcs on the Territory view (on by default; political
|
||
borders/war fronts stay regardless) ·
|
||
`Q` toggle political realm borders + realm-name (+dominant-culture) labels over **any** colour mode,
|
||
not just Territory (independent of `P`; off by default) ·
|
||
`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 (writes a fresh `world_<timestamp>.save`) · `F9` load
|
||
(the newest save) · a toolbar "Main Menu" button (World/Time section) opens the full-screen **Main
|
||
Menu** — edit any of the ~240 `planet.cfg` fields, set a seed, start a New World or browse/Load
|
||
World — without disturbing a running world (see below) ·
|
||
`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). The app opens on the **Main
|
||
Menu** rather than auto-generating a world; `planet.generate()` runs only once you
|
||
pick New World or Load World there (see the Main Menu entry above).
|
||
|
||
`planet.cfg` (human-editable key=value of every PlanetConfig param, auto-created
|
||
on first run, reload with `F2` or edit any field live in the Main Menu's Settings
|
||
tab) plus **one `world_<timestamp>.save` file per save** in the working dir
|
||
(binary: seed + config + full planet state, written/read by
|
||
`Planet::writeState`/`readState`, resumes deterministically) — `F5`/the toolbar
|
||
"Save" button always write a fresh timestamped file, `F9`/"Load" (or the Main
|
||
Menu's Load World tab) picks/lists them via `listSaveFiles()`. Config is
|
||
range-checked by `validateConfig()` on load/`F2`/New World; an invalid file/settings
|
||
reverts to safe defaults (without overwriting your `planet.cfg`) and shows a status
|
||
message. The save header is versioned (currently **25**; 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), and v25 the viewer's **colour mode + overlay toggles**
|
||
(kept out of `Planet::writeState`/`readState`'s signature — written/read directly in
|
||
`Viewer::saveGame()`/`loadGame()`, so it doesn't touch the ~18 `test_*.cpp` files that
|
||
call `readState` directly);
|
||
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;
|
||
pre-v25 saves load with the colour mode/overlays left at whatever the Viewer already held (same
|
||
"session state survives a load" behaviour saves have always had for anything not in the file).
|
||
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); dominance schism —
|
||
`cultDominanceShare` (0.80, world population share above which it can trigger), `cultDominanceProbMin`
|
||
(0.01, per-year chance right at the threshold) / `cultDominanceProbMax` (0.99, at 100% share — the
|
||
chance ramps linearly between the two); war schism — `cultWarSchismChance` (0.50, chance a war
|
||
between two same-culture realms splits one side, rolled once at declaration). 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. Dawn seeding (`Planet::seedCultures()`, before Step 8 evolution runs at all):
|
||
`cultDawnSettlementsPerCulture` (5, target dawn settlements per initial culture on a continent —
|
||
lower = more initial peoples) and `cultDawnMaxPerContinent` (6, hard cap on how many initial peoples
|
||
one continent can seed) — raise `cultDawnSettlementsPerCulture` or lower `cultDawnMaxPerContinent`
|
||
for a world that starts more culturally uniform (closer to the old always-one-per-continent
|
||
behaviour), lower/raise them the other way for more initial diversity.
|
||
- `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); `seaLevelMin` (-3000 m) /
|
||
`seaLevelMax` (+3000 m) — hard bounds on how far the controller may push
|
||
`seaLevel` (see the runaway-guard bugfix entry below). 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.
|