From 8ed9ae4515cc2966a7d62dec9acd747b0ea28b49 Mon Sep 17 00:00:00 2001 From: Jonas Reith Date: Sat, 27 Jun 2026 23:25:45 +0200 Subject: [PATCH] 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 --- BUILD.md | 9 +++++++++ CLAUDE.md | 28 ++++++++++++++++++++++++-- docs/design-notes.md | 8 +++++++- src/render/Colors.cpp | 11 ++++++++++ src/render/Colors.hpp | 5 ++++- src/render/Panels.cpp | 3 +++ src/render/Viewer.cpp | 6 ++++++ src/render/ViewerInput.cpp | 8 +++++++- src/render/ViewerRender.cpp | 2 +- src/sim/Planet.hpp | 9 ++++++--- src/sim/PlanetBiomes.cpp | 13 +++++++++--- src/sim/PlanetClimate.cpp | 40 +++++++++++++++++++++++++++++++++++++ src/sim/PlanetIO.cpp | 10 ++++++++-- src/sim/PlanetTypes.hpp | 12 +++++++++++ 14 files changed, 150 insertions(+), 14 deletions(-) diff --git a/BUILD.md b/BUILD.md index e04b2b3..df8b003 100644 --- a/BUILD.md +++ b/BUILD.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index a46184b..325a25b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`/ diff --git a/docs/design-notes.md b/docs/design-notes.md index 560d3a3..c8a1a8f 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -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 diff --git a/src/render/Colors.cpp b/src/render/Colors.cpp index 8edeacd..e1d9bf7 100644 --- a/src/render/Colors.cpp +++ b/src/render/Colors.cpp @@ -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); diff --git a/src/render/Colors.hpp b/src/render/Colors.hpp index 6971e43..065bbf2 100644 --- a/src/render/Colors.hpp +++ b/src/render/Colors.hpp @@ -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); diff --git a/src/render/Panels.cpp b/src/render/Panels.cpp index 26accb3..9e0201f 100644 --- a/src/render/Panels.cpp +++ b/src/render/Panels.cpp @@ -50,6 +50,9 @@ static std::vector 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) diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index 2e0c851..1df1895 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -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& temp = planet.temperature(); + const std::vector& summer = planet.summerTemp(); + const std::vector& winter = planet.winterTemp(); const std::vector& moist = planet.moisture(); // 0..1, already robustly normalized const std::vector& flora = planet.floraDensity(); const std::vector& 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; diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index 3565577..a71dd67 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -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(); } diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index 388076c..d14ab3c 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -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 | +/-", diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index 2d3de2b..a75b378 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -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& temperature() const { return sTemp; } // deg C + const std::vector& temperature() const { return sTemp; } // deg C, annual mean const std::vector& precipitation() const { return sPrecip; } // relative units const std::vector& moisture() const { return sMoist; } // 0..1 (median land -> 0.5) + const std::vector& summerTemp() const { return sTempSummer; } // deg C, warmest month + const std::vector& 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 sFlowTo, sHydroOrder; // Phase-3 climate scratch (derived each computeClimate(); not saved). sMoist is the - // 0..1-normalized precipitation the biome classifier reads. - std::vector 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 sTemp, sPrecip, sMoist, sTempSummer, sTempWinter; std::vector sWind; std::vector sUpwind; diff --git a/src/sim/PlanetBiomes.cpp b/src/sim/PlanetBiomes.cpp index a55e2f2..44e01c0 100644 --- a/src/sim/PlanetBiomes.cpp +++ b/src/sim/PlanetBiomes.cpp @@ -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; diff --git a/src/sim/PlanetClimate.cpp b/src/sim/PlanetClimate.cpp index 4db9a6f..4b4cecb 100644 --- a/src/sim/PlanetClimate.cpp +++ b/src/sim/PlanetClimate.cpp @@ -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 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; + } + } } diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 82a7ca6..e94463f 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -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")); diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index 2531c2f..a1bda04 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -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.