Seasons (obliquity): per-cell summer/winter temps + richer cold biomes

axialTilt was visual-only ("groundwork for seasons"). It now drives climate.

computeClimate() adds derived sTempSummer/sTempWinter around the annual mean:
  summer/winter = sTemp +/- A,  A = seasonAmpMax * sin(tilt)/sin(23.44) *
                                    latShape * continentality
- tiltFactor: 0 tilt -> no seasons, Earth tilt -> 1.
- latShape (pow(|lat|/90, seasonLatExp)): poles swing most.
- continentality: a multi-source BFS ring-distance from ocean cells -- oceans
  and coasts are muted by thermal inertia, interiors swing most.
Result: ~0 swing at the equatorial coast, large at high-latitude interiors.

classifyBiomes() blends WINTER temp into the Tundra/Taiga cold cutoffs via
biomeSeasonWeight (0 = annual-mean only = unchanged biomes; default 0.6), so
cold-winter continental interiors become boreal/tundra (Siberia effect). The
amplitude is geographically shaped, so cold biomes expand only where seasons
bite. Fields are derived/not-saved -> no save-format change.

Viewer: color key 6 now CYCLES Temperature -> summer -> winter -> seasonality
(new seasonColor ramp + labels); cell-info shows summer/winter. New season* +
biomeSeasonWeight config knobs (planet.cfg, validated). Docs updated.

Headless (test/season): summer >= mean >= winter; 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. test_logic + test_biota pass; full
app builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonas Reith 2026-06-27 23:25:45 +02:00
parent 708b23d774
commit 8ed9ae4515
14 changed files with 150 additions and 14 deletions

View File

@ -140,6 +140,15 @@ Biomes (PlanetConfig, Phase 3): per-cell biome classification thresholds. Temper
biomeGrassMoist 0.50 moisture below this -> Grassland/Savanna, else Forest
biomeTaigaMoist 0.40 cool + above this -> Taiga (else Tundra)
biomeLakeMinDepth 20 m filled-basin depth above sea level counting as a Lake
biomeSeasonWeight 0.6 winter temp weight in the Tundra/Taiga cutoffs (0 = annual mean only)
Seasons (PlanetConfig): axialTilt (above) drives per-cell summer/winter temps; color key 6
cycles temperature -> summer -> winter -> seasonality.
seasonAmpMax 18 C max seasonal half-range at full tilt/latitude/interior
seasonLatExp 1.2 latitude shape exponent (>1 pushes swing toward the poles)
seasonOceanFactor 0.15 ocean/coast seasonal-swing floor (thermal inertia)
seasonContinentRings 6 ocean-distance rings to reach full continentality (1 ~ 223 km)
Climate (PlanetConfig, Phase 3): temperature uses the biome* temp params above;
precipitation advects ocean moisture along zonal winds (windward rain, leeward rain

View File

@ -47,7 +47,10 @@ dynamic weather and life.
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.
Other follow-ups: feed precipitation into hydrology rainfall; seasons (obliquity).
- **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
@ -252,6 +255,20 @@ Working and verified (logic tested headless):
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
@ -409,7 +426,8 @@ 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; active mode shown top-center of the globe) ·
(`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) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) ·
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
@ -493,6 +511,12 @@ triangles (plates are fixed in phase 1).
`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.
- 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`/

View File

@ -92,7 +92,13 @@ get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seede
`computeClimate()` builds two derived per-cell fields:
- **Temperature** (°C) = latitude curve (`biomeEquatorTemp/PoleDrop/LatExp`, super-linear so
cold concentrates at poles) `biomeElevLapse` × elevation.
cold concentrates at poles) `biomeElevLapse` × elevation. This is the **annual mean**; the
**Seasons** pass adds derived `sTempSummer`/`sTempWinter` = mean ± `A`, where the seasonal
half-amplitude `A = seasonAmpMax · sin(axialTilt)/sin(23.44°) · latShape · continentality`.
Continentality is a multi-source-BFS ring distance from ocean cells (oceans/coasts muted by
thermal inertia; interiors swing most). `classifyBiomes()` blends winter temp into the
Tundra/Taiga cold cutoffs via `biomeSeasonWeight` (0 = mean only → unchanged biomes), so
cold-winter continental interiors turn boreal/tundra. Seasonal fields are derived/not-saved.
- **Precipitation**: zonal prevailing winds (easterly tropics/poles, westerly mid-lat); ocean
cells are a moisture source; each land cell takes its **upwind** neighbour's moisture, rains
out more on windward upslopes (orographic), loses a multiplicative fraction per cell

View File

@ -91,6 +91,9 @@ const char* colorModeName(ColorMode m) {
case ColorMode::FloraDensity: return "Flora density";
case ColorMode::FaunaDensity: return "Fauna density";
case ColorMode::FungaDensity: return "Funga density";
case ColorMode::TempSummer: return "Temperature (summer)";
case ColorMode::TempWinter: return "Temperature (winter)";
case ColorMode::Seasonality: return "Seasonality (summer-winter)";
}
return "?";
}
@ -138,6 +141,14 @@ Color tempColor(double celsius) {
return Color{ L(0), L(1), L(2), 255 };
}
// Seasonality ramp: summer-winter range in deg C, ~[0, 45]: calm grey -> warm orange.
Color seasonColor(double rangeC) {
static const unsigned char lo[3] = { 95, 100, 110 }, hi[3] = { 235, 130, 40 };
double t = std::clamp(rangeC / 45.0, 0.0, 1.0);
auto L = [&](int c){ return (unsigned char)(lo[c] + (hi[c] - lo[c]) * t); };
return Color{ L(0), L(1), L(2), 255 };
}
// Precipitation ramp over normalized [0,1]: tan (dry) -> green -> teal/blue (wet).
Color precipColor(double moist01) {
double t = std::clamp(moist01, 0.0, 1.0);

View File

@ -5,7 +5,8 @@
// Cell color mapping for the viewer. Pure functions of cell properties.
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
FloraDensity, FaunaDensity, FungaDensity };
FloraDensity, FaunaDensity, FungaDensity,
TempSummer, TempWinter, Seasonality }; // 6 cycles these temp sub-views
Color elevationColor(double e, double seaLevel);
Color plateColor(int id);
@ -22,6 +23,8 @@ const char* colorModeName(ColorMode m);
// (tan dry -> green -> blue wet).
Color tempColor(double celsius);
Color precipColor(double moist01);
// Seasonality: summer-winter temperature range in deg C (grey calm -> orange extreme).
Color seasonColor(double rangeC);
// Biota density ramps (0..1): flora barren->lush green, fauna pale->amber/red,
// funga pale->violet/brown.
Color floraColor(double d01);

View File

@ -50,6 +50,9 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
if (sized(p.temperature()) && sized(p.moisture()))
L.push_back(std::string(TextFormat("temp %.1f C precip %.0f%%",
p.temperature()[i], p.moisture()[i] * 100.0)));
if (sized(p.summerTemp()) && sized(p.winterTemp()))
L.push_back(std::string(TextFormat(" summer %.0f C / winter %.0f C",
p.summerTemp()[i], p.winterTemp()[i])));
L.push_back(std::string(TextFormat("geoAge %.0f My neighbors %d", age, (int)c.neighbors.size())));
// Hydrology (derived; present once routeFlow()/hydrology() has run).
if (sized(p.discharge()) && p.discharge()[i] > p.cfg.riverThreshold)

View File

@ -96,6 +96,8 @@ void Viewer::selectCell(int idx) {
void Viewer::recolor() {
double maxAge = 1.0; for (const auto& c : planet.cells) maxAge = std::max(maxAge, c.geoAge);
const std::vector<double>& temp = planet.temperature();
const std::vector<double>& summer = planet.summerTemp();
const std::vector<double>& winter = planet.winterTemp();
const std::vector<double>& moist = planet.moisture(); // 0..1, already robustly normalized
const std::vector<double>& flora = planet.floraDensity();
const std::vector<double>& fauna = planet.faunaDensity();
@ -114,6 +116,10 @@ void Viewer::recolor() {
case ColorMode::Crust: vcolors[i] = crustColor(planet.cells[i].oceanic); break;
case ColorMode::Biome: vcolors[i] = biomeColor(planet.cells[i].biome); break;
case ColorMode::Temperature: vcolors[i] = temp.empty() ? Color{90,90,90,255} : tempColor(temp[i]); break;
case ColorMode::TempSummer: vcolors[i] = summer.empty()? Color{90,90,90,255} : tempColor(summer[i]); break;
case ColorMode::TempWinter: vcolors[i] = winter.empty()? Color{90,90,90,255} : tempColor(winter[i]); break;
case ColorMode::Seasonality: vcolors[i] = (summer.empty()||winter.empty()) ? Color{90,90,90,255}
: seasonColor(summer[i] - winter[i]); break;
case ColorMode::Precip: vcolors[i] = moist.empty() ? Color{90,90,90,255} : precipColor(moist[i]); break;
case ColorMode::FloraDensity: vcolors[i] = flora.empty() ? Color{90,90,90,255} : floraColor(flora[i]); break;
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255} : faunaColor(fauna[i]); break;

View File

@ -102,7 +102,13 @@ void Viewer::handleInput() {
if (IsKeyPressed(KEY_THREE)) { mode = ColorMode::Age; recolor(); }
if (IsKeyPressed(KEY_FOUR)) { mode = ColorMode::Crust; recolor(); }
if (IsKeyPressed(KEY_FIVE)) { mode = ColorMode::Biome; recolor(); }
if (IsKeyPressed(KEY_SIX)) { mode = ColorMode::Temperature; recolor(); }
if (IsKeyPressed(KEY_SIX)) { // cycle temperature sub-views: mean->summer->winter->seasonality
mode = (mode == ColorMode::Temperature) ? ColorMode::TempSummer
: (mode == ColorMode::TempSummer) ? ColorMode::TempWinter
: (mode == ColorMode::TempWinter) ? ColorMode::Seasonality
: ColorMode::Temperature;
recolor();
}
if (IsKeyPressed(KEY_SEVEN)) { mode = ColorMode::Precip; recolor(); }
if (IsKeyPressed(KEY_EIGHT)) { mode = ColorMode::FloraDensity; recolor(); }
if (IsKeyPressed(KEY_NINE)) { mode = ColorMode::FaunaDensity; recolor(); }

View File

@ -179,7 +179,7 @@ void Viewer::renderHUD() {
}
y += 8;
line("hover: cell info | click tile: open detail panel | C close");
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp 7 precip 8 flora 9 fauna 0 funga");
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga (*6 cycles mean/summer/winter/season)");
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s]",
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off"));
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | R reseed | +/-",

View File

@ -51,9 +51,11 @@ public:
// rain, leeward rain shadow, dry continental interiors). Derived (not saved);
// call before classifyBiomes(), which consumes these fields.
void computeClimate();
const std::vector<double>& temperature() const { return sTemp; } // deg C
const std::vector<double>& temperature() const { return sTemp; } // deg C, annual mean
const std::vector<double>& precipitation() const { return sPrecip; } // relative units
const std::vector<double>& moisture() const { return sMoist; } // 0..1 (median land -> 0.5)
const std::vector<double>& summerTemp() const { return sTempSummer; } // deg C, warmest month
const std::vector<double>& winterTemp() const { return sTempWinter; } // deg C, coldest month
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
// fields (temperature + normalized precipitation). Derived + written back into
@ -152,8 +154,9 @@ private:
std::vector<int> sFlowTo, sHydroOrder;
// Phase-3 climate scratch (derived each computeClimate(); not saved). sMoist is the
// 0..1-normalized precipitation the biome classifier reads.
std::vector<double> sTemp, sPrecip, sMoist;
// 0..1-normalized precipitation the biome classifier reads. sTempSummer/sTempWinter are
// the obliquity-driven seasonal extremes around the annual mean sTemp (see Seasons).
std::vector<double> sTemp, sPrecip, sMoist, sTempSummer, sTempWinter;
std::vector<Vec3> sWind;
std::vector<int> sUpwind;

View File

@ -22,11 +22,18 @@ void Planet::classifyBiomes() {
const double GRASS_MOIST = cfg.biomeGrassMoist, TAIGA_MOIST = cfg.biomeTaigaMoist;
const double LAKE_MIN_DEPTH = cfg.biomeLakeMinDepth;
const bool haveLake = !sLakeDepth.empty();
// Seasons: blend winter temperature into the cold (Tundra/Taiga) cutoffs so cold-winter
// continental interiors turn boreal/tundra. The seasonal amplitude is itself geographically
// shaped (large only at high-latitude interiors), so this expands cold biomes where seasons
// bite, not uniformly. biomeSeasonWeight = 0 -> coldT == annual mean -> biomes unchanged.
const double SEASON_W = cfg.biomeSeasonWeight;
const bool haveSeason = ((int)sTempWinter.size() == n);
for (int i = 0; i < n; ++i) {
const Cell& c = cells[i];
double elevAbove = std::max(0.0, c.elevation - sea);
double temp = sTemp[i]; // climate temperature (deg C)
double temp = sTemp[i]; // climate temperature (deg C, annual mean)
double coldT = haveSeason ? temp + SEASON_W * (sTempWinter[i] - temp) : temp; // winter-blended
double moist = sMoist[i]; // climate precipitation, normalized 0..1
double lakeD = haveLake ? sLakeDepth[i] : 0.0;
bool adjOcean = false, adjWater = false;
@ -43,9 +50,9 @@ void Planet::classifyBiomes() {
else if (elevAbove > MOUNTAIN_ELEV) b = Biome::Mountains;
else if (elevAbove > HILLS_ELEV) b = Biome::Hills;
else { // lowland / plains
if (temp < TUNDRA_TEMP) b = Biome::Tundra;
if (coldT < TUNDRA_TEMP) b = Biome::Tundra;
else if (elevAbove < LOWLAND_ELEV && moist > WETLAND_MOIST && adjWater) b = Biome::Wetland; // swamps hug water
else if (temp < TAIGA_TEMP) b = (moist > TAIGA_MOIST) ? Biome::Taiga : Biome::Tundra;
else if (coldT < TAIGA_TEMP) b = (moist > TAIGA_MOIST) ? Biome::Taiga : Biome::Tundra;
else if (moist < DESERT_MOIST) b = Biome::Desert;
else if (moist < GRASS_MOIST) b = (temp > SAVANNA_TEMP) ? Biome::Savanna : Biome::Grassland;
else b = Biome::Forest;

View File

@ -26,6 +26,8 @@ void Planet::computeClimate() {
sTemp.assign(n, 0.0);
sPrecip.assign(n, 0.0);
sMoist.assign(n, 0.0);
sTempSummer.assign(n, 0.0);
sTempWinter.assign(n, 0.0);
sWind.assign(n, Vec3{0, 0, 0});
sUpwind.assign(n, -1);
@ -131,4 +133,42 @@ void Planet::computeClimate() {
ref = std::max(1e-6, landP[mid] / 0.5); // median -> 0.5
}
for (int i = 0; i < n; ++i) sMoist[i] = std::clamp(sPrecip[i] / ref, 0.0, 1.0);
// --- Seasons (obliquity) ------------------------------------------------------
// Per-cell summer (warmest-month) and winter (coldest-month) temperatures around
// the annual mean sTemp. The seasonal half-amplitude grows with axial tilt, with
// latitude (poles swing most), and with continentality -- distance from the ocean,
// which moderates coastal climates via its thermal inertia. These are STATIC fields
// (the extremes), not an animated year, so hemisphere phase is irrelevant.
{
// Continentality: multi-source BFS ring distance from ocean cells over the fixed
// neighbour graph. Ring distance is order-independent -> deterministic, O(n).
const int rings = std::max(1, cfg.seasonContinentRings);
std::vector<int> dist(n, -1), frontier, next;
for (int i = 0; i < n; ++i)
if (cells[i].elevation <= sea) { dist[i] = 0; frontier.push_back(i); }
for (int r = 1; r <= rings && !frontier.empty(); ++r) {
next.clear();
for (int i : frontier)
for (int nb : cells[i].neighbors)
if (dist[nb] < 0) { dist[nb] = r; next.push_back(nb); }
frontier.swap(next);
}
const double tiltFactor = std::sin(cfg.axialTilt * M_PI / 180.0) /
std::sin(23.44 * M_PI / 180.0); // 0 tilt -> 0, Earth -> 1
const double ampMax = cfg.seasonAmpMax, latExp = cfg.seasonLatExp;
const double oceanF = cfg.seasonOceanFactor;
for (int i = 0; i < n; ++i) {
double cont;
if (cells[i].elevation <= sea) cont = oceanF; // ocean: muted swing
else if (dist[i] < 0) cont = 1.0; // deep interior (unreached)
else cont = (double)dist[i] / rings;// coast 0 -> interior 1
cont = std::clamp(std::max(oceanF, cont), 0.0, 1.0);
double lat = std::asin(std::clamp(cells[i].unit.y, -1.0, 1.0));
double latShape = std::pow(std::fabs(lat) / (M_PI / 2.0), latExp);
double A = ampMax * std::max(0.0, tiltFactor) * latShape * cont;
sTempSummer[i] = sTemp[i] + A;
sTempWinter[i] = sTemp[i] - A;
}
}
}

View File

@ -27,15 +27,16 @@
D(biomeIceTemp) D(biomeTundraTemp) D(biomeTaigaTemp) D(biomeSavannaTemp) \
D(biomeMountainElev) D(biomeHillsElev) D(biomeBeachBand) D(biomeLowlandElev) \
D(biomeWetlandMoist) D(biomeDesertMoist) D(biomeGrassMoist) D(biomeTaigaMoist) \
D(biomeLakeMinDepth) \
D(biomeLakeMinDepth) D(biomeSeasonWeight) \
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
D(climateOroRefHeight) D(climateContinentality) \
D(seasonAmpMax) D(seasonLatExp) D(seasonOceanFactor) \
D(bioVegTempMin) D(bioVegTempOpt) D(bioVegMoistRef) D(bioFaunaProductivity) \
D(bioCarnPreyMin) D(bioCarnScale) D(bioFungaMoistRef) D(bioFungaFloraWeight) \
D(bioFungaTempMin) D(bioRegionBonus) \
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
I(climateWindPasses) I(climateMoistureSmooth) \
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) \
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) \
U(seed)
@ -162,11 +163,15 @@ std::string validateConfig(const PlanetConfig& cfg) {
E(rng(cfg.biomeGrassMoist, 0.0, 1.0, "biomeGrassMoist"));
E(rng(cfg.biomeTaigaMoist, 0.0, 1.0, "biomeTaigaMoist"));
E(rng(cfg.biomeLakeMinDepth, 0.0, 5000.0, "biomeLakeMinDepth"));
E(rng(cfg.biomeSeasonWeight, 0.0, 1.0, "biomeSeasonWeight"));
E(rng(cfg.climateOceanMoisture, 0.0, 1.0e3, "climateOceanMoisture"));
E(rng(cfg.climateRainEfficiency, 0.0, 1.0, "climateRainEfficiency"));
E(rng(cfg.climateOrographic, 0.0, 50.0, "climateOrographic"));
E(rng(cfg.climateOroRefHeight, 1.0, 1.0e5, "climateOroRefHeight"));
E(rng(cfg.climateContinentality, 0.0, 1.0, "climateContinentality"));
E(rng(cfg.seasonAmpMax, 0.0, 60.0, "seasonAmpMax"));
E(rng(cfg.seasonLatExp, 0.1, 6.0, "seasonLatExp"));
E(rng(cfg.seasonOceanFactor, 0.0, 1.0, "seasonOceanFactor"));
E(rng(cfg.bioVegTempMin, -40.0, 30.0, "bioVegTempMin"));
E(rng(cfg.bioVegTempOpt, -20.0, 50.0, "bioVegTempOpt"));
E(rng(cfg.bioVegMoistRef, 0.01, 1.0, "bioVegMoistRef"));
@ -188,6 +193,7 @@ std::string validateConfig(const PlanetConfig& cfg) {
E(irng(cfg.seaLevelEvery, 1, 100000, "seaLevelEvery"));
E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses"));
E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth"));
E(irng(cfg.seasonContinentRings, 1, 100, "seasonContinentRings"));
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));

View File

@ -186,6 +186,8 @@ struct PlanetConfig {
double biomeGrassMoist = 0.50; // moisture below this -> Grassland/Savanna, else Forest
double biomeTaigaMoist = 0.40; // cool + above this -> Taiga (else Tundra)
double biomeLakeMinDepth= 20.0; // filled-basin depth above sea level counting as a Lake
double biomeSeasonWeight= 0.6; // how much winter temp (vs annual mean) sets the cold
// Tundra/Taiga cutoffs (0 = mean only/old behaviour, 1 = winter)
// --- Phase 3: climate (orographic precipitation) -- see PlanetClimate.cpp --
// Temperature reuses the biome* temperature fields above. Precipitation advects
@ -199,6 +201,16 @@ struct PlanetConfig {
int climateWindPasses = 50; // moisture-advection iterations (steady state)
int climateMoistureSmooth = 12; // precipitation diffusion passes (wet/dry transition zones)
// --- Seasons (obliquity) -- see PlanetClimate.cpp -----------------------
// axialTilt (above) drives a per-cell seasonal temperature range around the annual
// mean sTemp: summer/winter = mean +/- A, with A = seasonAmpMax * tiltFactor *
// latShape * continentality. Big swings at high-latitude continental interiors,
// small near coasts/equator. Static fields (warmest/coldest month), not animated.
double seasonAmpMax = 18.0; // max seasonal half-amplitude (C) at full tilt/lat/interior
double seasonLatExp = 1.2; // latitude shape exponent (>1 concentrates swing toward poles)
double seasonOceanFactor = 0.15; // continentality floor: ocean/coast seasonal swing fraction
int seasonContinentRings = 6; // ocean-distance rings to reach full continentality (1 = ~223 km)
// --- Biota: flora / fauna / funga (see PlanetBiota.cpp + *Gen.cpp) -------
// Density scalars (derived each tick) drive the colour views; the discrete
// slot/point population (generated on demand, saved) draws archetypes by size.