From aa32e38dad2186b04a20c2b9ce8fd6e7fc8686ef Mon Sep 17 00:00:00 2001 From: Jonas Reith Date: Sun, 28 Jun 2026 10:04:45 +0200 Subject: [PATCH] Live World stage: clock, day/night, seasons, moons, tides & ocean currents Adds the slow real-time "Live World" mode (key W on a settled world) that runs the finished planet on an hours->weeks/months clock (liveTime/liveRate, [ / ] ramp the rate) with geology frozen. Everything new is a derived per-cell field flowed over the fixed grid. Day/night & seasons (PlanetLive.cpp): a raylib-free per-cell insolation field (computeInsolation) drives a moving day/night terminator (N) from time-of-day rotation + seasonal declination (axialTilt); a live seasonal temperature cycles the static summer/winter fields over the year (computeLiveSeason); a moving snow/sea-ice line tracks it. Day/night + snow are render overlays over any colour mode (3D + 2D). Save v8 stores the live clock. Sky & tides (PlanetOcean.cpp): 1-3 random moons (separate RNG, saved v9) orbit on the clock and, with the now small/distant sun, raise an equilibrium tide (computeTides -> sTide), shown as a tide-coloured coastline (T, buildCoastline + tideColor). Moons render with sun-lit phases, orbit rings and eclipses (solar shadow spot in the day/night overlay, lunar dimming). The 2D map is left-aligned; the freed space holds a Live-World "Sky & tides" panel (per-moon phase + a selected coastal tile's tidal phase). Ocean currents + climate feedback (PlanetOcean.cpp): computeOceanCurrents builds a per-ocean-cell tangent velocity from wind stress + Coriolis deflection + coast-following (gyres); computeClimate feeds warm (poleward) / cold (equatorward) currents back into sTemp as a bounded coastal anomaly (climateCurrentFactor) before seasons, so biomes shift. Rendered as warm/cold current arrows (O). Config: dayLengthHours/yearLengthDays/snowTemp/seaIceTemp/tideAmplitude/tideSunFactor/ climateCurrentFactor. Save header v7->v9 (version-gated; older saves load fine). Headless test_live.cpp + test_ocean.cpp; test_logic/test_biota still pass; GUI build clean. Docs updated. Co-Authored-By: Claude Opus 4.8 --- BUILD.md | 33 +++++- CLAUDE.md | 125 +++++++++++++++++++-- CMakeLists.txt | 2 + docs/design-notes.md | 62 +++++++++++ src/render/Colors.cpp | 13 +++ src/render/Colors.hpp | 3 + src/render/Overlays.cpp | 82 ++++++++++++++ src/render/Overlays.hpp | 19 ++++ src/render/Panels.cpp | 20 +++- src/render/Panels.hpp | 3 +- src/render/Viewer.cpp | 104 +++++++++++++++++- src/render/Viewer.hpp | 26 ++++- src/render/ViewerInput.cpp | 27 ++++- src/render/ViewerRender.cpp | 213 ++++++++++++++++++++++++++++++++++-- src/sim/Planet.cpp | 1 + src/sim/Planet.hpp | 42 ++++++- src/sim/PlanetClimate.cpp | 32 ++++++ src/sim/PlanetIO.cpp | 16 ++- src/sim/PlanetLive.cpp | 52 +++++++++ src/sim/PlanetOcean.cpp | 152 +++++++++++++++++++++++++ src/sim/PlanetTypes.hpp | 27 +++++ test_live.cpp | 137 +++++++++++++++++++++++ test_ocean.cpp | 144 ++++++++++++++++++++++++ 23 files changed, 1296 insertions(+), 39 deletions(-) create mode 100644 src/sim/PlanetLive.cpp create mode 100644 src/sim/PlanetOcean.cpp create mode 100644 test_live.cpp create mode 100644 test_ocean.cpp diff --git a/BUILD.md b/BUILD.md index df8b003..9fbd15c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -47,8 +47,12 @@ the full ~2.8x speedup; the default uses all cores for no extra gain: J toggle rivers (Phase 2.5 hydrology) H start/stop Phase 2.5 (hydrology: rivers, lakes, fluvial erosion) L generate biota population (flora/fauna/funga; settled world; re-press regenerates) + W enter / leave Live World (slow real-time clock; settled world) + N toggle the day/night terminator (Live World) + T toggle the tide-coloured coastline (Live World) + O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue) SPACE pause while forming / re-evolve once settled (or the on-screen button) - [ / ] drift speed (My per real second, Phase 2/3) + [ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month) S single tectonic tick F fast-forward Phase-1 forming to settled (instant) R reseed planet (restart forming) @@ -67,8 +71,9 @@ CLI flags (applied before the first load/generate): planet.cfg human-editable key=value config of every PlanetConfig parameter; auto-created on first run, reload live with F2. Range-checked on load; an invalid file reverts to safe defaults (not overwritten). - planet.save binary snapshot (versioned, currently v6): seed + config + full planet - state; F5 writes it, F9 reloads and resumes deterministically. As of v6 + planet.save binary snapshot (versioned, currently v9: +moons; v8 +Live World clock; + v7 +biota): seed + config + full planet state; F5 writes it, F9 reloads and + resumes deterministically. As of v6 the config is stored as a self-describing key=value block (like planet.cfg), so adding/removing config fields no longer breaks saves (unknown keys ignored, missing keys default). v6 cannot load pre-v6 @@ -161,6 +166,7 @@ shadow, dry interiors) then diffuses it. Color modes 6 (temperature) / 7 (precip climateContinentality 0.05 fractional moisture lost per inland cell (dries interiors) climateWindPasses 50 moisture-advection iterations (steady state) climateMoistureSmooth 12 precipitation diffusion passes (raise = smoother, more grass/forest) + climateCurrentFactor 4 C max coastal warming/cooling from ocean currents (0 = off; O shows arrows) Biota (PlanetConfig): flora/fauna/funga. Density scalars drive color modes 8/9/0; the discrete slot/point population is generated on demand (L) and saved (v7). @@ -182,17 +188,32 @@ the discrete slot/point population is generated on demand (L) and saved (v7). bioFaunaPoints 16 fauna point budget at full density bioFungaPoints 14 funga point budget at full density +Live World (PlanetConfig): the slow real-time clock (hours -> weeks/months) over the +finished planet -- a moving day/night terminator, a live seasonal temperature cycle and a +moving snow line. axialTilt (above) drives the seasonal declination. Press W to enter. + + dayLengthHours 24 hours in one planetary day (rotation -> day/night) + yearLengthDays 365.25 days in one planetary year (orbit -> seasons) + snowTemp 0 C land below the live temperature shows snow + seaIceTemp -2 C ocean below the live temperature shows sea ice + tideAmplitude 0.6 m equilibrium-tide scale per unit tide-raising weight + tideSunFactor 0.46 sun's tide weight relative to a unit moon (Earth ~0.46) + +Moons (1-3, randomized in generateMoons() + saved v9): orbit on the live clock, raise the +tides with the sun, and render as small lit spheres with phases, orbit rings and eclipses. + ## Headless logic test (no display) 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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \ + src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp \ + src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \ src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \ -o /tmp/t && /tmp/t - # Biota suite: same source list, swap test_logic.cpp -> test_biota.cpp + # Biota / Live World / Ocean suites: same source list, swap test_logic.cpp -> + # test_biota.cpp, test_live.cpp or test_ocean.cpp Verifies geometry, plate assignment, gradual non-saturating relief and determinism. Run after changing Planet::step(). diff --git a/CLAUDE.md b/CLAUDE.md index 325a25b..bc6adec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,10 +56,33 @@ dynamic weather and life. > lives in **`docs/design-notes.md`** — important because Claude's auto-memory does not > travel with the repo. -**Live World (future):** with planet creation finished, run the world at a slow real-time +**Live World (in progress):** with planet creation finished, run the world at a slow real-time scale with dynamic **weather** (clouds, rain, storms, fronts), day/night, and living **ecosystems / civilization** evolving in real time. This is a separate large effort; the fixed-grid Eulerian model + the climate fields are the groundwork for it. +- **Clock + day/night groundwork** *(done — see `PlanetLive.cpp`)* — a slow real-time clock + (hours → weeks/months, `liveTime`/`liveRate`, key `W` to enter once settled, `[`/`]` ramp + the rate), a moving **day/night terminator** (sun from time-of-day rotation + seasonal + declination via `axialTilt`; key `N`), a raylib-free per-cell **insolation field** + (`computeInsolation`, the weather hook), a **live seasonal temperature** cycling the + static summer/winter fields over the year (`computeLiveSeason`), and a **moving snow / sea-ice + line**. Day/night + snow are render overlays over any colour mode (3D + 2D). Save **v8** adds + the live-clock state. Still **future:** actual weather, day/night temperature swing, + precipitation/evaporation, living/evolving ecosystems. +- **Moons + tides + distant sun** *(done — see `PlanetOcean.cpp`)* — **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. ## Current state @@ -299,6 +322,54 @@ Working and verified (logic tested headless): organism list. `bio*` config knobs. Headless `test_biota.cpp`: density ranges/zeros, fauna≤ capacity, carnivore gating, slot/point budgets, determinism + RNG isolation, v7 round-trip, pre-v7 loads empty. v7 reads v6-and-older (no biota block → empty population; press `L`). +- **Live World — clock + day/night + live seasons + snow line:** the first **Live World** stage + (the slow real-time arc after World Creation). Engine (`src/sim/PlanetLive.cpp`, raylib-free, + derived/not-saved): `computeInsolation(dayOfYear01, timeOfDay01)` → `sInsolation` (0..1 cosine + solar incidence; declination `axialTilt·sin(2π·doy)`, sub-solar longitude sweeps once per day; + the foundation the future weather sim reads) and `computeLiveSeason(doy)` → `sLiveTemp` (the + annual-mean `sTemp` swung toward the existing `summerTemp`/`winterTemp` by the seasonal phase, + anti-phased across hemispheres). Viewer: key `W` (settled world) toggles **Live World** — drift + freezes and `liveTime` advances at `liveRate` (sim hours/real-second), `[`/`]` ramp it + hour→month; 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. +- **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. - 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 @@ -330,6 +401,8 @@ src/ 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 (Live World sky & tides) 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 @@ -394,12 +467,13 @@ raylib 5.5 is fetched automatically — do not vendor it. 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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \ + src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp \ + src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \ src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \ -o /tmp/t && /tmp/t ``` -(Swap `test_logic.cpp` for `test_biota.cpp` to run the Biota suite — same source list.) +(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp` or `test_ocean.cpp` to run the +Biota / Live World / Ocean suites — same source list.) Use this to verify tectonics after changing `Planet::step()` without launching the window (the engine lives in `src/sim` and is raylib-free, so it links without @@ -429,10 +503,13 @@ 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) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) · +all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) · +`O` ocean-current arrows (warm/cold) · `SPACE` or on-screen button pause · +`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) · `S` single tick · `F` fast-forward Phase-1 forming to settled · `H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga, -on a settled world; re-press regenerates) · `R` reseed · +on a settled world; re-press regenerates) · `W` enter/leave **Live World** (settled world) · +`R` reseed · `+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load · `F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate. @@ -441,6 +518,13 @@ Phase 2** / **Start Phase 3**; `H` starts/stops it manually. In Phase 3 drift keeps running at a finer timestep (`cflDtMy()*phase3DtScale`) while rivers, lakes and fluvial erosion evolve. +Live World (`W`, on a settled world): geological drift freezes and a slow real-time clock runs +(`liveTime` in hours, `liveRate` = sim hours/real-second, ramped hour→month with `[`/`]`). A +moving day/night terminator (`N`), a live seasonal temperature cycle and a moving snow/sea-ice +line animate over whatever colour mode is active; the HUD shows a `Year/Day/HH:MM` calendar. +**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). + CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config file (both applied before the initial load/generate). @@ -450,11 +534,13 @@ PlanetConfig param, auto-created on first run, reload with `F2`) and `Planet::writeState`/`readState`, resumes deterministically). Config is range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe defaults (without overwriting your `planet.cfg`) and shows a status message. The -save header is versioned (currently **7**; v2 adds the `[`/`]` drift rate, v3 a +save header is versioned (currently **9**; v2 adds the `[`/`]` drift rate, v3 a `phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing key=value text block** instead of a raw POD dump, v7 appends the **biota population** -block — three Organism lists per cell, gated by a flag byte); newer-than-supported is -rejected. Older saves (no biota block) load fine with an empty population (press `L`). +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); 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. **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 — @@ -462,8 +548,10 @@ 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); right column 30% wide = cell info -(top 50% h) + subareas (bottom 50% h). 3D hover uses a custom camera ray with the +1344x648) + 2D Equal Earth map (bottom, 40% h, **left-aligned**, with the freed space at +its right holding the Live-World **"Sky & tides" panel** — `liveInfoRect`, `renderLiveInfo`: +per-moon phase discs + a selected coastal tile's tidal phase); right column 30% wide = cell +info (top 50% h) + subareas (bottom 50% h). 3D hover uses a custom camera ray with the 3D viewport (1344x648 at the origin -- GetScreenToWorldRay assumes the full screen, wrong here); 2D hover uses EqualEarth::inverse (minus the `mapLon` pan). Borders (`B`), drift vectors (`D`) and a lat/lon graticule (`G`) draw in BOTH the @@ -511,6 +599,9 @@ 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. + `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. - 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` @@ -526,6 +617,18 @@ triangles (plates are fixed in phase 1). it; Tiny=1…Huge=5), `bioRegionBonus` (how strongly a cell copies same-biome neighbours → homogeneity vs variety). To add organisms, append to `biotaArchetypes()` in PlanetBiota.cpp (append-only — indices are serialized in v7 saves). +- **Live World (`dayLengthHours`/`yearLengthDays`/`snowTemp`/`seaIceTemp`, `planet.cfg`):** + `dayLengthHours` (24) sets the day/night period (and the `d/s` rate unit), `yearLengthDays` + (365.25) the season period; `axialTilt` drives the seasonal declination (0 = no day/night + tilt). `snowTemp` (0 °C) is the land snow line, `seaIceTemp` (-2 °C) the ocean sea-ice line — + raise either to grow the white caps. Render constants (night-floor brightness, terminator + softness, snow-blend ramp) live in `rebuildLiveOverlay()` (src/render/Viewer.cpp), not config. +- **Moons & tides (`tideAmplitude`/`tideSunFactor`, `planet.cfg`):** `tideAmplitude` (0.6 m) + scales the equilibrium tide per unit tide-raising weight (raise for a more dramatic coastline + swing); `tideSunFactor` (0.46) is the sun's tide weight vs a unit moon. Moon count (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. - `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main knob for how fast/high relief builds. - `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d3e50d..e66b037 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,8 @@ add_executable(planetsim src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp + src/sim/PlanetLive.cpp + src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp diff --git a/docs/design-notes.md b/docs/design-notes.md index c8a1a8f..28e687f 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -31,6 +31,10 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`). - `PlanetHydrology.cpp` — `routeFlow`/`computeHydrology`/`hydrology` (depression-fill→lakes, steepest-descent→rivers, mass-conserving stream-power incision). - `PlanetClimate.cpp` — `computeClimate()` (temperature + orographic precipitation). +- `PlanetLive.cpp` — `computeInsolation()`/`computeLiveSeason()` (Live World: day/night + live + seasonal temperature; derived, not saved). +- `PlanetOcean.cpp` — moons (`generateMoons`, `moonDirection`/`sunDirection`/`moonOrbitNormal`) + + `computeTides()` (Live World sky & equilibrium tides). Moons are saved (v9); tides derived. - `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate). - `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw + `computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga). @@ -99,6 +103,8 @@ get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seede 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. + **Ocean currents** add a bounded coastal warm/cold anomaly to this mean before the seasons pass + (`climateCurrentFactor`; see the Ocean section). - **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 @@ -110,6 +116,61 @@ get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seede deserts emerge; 13 biomes incl. polar Ice; wetlands require water adjacency. All biome & climate thresholds are tunable `biome*` / `climate*` keys in `planet.cfg`. +## Live World (slow real-time clock) — day/night + live seasons (derived, not saved) + +The arc after World Creation: the finished planet runs on a slow real-time clock instead of +the geological My clock. `PlanetLive.cpp` (raylib-free) builds two derived per-cell fields, +recomputed each frame like climate (never saved): +- `computeInsolation(dayOfYear01, timeOfDay01)` → `sInsolation` ∈ [0,1], the instantaneous + solar incidence `max(0, cell.unit · sunDir)`. `sunDir = lonLatToDir(λ, δ)` with declination + `δ = axialTilt·sin(2π·dayOfYear01)` (0 at equinox, ±tilt at solstice → polar day/night) and + sub-solar longitude `λ = π·(1−2·timeOfDay01)` sweeping once per day. **This is the hook the + future weather sim reads** (daytime heating). Computed in **model space** (the fixed cell + units) so it stays consistent with both the tilted 3D globe (the lit pattern rotates with the + globe; the seasonal lean is carried by `δ`, not the render tilt) and the model-space 2D map. +- `computeLiveSeason(dayOfYear01)` → `sLiveTemp`, the annual-mean `sTemp` swung toward the + static `summerTemp`/`winterTemp` by the seasonal phase `g = sin(2π·doy)·sign(lat)` + (`liveTemp = mean + A·g`, `A = (summer−winter)/2`), anti-phased across hemispheres. + +Viewer (Eulerian, geometry fixed — all overlays are per-cell render passes): key `W` (settled +world) toggles `liveWorld`; drift freezes and `liveTime` (hours) advances at `liveRate` (sim +hours/real-second, ramped hour→month with `[`/`]`). `rebuildLiveOverlay()` builds `illum` (soft +day/night terminator over `sInsolation`, 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. Save **v8** appends a Live World flag + `liveTime` (header, version-gated). Knobs: +`dayLengthHours`/`yearLengthDays`/`snowTemp`/`seaIceTemp` in `planet.cfg`. + +## Moons & tides (Live World sky/oceans) + +`PlanetOcean.cpp` (raylib-free): `generateMoons()` seeds **1–3 `Moon`s** from a **separate RNG** +(`cfg.seed ^ 0x900D5EED`) so it never touches the tectonic `rngState` — moons are world objects +(not cells) and are **saved (v9)** via `writeState`/`readState(..., hasMoons)` (pre-v9 saves +synthesize them from the seed). Sky geometry is one source of truth: `sunDirection(doy,tod)` = +celestial dir leaned by declination then spun `-2π·tod` about +Y; `moonDirection(i,tod,days)` = +inclined orbit circle `Ω=2π·days/period+phase` then the same spin (so a fixed cell sees ≈one +lunar pass/day). `computeInsolation` now calls `sunDirection`. **Tides** (`computeTides` → +`sTide`, derived/not saved): equilibrium two-bulge potential `Σ_body w·(cosθ²−⅓)` over the moons +(weight `tideWeight`) + sun (`tideSunFactor`), scaled `tideAmplitude` — zero-mean, high under a +body and its antipode, low at 90°, sweeping ≈twice/day. + +Render (Viewer): the coastline is traced once per terrain change (`buildCoastline`, dual-contour +on the land/ocean split, recording the adjacent ocean cell per segment) and coloured by +`tideColor(sTide[oceanCell])` (`T`; auto-scaled), in 3D + 2D. The 3D sun is small/distant with a +halo; moons render at a visible orbit band with a sun-lit **phase** (offset-dark-sphere trick), +faint **orbit rings** (great circle ⟂ `moonOrbitNormal`), and **eclipses** — solar darkens a spot +in `rebuildLiveOverlay`'s `illum` near the sub-solar point when a moon transits the sun; lunar +dims a moon reddish in the planet's shadow. + +**Ocean currents** (`computeOceanCurrents`, also `PlanetOcean.cpp`): a per-ocean-cell tangent +velocity `sCurrent` from wind stress (`sWind`) rotated by a **Coriolis** deflection (right N / +left S about the cell normal), with the across-shore component removed at land neighbours so the +stream follows the coast (gyres), then smoothed and re-tangented (zero on land). `computeClimate` +calls it right after the wind pass and feeds **warm (poleward) / cold (equatorward)** currents +back into `sTemp` as a bounded coastal anomaly (`climateCurrentFactor`, smoothed onto coasts, +applied before seasons → biomes shift with it). Rendered as warm/cold arrows over the sea +(`buildCurrents`, key `O`). Currents/feedback are derived (not saved). + ## Headless testing Engine is raylib-free, so logic is tested without a display. Build/run: @@ -117,6 +178,7 @@ Engine is raylib-free, so logic is tested without a display. Build/run: 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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \ src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t # test_biota.cpp uses the same source list (Biota suite). diff --git a/src/render/Colors.cpp b/src/render/Colors.cpp index e1d9bf7..6255606 100644 --- a/src/render/Colors.cpp +++ b/src/render/Colors.cpp @@ -149,6 +149,19 @@ Color seasonColor(double rangeC) { return Color{ L(0), L(1), L(2), 255 }; } +// Tide: diverging around mid-tide. Low (negative) -> amber, high (positive) -> cyan. +Color tideColor(double level, double range) { + static const unsigned char loC[3] = { 235, 175, 80 }; // low tide (amber) + static const unsigned char midC[3] = { 150, 175, 185 }; // mid tide (pale) + static const unsigned char hiC[3] = { 55, 165, 235 }; // high tide (cyan) + double t = std::clamp(level / std::max(1e-6, range), -1.0, 1.0); + const unsigned char* a = t < 0.0 ? loC : midC; + const unsigned char* b = t < 0.0 ? midC : hiC; + double f = std::fabs(t); + auto L = [&](int c){ return (unsigned char)(a[c] + (b[c] - a[c]) * f); }; + 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 065bbf2..f729e98 100644 --- a/src/render/Colors.hpp +++ b/src/render/Colors.hpp @@ -25,6 +25,9 @@ Color tempColor(double celsius); Color precipColor(double moist01); // Seasonality: summer-winter temperature range in deg C (grey calm -> orange extreme). Color seasonColor(double rangeC); +// Tide level (m), diverging around 0: low tide amber -> high tide cyan. `range` sets the +// saturation scale (meters at which the colour is fully amber/cyan). +Color tideColor(double level, double range); // 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/Overlays.cpp b/src/render/Overlays.cpp index 9e1be25..4f832ca 100644 --- a/src/render/Overlays.cpp +++ b/src/render/Overlays.cpp @@ -97,6 +97,71 @@ void buildRivers(const Planet& p, float radius, } } +void buildCoastline(const Planet& p, float radius, + std::vector& segs, std::vector& oceanCell) { + segs.clear(); oceanCell.clear(); + const double sea = p.cfg.seaLevel; + auto isLand = [&](int i) { return p.cells[i].elevation > sea; }; + auto midV = [&](int i, int j) -> Vector3 { + Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius; + return Vector3{ (float)m.x, (float)m.y, (float)m.z }; + }; + // The ocean cell nearest a coast segment, for sampling tide. Prefer an ocean endpoint of + // the cut edge; fall back to the lone/other cell that is ocean. + auto emit = [&](const Vector3& a, const Vector3& b, int oc) { + segs.push_back(a); segs.push_back(b); oceanCell.push_back(oc); + }; + const std::vector& tri = p.triIndices(); + for (size_t k = 0; k + 2 < tri.size(); k += 3) { + int ia = tri[k], ib = tri[k + 1], ic = tri[k + 2]; + bool la = isLand(ia), lb = isLand(ib), lc = isLand(ic); + if (la == lb && lb == lc) continue; // all land or all ocean: no coast + // Exactly one vertex differs from the other two -> one cut separating it. + int lone, o1, o2; + if (la == lb) { lone = ic; o1 = ia; o2 = ib; } + else if (lb == lc) { lone = ia; o1 = ib; o2 = ic; } + else { lone = ib; o1 = ia; o2 = ic; } + int oc = isLand(lone) ? o1 : lone; // the ocean side of the cut + emit(midV(lone, o1), midV(lone, o2), oc); + } +} + +void buildCurrents(const Planet& p, float radius, + std::vector& segs, std::vector& cols) { + segs.clear(); cols.clear(); + const std::vector& cur = p.current(); + if (cur.empty()) return; + const double sea = p.cfg.seaLevel; + double maxSp = 1e-9; + for (const Vec3& v : cur) maxSp = std::max(maxSp, v.length()); + const Vec3 up{0, 1, 0}; + const Color warm{235, 120, 90, 255}, cold{90, 160, 235, 255}; + auto V = [](const Vec3& q) { return Vector3{ (float)q.x, (float)q.y, (float)q.z }; }; + for (int i = 0; i < (int)p.cells.size(); i += 3) { // subsample for readability + if (p.cells[i].elevation > sea) continue; + const Vec3& vel = cur[i]; + double sp = vel.length(); + if (sp < 0.18 * maxSp) continue; // skip the slack water + const Vec3& nrm = p.cells[i].unit; + Vec3 dir = vel * (1.0 / sp); + double len = 0.02 + 0.03 * (sp / maxSp); + Vec3 base = nrm * (double)radius; + Vec3 tip = base + dir * len; + Vec3 perp = dir.cross(nrm).normalized(); + Vec3 back = dir * -1.0; double hl = len * 0.35; + Vec3 h1 = tip + (back * 0.8 + perp * 0.6) * hl; + Vec3 h2 = tip + (back * 0.8 - perp * 0.6) * hl; + // Warm if flowing poleward (toward the nearer pole), cold if equatorward. + Vec3 northT = up - nrm * up.dot(nrm); double nl = northT.length(); + double pw = 0.0; + if (nl > 1e-9) { northT = northT * (1.0 / nl); pw = dir.dot(northT) * (nrm.y >= 0 ? 1.0 : -1.0); } + Color c = pw >= 0.0 ? warm : cold; + segs.push_back(V(base)); segs.push_back(V(tip)); cols.push_back(c); + segs.push_back(V(tip)); segs.push_back(V(h1)); cols.push_back(c); + segs.push_back(V(tip)); segs.push_back(V(h2)); cols.push_back(c); + } +} + std::vector> buildGraticule() { std::vector> g; const double D = M_PI / 180.0; @@ -171,6 +236,23 @@ void drawSegments2D(const std::vector& segs, Color col, float width, rlEnd(); rlSetLineWidth(1.0f); } +void drawColoredSegments2D(const std::vector& segs, const std::vector& cols, + float width, Rectangle r, double lonOffset) { + if (segs.empty()) return; + rlSetLineWidth(width); rlBegin(RL_LINES); + for (size_t i = 0, c = 0; i + 1 < segs.size(); i += 2, ++c) { + Vec3 a = Vec3{segs[i].x, segs[i].y, segs[i].z}.normalized(); + Vec3 b = Vec3{segs[i + 1].x, segs[i + 1].y, segs[i + 1].z}.normalized(); + double alo, ala, blo, bla; dirToLonLat(a, alo, ala); dirToLonLat(b, blo, bla); + Vector2 pa = projLonLat(alo, ala, lonOffset, r), pb = projLonLat(blo, bla, lonOffset, r); + if (fabsf(pa.x - pb.x) > r.width * 0.5f) continue; + const Color& col = cols[c < cols.size() ? c : cols.size() - 1]; + rlColor4ub(col.r, col.g, col.b, 255); + rlVertex2f(pa.x, pa.y); rlVertex2f(pb.x, pb.y); + } + rlEnd(); rlSetLineWidth(1.0f); +} + void drawSubgrids(const std::vector>& sgs, float visBase, float elevExagg, double seaLevel, float eps) { for (const auto& sg : sgs) { diff --git a/src/render/Overlays.hpp b/src/render/Overlays.hpp index ba074f5..887e7ab 100644 --- a/src/render/Overlays.hpp +++ b/src/render/Overlays.hpp @@ -25,6 +25,20 @@ void buildDriftArrows(const Planet& p, float radius, void buildRivers(const Planet& p, float radius, std::vector& rivers, std::vector& bigRivers); +// ---- Coastline (land/ocean boundary, dual contour through triangles) -------- +// Like buildBorders but on the elevation-vs-seaLevel split. For each emitted segment +// (pairs in `segs`) records a representative adjacent OCEAN cell in `oceanCell` (one +// entry per segment, i.e. per 2 points) so the viewer can colour it by that cell's tide. +void buildCoastline(const Planet& p, float radius, + std::vector& segs, std::vector& oceanCell); + +// ---- Ocean currents (subsampled arrows, warm/cold) -------------------------- +// Short arrows along Planet::current() over a subsample of ocean cells. `cols` has one +// colour per segment (warm = poleward/red, cold = equatorward/blue). Needs computeClimate +// (which computes the current field) to have run. +void buildCurrents(const Planet& p, float radius, + std::vector& segs, std::vector& cols); + // ---- Lat/lon graticule ------------------------------------------------------ // Polylines of (lon,lat) radians (Vector2.x=lon, .y=lat). std::vector> buildGraticule(); @@ -38,6 +52,11 @@ void drawGraticuleLabels2D(Rectangle r, double lonOffset); void drawSegments2D(const std::vector& segs, Color col, float width, Rectangle r, double lonOffset); +// As drawSegments2D but with a per-segment colour (cols has one entry per segment, +// i.e. per 2 points). Used for the tide-shaded coastline. +void drawColoredSegments2D(const std::vector& segs, const std::vector& cols, + float width, Rectangle r, double lonOffset); + // ---- Subgrid overlay (high-res patch over a selected cell) ------------------ void drawSubgrids(const std::vector>& sgs, float visBase, float elevExagg, double seaLevel, float eps); diff --git a/src/render/Panels.cpp b/src/render/Panels.cpp index 9e0201f..acbc370 100644 --- a/src/render/Panels.cpp +++ b/src/render/Panels.cpp @@ -53,6 +53,17 @@ static std::vector cellInfo(const Planet& p, int i, double elev, do 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]))); + // Live World: current-season temperature + whether it's day or night + snow cover. + if (sized(p.liveTemp())) { + bool day = sized(p.insolation()) && p.insolation()[i] > 0.05; + bool snow = (elev > p.cfg.seaLevel) ? (p.liveTemp()[i] < p.cfg.snowTemp) + : (p.liveTemp()[i] < p.cfg.seaIceTemp); + L.push_back(std::string(TextFormat("live %.1f C %s%s", p.liveTemp()[i], + day ? "day" : "night", snow ? " snow" : ""))); + } + if (sized(p.tide())) + L.push_back(std::string(TextFormat("tide %+.2f m (%s)", p.tide()[i], + p.tide()[i] >= 0.0 ? "high" : "low"))); 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) @@ -166,7 +177,8 @@ void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected) { } } -void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting) { +void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting, + bool live, double liveHours) { DrawRectangleRec(r, Color{12, 14, 22, 235}); DrawRectangleLinesEx(r, 1, Color{120, 120, 150, 255}); int x = (int)r.x + 16, y = (int)r.y + 12; @@ -198,6 +210,7 @@ void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting) { L(TextFormat("Surface area: %.0f M km2", surfKm2 / 1.0e6)); L(TextFormat("Plates: %d active %d continental / %d oceanic", used, contPlates, used - contPlates)); L(TextFormat("Young ridges: %d strips %d cells", babyPlates, babyCells)); + L(TextFormat("Moons: %d", (int)p.getMoons().size())); int water = N - landGeo; double wlRatio = landGeo > 0 ? (double)water / landGeo : 0.0; L(TextFormat("Land %.0f%% Ocean %.0f%% (water:land %.2f:1)", @@ -205,7 +218,10 @@ void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting) { L(TextFormat("Sea level: %+.0f m", seaLvl)); L(TextFormat("Crust: %.0f%% continental / %.0f%% oceanic", 100.0 * cont / N, 100.0 * (N - cont) / N)); L(TextFormat("Elevation: %.0f .. %.0f m mean %.0f m", mn, mx, sum / N)); - if (drifting) L(TextFormat("Sim time: %.0f My", elapsedMy)); + if (live) L(TextFormat("Live World: year %ld, day %.1f", + (long)(liveHours / p.cfg.dayLengthHours / p.cfg.yearLengthDays) + 1, + std::fmod(liveHours / p.cfg.dayLengthHours, p.cfg.yearLengthDays))); + else if (drifting) L(TextFormat("Sim time: %.0f My", elapsedMy)); y += 8; DrawText("plate cells size area speed land", x, y, 15, Color{150, 155, 170, 255}); y += 21; diff --git a/src/render/Panels.hpp b/src/render/Panels.hpp index 188177d..dce1e75 100644 --- a/src/render/Panels.hpp +++ b/src/render/Panels.hpp @@ -17,4 +17,5 @@ void drawDetailPanel(const Planet& p, const std::shared_ptr& sg, void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected); // World statistics panel (shown in the subareas quadrant when no tile selected). -void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting); +void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting, + bool live = false, double liveHours = 0.0); diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index 1df1895..b91d6e8 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -35,9 +35,13 @@ bool Viewer::init(int argc, char** argv) { int mapH = mapAreaH - 30; int mapW = (int)(mapH * (EqualEarth::halfWidth() / EqualEarth::halfHeight())); if (mapW > leftW - 30) { mapW = leftW - 30; mapH = (int)(mapW / (EqualEarth::halfWidth() / EqualEarth::halfHeight())); } - mapRect = Rectangle{ (float)((leftW - mapW) / 2), + // Left-align the 2D map (was centered) so the freed space at right holds the live sky panel. + const float mapMargin = 16.0f; + mapRect = Rectangle{ mapMargin, (float)(mapAreaY + (mapAreaH - mapH) / 2), (float)mapW, (float)mapH }; + float liveX = mapRect.x + mapRect.width + 16.0f; + liveInfoRect = Rectangle{ liveX, mapRect.y, (float)leftW - liveX - 8.0f, mapRect.height }; // Right column. hoverRect = Rectangle{ (float)rightX + 8, 8.0f, (float)rightW - 16, (float)rightH - 16 }; @@ -136,6 +140,71 @@ void Viewer::recolor() { minE = planet.minElevation(); maxE = planet.maxElevation(); } +// Live World: from the sim's insolation + live-temperature fields, build the per-cell +// day/night brightness (illum) and the shaded draw colours (base colour -> snow/ice tint -> +// day/night dim). Cheap O(n); called every frame while in Live World. +void Viewer::rebuildLiveOverlay() { + const size_t n = planet.cells.size(); + const std::vector& sun = planet.insolation(); + const std::vector& lt = planet.liveTemp(); + const double sea = planet.cfg.seaLevel; + const double snowT = planet.cfg.snowTemp; + const double iceT = planet.cfg.seaIceTemp; + const float nightFloor = 0.18f; // night side dim (not black) so colours read + + illum.assign(n, 1.0f); + shadedColors.resize(n); + auto smoothstep = [](double e0, double e1, double x) { + double t = (e1 > e0) ? (x - e0) / (e1 - e0) : 0.0; + t = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t); + return t * t * (3.0 - 2.0 * t); + }; + // Solar eclipse: a moon roughly between the sun and the planet (its model-space direction + // near the sun's) casts a shadow around the sub-solar point. Strength ramps with alignment. + const Vec3 sd{ sunDir.x, sunDir.y, sunDir.z }; + const double eclipseReach = 0.13; // rad: how close a moon must be to the sun to eclipse + const double umbra = 0.10; // rad: angular radius of the shadow spot + double eclipseStrength = 0.0; + for (const auto& md : moonDirs) { + double d = std::acos(std::clamp((double)(md.x*sd.x + md.y*sd.y + md.z*sd.z), -1.0, 1.0)); + if (d < eclipseReach) eclipseStrength = std::max(eclipseStrength, 1.0 - d / eclipseReach); + } + auto blend = [](unsigned char c, unsigned char to, double a) { + return (unsigned char)(c + (to - c) * a); + }; + for (size_t i = 0; i < n; ++i) { + // Day/night: soft sunrise band over the clamped cosine incidence. + float f = nightFloor; + if (!sun.empty()) f = nightFloor + (1.0f - nightFloor) * (float)smoothstep(0.0, 0.12, sun[i]); + // Eclipse shadow: darken cells near the sub-solar point while a moon transits the sun. + if (eclipseStrength > 0.0 && !sun.empty()) { + double dd = std::acos(std::clamp(planet.cells[i].unit.x*sd.x + planet.cells[i].unit.y*sd.y + + planet.cells[i].unit.z*sd.z, -1.0, 1.0)); + double sh = eclipseStrength * std::exp(-(dd / umbra) * (dd / umbra)); + f *= (float)std::max(0.10, 1.0 - 0.85 * sh); + } + illum[i] = f; + + Color c = vcolors[i]; + // Snow on cold land, sea ice on cold ocean (live seasonal temperature). + if (!lt.empty()) { + double e = planet.cells[i].elevation; + if (e > sea) { + double a = (snowT - lt[i]) / 8.0; // fully snow ~8 C below freezing + if (a > 0.0) { a = a > 0.85 ? 0.85 : a; + c = Color{ blend(c.r, 242, a), blend(c.g, 246, a), blend(c.b, 250, a), 255 }; } + } else { + double a = (iceT - lt[i]) / 6.0; // sea ice + if (a > 0.0) { a = a > 0.9 ? 0.9 : a; + c = Color{ blend(c.r, 212, a), blend(c.g, 226, a), blend(c.b, 236, a), 255 }; } + } + } + // Day/night dimming over the (possibly snow-tinted) colour. + if (dayNightOn) c = Color{ (unsigned char)(c.r * f), (unsigned char)(c.g * f), (unsigned char)(c.b * f), 255 }; + shadedColors[i] = c; + } +} + void Viewer::refreshView() { if (phase3) planet.computeHydrology(); // refresh lakes/rivers for the view planet.computeClimate(); // temperature + precipitation fields @@ -147,6 +216,8 @@ void Viewer::refreshView() { buildDriftArrows(planet, driftR, driftArrows, plateLabels); } if (phase3) buildRivers(planet, riverR, rivers, bigRivers); + buildCoastline(planet, riverR, coast, coastOcean); // land/ocean boundary (for tide lines) + buildCurrents(planet, driftR, currentSegs, currentCols); // ocean current arrows (warm/cold) if (selectedCell >= 0) rebuildSub(); } @@ -180,12 +251,15 @@ void Viewer::saveGame(const char* path) { std::ofstream os(path, std::ios::binary); if (!os) { setStatus("Save failed"); return; } uint32_t ver = SAVE_VERSION; uint8_t st = settled ? 1 : 0; uint8_t p3 = phase3 ? 1 : 0; + uint8_t lw = liveWorld ? 1 : 0; os.write("PLSV", 4); os.write(reinterpret_cast(&ver), sizeof ver); os.write(reinterpret_cast(&elapsedMy), sizeof elapsedMy); os.write(reinterpret_cast(&st), sizeof st); os.write(reinterpret_cast(&driftRate), sizeof driftRate); os.write(reinterpret_cast(&p3), sizeof p3); // v3: Phase-3 flag + os.write(reinterpret_cast(&lw), sizeof lw); // v8: Live World flag + os.write(reinterpret_cast(&liveTime), sizeof liveTime); // v8: live clock (hours) planet.writeState(os); setStatus(os ? std::string("Saved ") + path : "Save failed"); } @@ -194,20 +268,24 @@ void Viewer::loadGame(const char* path) { std::ifstream is(path, std::ios::binary); if (!is) { setStatus(std::string("No ") + path); return; } char magic[4] = {0}; uint32_t ver = 0; double em = 0; uint8_t st = 0; double dr = 4.0; uint8_t p3 = 0; + uint8_t lw = 0; double lh = 0.0; is.read(magic, 4); is.read(reinterpret_cast(&ver), sizeof ver); is.read(reinterpret_cast(&em), sizeof em); is.read(reinterpret_cast(&st), sizeof st); if (ver >= 2) is.read(reinterpret_cast(&dr), sizeof dr); if (ver >= 3) is.read(reinterpret_cast(&p3), sizeof p3); + if (ver >= 8) { is.read(reinterpret_cast(&lw), sizeof lw); + is.read(reinterpret_cast(&lh), sizeof lh); } // v8: Live World clock if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; } - if (!planet.readState(is, ver >= 4, ver >= 7)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota + if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota, v9: moons cfg = planet.cfg; // adopt the loaded config elapsedMy = em; settled = (st != 0); planet.drifting = settled; // resume drift boosts iff mid-drift phase3 = (p3 != 0); phase3Prompt = false; phase3PromptAt = phase3 ? elapsedMy : (elapsedMy + planet.cfg.phase3AfterMy); driftRate = dr; + liveWorld = (lw != 0); liveTime = lh; // v8: resume the Live World clock settleRun = settleNeed; // keep the settled latch consistent dtMy = settled ? planet.cflDtMy() : 0.0; driftAccum = 0.0; formAccum = 0.0; @@ -222,6 +300,28 @@ void Viewer::loadGame(const char* path) { // Advance the simulation this frame: Phase-1 forming (paced ticks toward // equilibrium), or Phase-2 drift / Phase-3 drift+hydrology at a finer dt. void Viewer::stepSim() { + if (liveWorld) { + // --- Live World: advance the slow clock; geology is frozen -------- + if (!paused) liveTime += liveRate * GetFrameTime(); // hours + double days = liveTime / planet.cfg.dayLengthHours; + double dayOfYear01 = days / planet.cfg.yearLengthDays; + dayOfYear01 -= std::floor(dayOfYear01); + double timeOfDay01 = days - std::floor(days); + planet.computeInsolation(dayOfYear01, timeOfDay01); + planet.computeLiveSeason(dayOfYear01); + planet.computeTides(dayOfYear01, timeOfDay01, days); + Vec3 s = planet.sunDirection(dayOfYear01, timeOfDay01); // shared sky geometry + sunDir = Vector3{ (float)s.x, (float)s.y, (float)s.z }; + moonDirs.clear(); moonNormals.clear(); + for (int m = 0; m < (int)planet.getMoons().size(); ++m) { + Vec3 md = planet.moonDirection(m, timeOfDay01, days); + Vec3 mn = planet.moonOrbitNormal(m, timeOfDay01); + moonDirs.push_back(Vector3{ (float)md.x, (float)md.y, (float)md.z }); + moonNormals.push_back(Vector3{ (float)mn.x, (float)mn.y, (float)mn.z }); + } + rebuildLiveOverlay(); + return; + } if (!paused && !settled) { // --- Phase 1: forming, paced ticks toward equilibrium ------------- formAccum += GetFrameTime() * formRate; diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index f20d18d..f239ebb 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -15,7 +15,7 @@ // ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing). struct Viewer { // ---- Files / save format ------------------------------------------------ - static constexpr uint32_t SAVE_VERSION = 7; // v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3 + static constexpr uint32_t SAVE_VERSION = 9; // v9: +moons; v8: +Live World clock; v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3 const char* CONFIG_PATH = "planet.cfg"; const char* SAVE_PATH = "planet.save"; std::string configPath = "planet.cfg"; // initial config (--config overrides) @@ -26,6 +26,7 @@ struct Viewer { int view3DW = 0, view3DH = 0; RenderTexture2D rt3d{}; Rectangle mapRect{}, hoverRect{}, panelRect{}, gridRect{}; + Rectangle liveInfoRect{}; // free space right of the (left-aligned) 2D map: moon/tide phase Rectangle pauseBtn{}, p3ContinueBtn{}, p3StartBtn{}; float pbCx = 0.0f, pbCy = 0.0f; @@ -72,6 +73,24 @@ struct Viewer { bool phase3 = false, phase3Prompt = false; double phase3PromptAt = 0.0; + // Live World: a slow real-time clock (hours -> weeks/months) over the finished planet. + // Geological drift freezes while it runs; a moving day/night terminator, a live seasonal + // temperature cycle and a moving snow line animate. liveRate is sim hours per real second. + bool liveWorld = false, dayNightOn = true; + double liveTime = 0.0; // hours since the live clock started + double liveRate = 1.0; // sim hours advanced per real second (ramps hour->month) + std::vector illum; // per-cell day/night brightness (1 = day, floor = night) + std::vector shadedColors;// vcolors + snow/ice tint + day/night dim (live overlay) + Vector3 sunDir{0.0f, 0.0f, 1.0f}; // model-space sub-solar direction (for the 3D sun marker) + std::vector moonDirs; // model-space sub-lunar directions (one per moon, for render) + std::vector moonNormals;// model-space orbit-plane normals (one per moon, for the ring) + std::vector coast; // coastline segments (land/ocean boundary, rebuilt with terrain) + std::vector coastOcean; // ocean cell per coast segment (to sample tide) + std::vector coastCols; // per-segment tide colour (filled each frame when showTides) + bool showTides = false; // colour the coastline by the live tide level (key T) + std::vector currentSegs; std::vector currentCols; // ocean-current arrows + bool showCurrents = false; // ocean current arrows, warm/cold (key O) + // Selection + subgrid (phase 4/5 preview). int selectedCell = -1; double selectedThresh = 0.06; @@ -102,6 +121,10 @@ struct Viewer { void selectCell(int idx); void recolor(); void refreshView(); + void rebuildLiveOverlay(); // Live World: fill illum + shadedColors from sim fields + // Colors the 3D globe + 2D map actually draw: the live overlay when in Live World, else the + // plain per-cell colours. + const std::vector& displayColors() const { return liveWorld ? shadedColors : vcolors; } void regenWorld(); // after generate(): geometry changed void regen(); // generate(cfg) + regenWorld() void stepOnce(); // one tick + settle bookkeeping @@ -118,6 +141,7 @@ struct Viewer { void renderFrame(); void renderGlobe3D(); void renderMap2D(); + void renderLiveInfo(); // Live World: moon phases + (coastal) tidal phase, beside the 2D map void renderPanels(); void renderHUD(); void renderPrompt(); diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index a71dd67..b0cc487 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -131,6 +131,20 @@ void Viewer::handleInput() { { mode = ColorMode::FloraDensity; recolor(); } setStatus("Biota generated (flora/fauna/funga)"); } + if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock) + liveWorld = !liveWorld; + if (liveWorld) { + phase3Prompt = false; paused = false; + refreshView(); // fresh base colours; overlay builds in stepSim + setStatus("Live World started"); + } else { + paused = true; refreshView(); // back to World Creation (drift), paused + setStatus("Live World stopped"); + } + } + if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator + if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline + if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); } if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); } if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled) @@ -152,7 +166,14 @@ void Viewer::handleInput() { if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH); if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH); if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); } - // Drift speed (Phase 2): My simulated per real second. - if (IsKeyPressed(KEY_RIGHT_BRACKET)) driftRate = std::min(driftRate * 1.5, 80.0); - if (IsKeyPressed(KEY_LEFT_BRACKET)) driftRate = std::max(driftRate / 1.5, 0.5); + // Speed control: Live World ramps the live clock (sim hours/s, ~hour -> month); + // otherwise it sets the drift rate (My simulated per real second). + if (IsKeyPressed(KEY_RIGHT_BRACKET)) { + if (liveWorld) liveRate = std::min(liveRate * 1.5, 720.0); + else driftRate = std::min(driftRate * 1.5, 80.0); + } + if (IsKeyPressed(KEY_LEFT_BRACKET)) { + if (liveWorld) liveRate = std::max(liveRate / 1.5, 0.25); + else driftRate = std::max(driftRate / 1.5, 0.5); + } } diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index d14ab3c..91b8934 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -20,6 +20,7 @@ void Viewer::renderGlobe3D() { rlPushMatrix(); rlRotatef((float)planet.cfg.axialTilt, 0.0f, 0.0f, 1.0f); const std::vector& tri = planet.triIndices(); + const std::vector& dc = displayColors(); // live overlay (day/night + snow) or plain rlBegin(RL_TRIANGLES); for (size_t k = 0; k + 2 < tri.size(); k += 3) { int idx[3] = { tri[k], tri[k + 1], tri[k + 2] }; @@ -27,7 +28,7 @@ void Viewer::renderGlobe3D() { const Cell& cc = planet.cells[idx[j]]; const Vec3& u = cc.unit; float r = visBase + (float)cc.elevation * elevExagg; - const Color& col = vcolors[idx[j]]; + const Color& col = dc[idx[j]]; rlColor4ub(col.r, col.g, col.b, 255); rlVertex3f((float)(u.x * r), (float)(u.y * r), (float)(u.z * r)); } @@ -72,6 +73,36 @@ void Viewer::renderGlobe3D() { }; drawRiv(rivers, 1.5f); drawRiv(bigRivers, 3.0f); } + // Live World tide: colour the coastline by the local tide level (per-segment colour cached + // in coastCols so the 2D map reuses it). Auto-scaled to the current tide extent. + coastCols.clear(); + if (liveWorld && showTides && !coast.empty()) { + const std::vector& td = planet.tide(); + double range = 1e-6; + for (int oc : coastOcean) if (oc >= 0 && oc < (int)td.size()) range = std::max(range, std::fabs(td[oc])); + coastCols.reserve(coastOcean.size()); + for (int oc : coastOcean) + coastCols.push_back((oc >= 0 && oc < (int)td.size()) ? tideColor(td[oc], range) : Color{150,175,185,255}); + rlSetLineWidth(3.0f); rlBegin(RL_LINES); + for (size_t i = 0, c = 0; i + 1 < coast.size(); i += 2, ++c) { + const Color& col = coastCols[c]; + rlColor4ub(col.r, col.g, col.b, 255); + rlVertex3f(coast[i].x, coast[i].y, coast[i].z); + rlVertex3f(coast[i + 1].x, coast[i + 1].y, coast[i + 1].z); + } + rlEnd(); rlSetLineWidth(1.0f); + } + // Ocean currents: warm/cold arrows over the sea (per-segment colour). + if (showCurrents && !currentSegs.empty()) { + rlSetLineWidth(2.0f); rlBegin(RL_LINES); + for (size_t i = 0, c = 0; i + 1 < currentSegs.size(); i += 2, ++c) { + const Color& col = currentCols[c]; + rlColor4ub(col.r, col.g, col.b, 255); + rlVertex3f(currentSegs[i].x, currentSegs[i].y, currentSegs[i].z); + rlVertex3f(currentSegs[i + 1].x, currentSegs[i + 1].y, currentSegs[i + 1].z); + } + rlEnd(); rlSetLineWidth(1.0f); + } if (showGrat) drawGraticule3D(graticule, gratR); // Markers: selected (orange), hovered cell (yellow), hovered subcell (white). if (selectedCell >= 0) { @@ -97,6 +128,57 @@ void Viewer::renderGlobe3D() { DrawSphere(Vector3{0.0f, ax, 0.0f}, 0.05f, Color{230, 90, 80, 255}); // north DrawSphere(Vector3{0.0f, -ax, 0.0f}, 0.05f, Color{80, 140, 230, 255}); // south } + // Live World sky: a small, distant sun (bright core + faint halo) and the orbiting moons + // (sun-lit phase + orbit ring; dimmed reddish during a lunar eclipse). All inside the tilted + // matrix so they stay consistent with the model-space lit pattern. + if (liveWorld) { + // Sun: far away + small, with a couple of translucent halo shells so it still reads. + const float sunDist = 9.0f; + Vector3 sp{ sunDir.x * sunDist, sunDir.y * sunDist, sunDir.z * sunDist }; + DrawSphere(sp, 0.60f, Color{255, 240, 180, 26}); + DrawSphere(sp, 0.34f, Color{255, 238, 170, 55}); + DrawSphere(sp, 0.17f, Color{255, 246, 205, 255}); + + const auto& mns = planet.getMoons(); + for (size_t m = 0; m < mns.size() && m < moonDirs.size(); ++m) { + const Vector3& dir = moonDirs[m]; + float dist = visBase + 0.8f + (float)(mns[m].orbitRadius / 30.0) * 4.0f; // visible band + Vector3 mp{ dir.x * dist, dir.y * dist, dir.z * dist }; + float rr = (float)mns[m].dispRadius; + + // Faint orbit ring: the great circle perpendicular to the orbit-plane normal. + if (m < moonNormals.size()) { + Vec3 nrm = Vec3{moonNormals[m].x, moonNormals[m].y, moonNormals[m].z}.normalized(); + Vec3 u = std::fabs(nrm.y) < 0.9 ? nrm.cross(Vec3{0,1,0}).normalized() + : nrm.cross(Vec3{1,0,0}).normalized(); + Vec3 v = nrm.cross(u); + rlBegin(RL_LINES); rlColor4ub(120, 130, 160, 90); + const int seg = 64; + for (int k = 0; k < seg; ++k) { + double a0 = 2.0 * M_PI * k / seg, a1 = 2.0 * M_PI * (k + 1) / seg; + Vec3 p0 = (u * std::cos(a0) + v * std::sin(a0)) * dist; + Vec3 p1 = (u * std::cos(a1) + v * std::sin(a1)) * dist; + rlVertex3f((float)p0.x, (float)p0.y, (float)p0.z); + rlVertex3f((float)p1.x, (float)p1.y, (float)p1.z); + } + rlEnd(); + } + + // Lunar eclipse: moon near the anti-solar point (in the planet's shadow) -> dim red. + double antiAlign = -(dir.x*sunDir.x + dir.y*sunDir.y + dir.z*sunDir.z); // dot(dir,-sun) + bool eclipsed = antiAlign > std::cos(0.13); + Color lit = eclipsed ? Color{90, 35, 30, 255} : Color{210, 210, 215, 255}; + DrawSphere(mp, rr, lit); + // Phase via the offset-dark-sphere trick: lit fraction k = (1 - cos(phase))/2, with + // cos(phase)=dot(moonDir,sunDir) (new moon when aligned with the sun). Shift a dark + // sphere toward the unlit (anti-sun) side to occlude it; offset 0 = new, ~2r = full. + double cosPhase = dir.x*sunDir.x + dir.y*sunDir.y + dir.z*sunDir.z; + double k = (1.0 - cosPhase) * 0.5; // 0 = new, 1 = full + float off = (float)(k * 2.2 * rr); + Vector3 dp{ mp.x - sunDir.x * off, mp.y - sunDir.y * off, mp.z - sunDir.z * off }; + DrawSphere(dp, rr * 1.02f, Color{12, 12, 16, 255}); + } + } rlPopMatrix(); EndMode3D(); EndTextureMode(); @@ -106,11 +188,13 @@ void Viewer::renderGlobe3D() { void Viewer::renderMap2D() { DrawRectangleRec(mapRect, Color{6, 8, 14, 255}); BeginScissorMode((int)mapRect.x, (int)mapRect.y, (int)mapRect.width, (int)mapRect.height); - drawMap2D(planet, vcolors, map2D, mapRect, mapLon); + drawMap2D(planet, displayColors(), map2D, mapRect, mapLon); if (showGrat) { drawGraticule2D(graticule, mapRect, mapLon); drawGraticuleLabels2D(mapRect, mapLon); } if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f, mapRect, mapLon); if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f, mapRect, mapLon); if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, mapRect, mapLon); + if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, mapRect, mapLon); + if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, mapRect, mapLon); if (phase3 && showRivers) { drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, mapRect, mapLon); drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, mapRect, mapLon); @@ -131,6 +215,95 @@ void Viewer::renderMap2D() { DrawText("2D Equal Earth (hover, drag to pan)", (int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255}); } +// Live World "Sky & tides" panel in the freed space right of the (left-aligned) 2D map: +// the current phase of every moon, and the tidal phase of a selected coastal tile. +void Viewer::renderLiveInfo() { + if (!liveWorld) return; + Rectangle r = liveInfoRect; + DrawRectangleRec(r, Color{10, 12, 20, 235}); + DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255}); + int x = (int)r.x + 14, y = (int)r.y + 10; + DrawText("Sky & tides", x, y, 20, RAYWHITE); y += 30; + + // Sky geometry at the current clock (recomputed here so the panel is self-contained). + const double dayH = planet.cfg.dayLengthHours, yrD = planet.cfg.yearLengthDays; + double days = liveTime / dayH; + double doy = days / yrD; doy -= std::floor(doy); + double tod = days - std::floor(days); + const double dStep = 0.03; // ~ for waxing/waning + rising/falling + double days2 = days + dStep, doy2 = days2 / yrD - std::floor(days2 / yrD), tod2 = days2 - std::floor(days2); + Vec3 sun = planet.sunDirection(doy, tod); + Vec3 sun2 = planet.sunDirection(doy2, tod2); + auto illumFrac = [](const Vec3& moon, const Vec3& s) { return (1.0 - moon.dot(s)) * 0.5; }; + auto phaseName = [](double f, bool wax) -> const char* { + if (f < 0.04) return "New"; + if (f > 0.96) return "Full"; + if (f > 0.46 && f < 0.54) return wax ? "First quarter" : "Last quarter"; + if (f < 0.5) return wax ? "Waxing crescent" : "Waning crescent"; + return wax ? "Waxing gibbous" : "Waning gibbous"; + }; + // A small 2D phase disc: dark circle with the lit fraction filled (terminator ellipse). + auto drawPhase = [](float cx, float cy, float rad, double f, bool wax) { + DrawCircle((int)cx, (int)cy, rad, Color{26, 28, 36, 255}); + double cosphi = 1.0 - 2.0 * f; // terminator x = w * cosphi + for (int dy = -(int)rad; dy <= (int)rad; ++dy) { + double w = std::sqrt(std::max(0.0, (double)rad * rad - (double)dy * dy)); + double xt = w * cosphi, xa, xb; + if (wax) { xa = xt; xb = w; } else { xa = -w; xb = -xt; } + if (xb > xa) DrawLine((int)(cx + xa), (int)(cy + dy), (int)(cx + xb), (int)(cy + dy), Color{226, 226, 232, 255}); + } + DrawCircleLines((int)cx, (int)cy, rad, Color{120, 124, 145, 255}); + }; + + const auto& mns = planet.getMoons(); + for (size_t m = 0; m < mns.size(); ++m) { + Vec3 md = planet.moonDirection((int)m, tod, days); + Vec3 md2 = planet.moonDirection((int)m, tod2, days2); + double f = illumFrac(md, sun); + bool wax = illumFrac(md2, sun2) >= f; + float cy = (float)y + 20.0f; + drawPhase((float)x + 22.0f, cy, 20.0f, f, wax); + DrawText(TextFormat("Moon %d: %s", (int)m + 1, phaseName(f, wax)), x + 52, y + 6, 17, Color{210, 215, 225, 255}); + DrawText(TextFormat("%.0f%% lit period %.0f d", f * 100.0, mns[m].periodDays), x + 52, y + 27, 15, Color{150, 160, 175, 255}); + y += 50; + } + if (mns.empty()) { DrawText("(no moons)", x, y, 16, Color{150, 155, 170, 255}); y += 24; } + + // Tidal phase for a selected coastal tile (placeholder: high/low + rising/falling). + y += 8; + DrawText("Tidal phase", x, y, 18, Color{200, 205, 220, 255}); y += 26; + if (selectedCell >= 0 && selectedCell < (int)planet.cells.size()) { + const Cell& c = planet.cells[selectedCell]; + const double sea = planet.cfg.seaLevel; + bool selLand = c.elevation > sea, coastal = false; + for (int nb : c.neighbors) if ((planet.cells[nb].elevation > sea) != selLand) { coastal = true; break; } + if (coastal) { + // Single-cell tide now vs a step ahead -> rising/falling (the field itself is the + // equilibrium tide; a richer coastal/resonant model is future work). + auto cellTide = [&](double dy, double td, double dd) { + double h = 0.0; + for (int mm = 0; mm < (int)mns.size(); ++mm) { + double cc = c.unit.dot(planet.moonDirection(mm, td, dd)); + h += mns[mm].tideWeight * (cc * cc - 1.0 / 3.0); + } + double cs = c.unit.dot(planet.sunDirection(dy, td)); + h += planet.cfg.tideSunFactor * (cs * cs - 1.0 / 3.0); + return h * planet.cfg.tideAmplitude; + }; + double t0 = cellTide(doy, tod, days), t1 = cellTide(doy2, tod2, days2); + bool rising = t1 >= t0; + DrawText(TextFormat("coastal cell #%d", selectedCell), x, y, 15, Color{160, 170, 185, 255}); y += 21; + DrawText(TextFormat("%+.2f m %s, %s", t0, t0 >= 0.0 ? "high" : "low", rising ? "rising" : "falling"), + x, y, 17, tideColor(t0, std::max(0.05, std::fabs(t0)))); y += 24; + DrawText("(equilibrium model - placeholder)", x, y, 13, Color{120, 125, 140, 255}); + } else { + DrawText("selected tile is inland", x, y, 15, Color{150, 155, 170, 255}); + } + } else { + DrawText("click a coastal tile", x, y, 15, Color{150, 155, 170, 255}); + } +} + // Right column: hover/selection info (top) + detail panel or world stats (bottom). void Viewer::renderPanels() { drawHoverPanel(planet, hoverRect, hovered, selectedCell); @@ -139,7 +312,7 @@ void Viewer::renderPanels() { planet.cells[selectedCell].elevation, planet.cells[selectedCell].geoAge, panelRect, gridRect, hoveredSubIdx); else - drawStats(planet, panelRect, elapsedMy, settled); + drawStats(planet, panelRect, elapsedMy, settled, liveWorld, liveTime); } // Top-left HUD text + the clickable pause button. @@ -154,15 +327,34 @@ void Viewer::renderHUD() { int y = 10; auto line = [&](const std::string& s){ DrawText(s.c_str(), 12, y, 18, RAYWHITE); y += 22; }; double fastest = 0.0; for (const auto& pl : planet.plates) fastest = std::max(fastest, pl.speedCmYr); - line(!settled ? "Planet Sim - World Creation: forming" - : phase3 ? "Planet Sim - World Creation: hydrology" - : "Planet Sim - World Creation: drift & erosion"); + line(liveWorld ? "Planet Sim - Live World" + : !settled ? "Planet Sim - World Creation: forming" + : phase3 ? "Planet Sim - World Creation: hydrology" + : "Planet Sim - World Creation: drift & erosion"); line(TextFormat("Cells: %d Subdiv: %d CellWidth: %.0f km", (int)planet.cells.size(), cfg.subdivisions, planet.cellWidthMeters() / 1000.0)); line(TextFormat("Elevation: %.0f .. %.0f m", minE, maxE)); if (!settled) line(TextFormat("Forming terrain tick %lld max change %.1f m/tick%s", stepCount, maxChange, paused ? " [PAUSED]" : "")); + else if (liveWorld) { + const double dayH = planet.cfg.dayLengthHours, yrD = planet.cfg.yearLengthDays; + double days = liveTime / dayH; + long year = (long)std::floor(days / yrD) + 1; + long doy = (long)std::floor(days - std::floor(days / yrD) * yrD) + 1; + double hod = liveTime - std::floor(days) * dayH; // hours into the current day + int hh = (int)hod, mm = (int)((hod - hh) * 60.0); + line(TextFormat("Live World Year %ld Day %ld %02d:%02d%s", + year, doy, hh, mm, paused ? " [PAUSED]" : "")); + const double weekH = 7.0 * dayH, monthH = 30.0 * dayH; + const char* rl; double rv; + if (liveRate >= monthH) { rl = "mo/s"; rv = liveRate / monthH; } + else if (liveRate >= weekH) { rl = "wk/s"; rv = liveRate / weekH; } + else if (liveRate >= dayH) { rl = "d/s"; rv = liveRate / dayH; } + else { rl = "h/s"; rv = liveRate; } + line(TextFormat("rate %.1f %s day/night %s ([ / ] speed, N toggle, W exit)", + rv, rl, dayNightOn ? "on" : "off")); + } else { line(TextFormat("%s %.1f My elapsed %.1f My/s%s", phase3 ? "Hydrology - drift, rivers & erosion" : "Drift & erosion", @@ -180,10 +372,10 @@ 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 (*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 | +/-", - phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off")); + line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]", + showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off")); + line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-", + phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off")); line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg"); if (!statusMsg.empty() && GetTime() < statusUntil) { y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22; @@ -255,6 +447,7 @@ void Viewer::renderFrame() { } renderMap2D(); + renderLiveInfo(); renderPanels(); renderHUD(); renderPrompt(); diff --git a/src/sim/Planet.cpp b/src/sim/Planet.cpp index b922736..15abcb8 100644 --- a/src/sim/Planet.cpp +++ b/src/sim/Planet.cpp @@ -33,6 +33,7 @@ void Planet::generate(const PlanetConfig& c) { assignPlates(); seedInitialRelief(); + generateMoons(); // Live World satellites (separate RNG; does not perturb tectonics) computeClimate(); // temperature + precipitation fields (biomes read these) classifyBiomes(); // give the fresh world an initial biome per cell computeBiotaDensity(); // derived flora/fauna/funga density (population is on-demand) diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index a75b378..b58e5a6 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -14,6 +14,7 @@ public: PlanetConfig cfg; std::vector cells; std::vector plates; + std::vector moons; // Live World: 1-3 natural satellites (generated + saved) // Phase flag: false during Phase-1 forming (modest, original tectonics that // settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts @@ -57,6 +58,37 @@ public: const std::vector& summerTemp() const { return sTempSummer; } // deg C, warmest month const std::vector& winterTemp() const { return sTempWinter; } // deg C, coldest month + // Live World stage (PlanetLive.cpp): the slow real-time clock's derived fields. + // computeInsolation() = instantaneous solar incidence cos(sun angle), 0..1 -- the + // physical foundation for live weather + the day/night terminator. computeLiveSeason() + // = the live temperature cycling between winterTemp/summerTemp over the year (drives the + // live temperature view + the moving snow line). Both derived/not-saved; call after + // computeClimate(). dayOfYear01/timeOfDay01 are fractions in [0,1). + void computeInsolation(double dayOfYear01, double timeOfDay01); + void computeLiveSeason(double dayOfYear01); + const std::vector& insolation() const { return sInsolation; } // 0..1 cos incidence + const std::vector& liveTemp() const { return sLiveTemp; } // deg C, current season + + // Live World sky + oceans (PlanetOcean.cpp). Sub-solar / sub-lunar directions in model + // space at a clock fraction (drive insolation, tides and the 3D sun/moons -- one source of + // truth). generateMoons() seeds 1-3 moons from a separate RNG (tectonic determinism intact). + Vec3 sunDirection(double dayOfYear01, double timeOfDay01) const; + Vec3 moonDirection(int moonIdx, double timeOfDay01, double timeDays) const; + Vec3 moonOrbitNormal(int moonIdx, double timeOfDay01) const; // orbit-plane normal (for the ring) + const std::vector& getMoons() const { return moons; } + void generateMoons(); + // Tides: equilibrium tidal height (m) per cell from the moons + sun at the given clock. + // Derived/not saved; recomputed each frame like insolation. + void computeTides(double dayOfYear01, double timeOfDay01, double timeDays); + const std::vector& tide() const { return sTide; } + + // Ocean surface currents (PlanetOcean.cpp): a per-ocean-cell tangent velocity from wind + // stress + Coriolis deflection + coast-following (gyres). Derived/not saved; needs sWind + // (computeClimate() computes it, then calls this and feeds warm/cold currents back into + // sTemp -- see climateCurrentFactor). Zero on land cells. + void computeOceanCurrents(); + const std::vector& current() const { return sCurrent; } + // Phase 3 (biomes): classify every cell into a Biome from elevation + the climate // fields (temperature + normalized precipitation). Derived + written back into // cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves. @@ -91,7 +123,9 @@ public: // hasBiome: whether the stream carries the per-cell biome byte (save v4+). For // older saves (v3) pass false -- biomes are reclassified after the cells load. // hasBiota: whether the stream carries the biota population block (save v7+). - bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true); + // hasMoons: whether the stream carries the moons block (save v9+); older saves + // synthesize moons from the seed instead. + bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true, bool hasMoons = true); // Helpers for rendering / info. double cellWidthMeters() const; // approx lateral cell spacing @@ -160,6 +194,12 @@ private: std::vector sWind; std::vector sUpwind; + // Live World scratch (derived each frame in Live World; not saved). sInsolation is the + // instantaneous solar incidence; sLiveTemp is the temperature for the current day-of-year; + // sTide is the equilibrium tidal height (m) from the moons + sun. + std::vector sInsolation, sLiveTemp, sTide; + std::vector sCurrent; // ocean surface current velocity (tangent; zero on land) + // Biota: derived density scalars (0..1; recomputed each tick, not saved) and the // on-demand discrete population (saved). sHasBiota latches once generated/loaded. std::vector sFloraDensity, sFaunaDensity, sFungaDensity; diff --git a/src/sim/PlanetClimate.cpp b/src/sim/PlanetClimate.cpp index 4b4cecb..57dc546 100644 --- a/src/sim/PlanetClimate.cpp +++ b/src/sim/PlanetClimate.cpp @@ -63,6 +63,38 @@ void Planet::computeClimate() { sUpwind[i] = best; } + // 1b. Ocean currents + their temperature feedback. Currents need the wind just computed. + // Warm (poleward-flowing) currents carry equatorial heat toward the poles and cold + // (equatorward) currents the reverse; this raises/lowers coastal temperatures. We build a + // bounded anomaly on ocean cells (sign = poleward speed) and smooth it onto the coasts. + computeOceanCurrents(); + if (cfg.climateCurrentFactor > 0.0 && (int)sCurrent.size() == n) { + double maxSp = 1e-9; + for (int i = 0; i < n; ++i) maxSp = std::max(maxSp, sCurrent[i].length()); + std::vector dT(n, 0.0); + for (int i = 0; i < n; ++i) { + if (cells[i].elevation > sea) continue; // ocean cells only + const Vec3& nrm = cells[i].unit; + Vec3 northT = up - nrm * up.dot(nrm); // tangent toward +latitude + double nl = northT.length(); if (nl < 1e-9) continue; + northT = northT * (1.0 / nl); + double sign = (nrm.y >= 0.0) ? 1.0 : -1.0; // toward the nearer pole + double poleward = sCurrent[i].dot(northT) * sign / maxSp; // -1..1 + dT[i] = cfg.climateCurrentFactor * std::clamp(poleward, -1.0, 1.0); + } + // Spread the anomaly onto coasts and let it decay inland (neighbour averaging). + std::vector tmp(n); + for (int p = 0; p < 3; ++p) { + for (int i = 0; i < n; ++i) { + double s = dT[i]; int c = 1; + for (int nb : cells[i].neighbors) { s += dT[nb]; ++c; } + tmp[i] = s / c; + } + dT.swap(tmp); + } + for (int i = 0; i < n; ++i) sTemp[i] += dT[i]; + } + // 2. Steady-state moisture advection along the wind (iterative upwind differencing, // double-buffered -> deterministic). Ocean cells are a moisture source; land // cells take their upwind moisture, rain part of it out (more on windward upslopes) diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index e94463f..2cc8708 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -29,11 +29,13 @@ D(biomeWetlandMoist) D(biomeDesertMoist) D(biomeGrassMoist) D(biomeTaigaMoist) \ D(biomeLakeMinDepth) D(biomeSeasonWeight) \ D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \ - D(climateOroRefHeight) D(climateContinentality) \ + D(climateOroRefHeight) D(climateContinentality) D(climateCurrentFactor) \ 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) \ + D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \ + D(tideAmplitude) D(tideSunFactor) \ I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \ I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \ I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) \ @@ -169,6 +171,7 @@ std::string validateConfig(const PlanetConfig& cfg) { 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.climateCurrentFactor, 0.0, 30.0, "climateCurrentFactor")); 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")); @@ -182,6 +185,12 @@ std::string validateConfig(const PlanetConfig& cfg) { E(rng(cfg.bioFungaFloraWeight, 0.0, 1.0, "bioFungaFloraWeight")); E(rng(cfg.bioFungaTempMin, -50.0, 20.0, "bioFungaTempMin")); E(rng(cfg.bioRegionBonus, 0.0, 10.0, "bioRegionBonus")); + E(rng(cfg.dayLengthHours, 0.1, 1.0e5, "dayLengthHours")); + E(rng(cfg.yearLengthDays, 1.0, 1.0e7, "yearLengthDays")); + E(rng(cfg.snowTemp, -60.0, 30.0, "snowTemp")); + E(rng(cfg.seaIceTemp, -60.0, 20.0, "seaIceTemp")); + E(rng(cfg.tideAmplitude, 0.0, 100.0, "tideAmplitude")); + E(rng(cfg.tideSunFactor, 0.0, 5.0, "tideSunFactor")); E(irng(cfg.subdivisions, 0, 7, "subdivisions")); E(irng(cfg.plateCount, 1, 100, "plateCount")); E(irng(cfg.beltWidth, 1, 12, "beltWidth")); @@ -260,6 +269,7 @@ void Planet::writeState(std::ostream& os) const { writeVec(os, sPrevCount); writeVec(os, sStaleStreak); writeVec(os, sFreePlateIds); + writeVec(os, moons); // v9: natural satellites (Live World) // v7: discrete biota population (sBiota). A flag byte gates the block so a // not-yet-populated world stays compact; otherwise three Organism lists per cell. uint8_t hasBio = sHasBiota ? 1 : 0; writePod(os, hasBio); @@ -271,7 +281,7 @@ void Planet::writeState(std::ostream& os) const { } } -bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota) { +bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons) { // Read the length-prefixed key=value config block (see writeState). A default // PlanetConfig is parsed over, so fields absent from an older save keep their // current defaults. The length guard rejects pre-v6 (raw-POD-config) saves. @@ -300,6 +310,8 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota) { readVec(is, sPrevCount); readVec(is, sStaleStreak); readVec(is, sFreePlateIds); + if (hasMoons) readVec(is, moons); // v9: natural satellites + else generateMoons(); // pre-v9 save: synthesize moons from the seed if (!hasBiome) classifyBiomes(); // old (v3) save: reclassify from loaded state // v7: discrete biota population. buildGeometry() already sized sBiota empty; // older saves (hasBiota=false) just keep the empty population (press L to fill). diff --git a/src/sim/PlanetLive.cpp b/src/sim/PlanetLive.cpp new file mode 100644 index 0000000..5100da9 --- /dev/null +++ b/src/sim/PlanetLive.cpp @@ -0,0 +1,52 @@ +#include "Planet.hpp" +#include "Projection.hpp" // lonLatToDir, dirToLonLat +#include + +// Live World stage: the slow real-time clock (hours -> weeks/months). Geometry is +// fixed, so -- like climate -- these are derived per-cell fields flowed over the grid, +// recomputed each frame and never saved. They are the foundation the future weather +// simulation will read. +// +// Sun geometry is computed in MODEL space (the fixed cell unit vectors). The viewer +// renders the globe leaned by axialTilt about world Z, but since the lit pattern is +// baked per cell in model space it rotates with the globe automatically; the seasonal +// lean of day/night is carried by the solar declination below, not the render tilt. +// The 2D Equal Earth map also works in model-space lon/lat, so both views stay +// consistent without any tilt bookkeeping here. + +// Instantaneous solar incidence cos(theta) in [0,1] per cell. +// declination delta = axialTilt * sin(2*pi * dayOfYear01) (0 at equinox, +/-tilt at solstice) +// sub-solar lon lambda sweeps the globe once per day (westward as the planet spins east) +// sunDir = lonLatToDir(lambda, delta); incidence = max(0, cell.unit . sunDir) +void Planet::computeInsolation(double dayOfYear01, double timeOfDay01) { + const int n = (int)cells.size(); + sInsolation.assign(n, 0.0); + + const Vec3 sun = sunDirection(dayOfYear01, timeOfDay01); // shared sky geometry (PlanetOcean.cpp) + + for (int i = 0; i < n; ++i) { + double c = cells[i].unit.dot(sun); + sInsolation[i] = c > 0.0 ? c : 0.0; + } +} + +// Live temperature for the current day-of-year: the annual mean sTemp swung toward the +// summer or winter extreme by the seasonal phase, opposite across the two hemispheres. +// g = sin(2*pi * dayOfYear01) * sign(lat) in [-1,1] (+1 = this hemisphere's summer) +// liveTemp = mean + A * g, A = (summerTemp - winterTemp)/2 (the seasonal half-amplitude) +// At g=+1 -> summer, g=-1 -> winter, equator (g~0) -> mean. Requires computeClimate() first. +void Planet::computeLiveSeason(double dayOfYear01) { + const int n = (int)cells.size(); + sLiveTemp.assign(n, 0.0); + if ((int)sTemp.size() != n) return; // no climate yet + const bool haveSeason = ((int)sTempSummer.size() == n && (int)sTempWinter.size() == n); + + const double season = std::sin(2.0 * M_PI * dayOfYear01); + for (int i = 0; i < n; ++i) { + double mean = sTemp[i]; + if (!haveSeason) { sLiveTemp[i] = mean; continue; } + double A = 0.5 * (sTempSummer[i] - sTempWinter[i]); // >= 0 + double g = season * (cells[i].unit.y >= 0.0 ? 1.0 : -1.0); + sLiveTemp[i] = mean + A * g; + } +} diff --git a/src/sim/PlanetOcean.cpp b/src/sim/PlanetOcean.cpp new file mode 100644 index 0000000..0c2f372 --- /dev/null +++ b/src/sim/PlanetOcean.cpp @@ -0,0 +1,152 @@ +#include "Planet.hpp" +#include "Projection.hpp" // lonLatToDir +#include +#include +#include +#include + +// Live World sky & oceans. Geometry is fixed, so the moons are world objects (not cells) and +// the tide is a derived per-cell field flowed over the grid -- both animated by the live clock. +// Stage 1: moon generation, sub-solar/sub-lunar directions (one source of truth, shared by +// insolation/tides/render), and the equilibrium tide. (Stage 2 adds ocean currents here.) + +namespace { + // Rotations about the model axes (Y = north/spin axis). rotateY by a advances longitude by + // a: lonLatToDir(lon,lat) rotated by a -> lonLatToDir(lon+a,lat). + inline Vec3 rotateX(const Vec3& v, double a) { + double c = std::cos(a), s = std::sin(a); + return Vec3{ v.x, c * v.y - s * v.z, s * v.y + c * v.z }; + } + inline Vec3 rotateY(const Vec3& v, double a) { + double c = std::cos(a), s = std::sin(a); + return Vec3{ c * v.x - s * v.z, v.y, s * v.x + c * v.z }; + } +} + +// 1-3 satellites from a SEPARATE RNG (seeded from cfg.seed) so generating moons never touches +// the tectonic stream (rngState) -- mirrors the biota RNG isolation. +void Planet::generateMoons() { + moons.clear(); + uint32_t s = cfg.seed ? (cfg.seed ^ 0x900D5EEDu) : 0x900D5EEDu; + auto next = [&]() { s ^= s << 13; s ^= s >> 17; s ^= s << 5; return s; }; + auto rf = [&]() { return (next() & 0xFFFFFFu) / double(0x1000000); }; + int count = 1 + (int)(next() % 3u); // 1..3 + for (int i = 0; i < count; ++i) { + Moon m; + m.orbitRadius = 8.0 + rf() * 22.0; // 8..30 planet radii (render scale) + m.periodDays = 6.0 + rf() * 54.0; // 6..60 days + m.phase = rf() * 2.0 * M_PI; + m.inclination = (rf() * 2.0 - 1.0) * (30.0 * M_PI / 180.0); // +/-30 deg + m.tideWeight = 0.3 + rf() * 0.9; // 0.3..1.2 (Moon mass proxy) + m.dispRadius = 0.05 + m.tideWeight * 0.06; // visual size ~ mass + moons.push_back(m); + } +} + +// Sub-solar direction (model space). Celestial dir at a reference longitude, leaned by the +// seasonal declination, then spun by the planet's rotation (-2*pi*timeOfDay). Equivalent to the +// old lonLatToDir(pi*(1-2t), decl), so day/night is unchanged. +Vec3 Planet::sunDirection(double dayOfYear01, double timeOfDay01) const { + double decl = cfg.axialTilt * M_PI / 180.0 * std::sin(2.0 * M_PI * dayOfYear01); + Vec3 C = lonLatToDir(M_PI, decl); + return rotateY(C, -2.0 * M_PI * timeOfDay01); +} + +// Sub-lunar direction (model space): the moon orbits in an inclined plane, then the planet spin +// sweeps it across the sky (so a fixed cell sees ~one lunar pass per day). +Vec3 Planet::moonDirection(int moonIdx, double timeOfDay01, double timeDays) const { + const Moon& m = moons[moonIdx]; + double Om = 2.0 * M_PI * timeDays / std::max(1e-9, m.periodDays) + m.phase; + Vec3 eq{ std::cos(Om), 0.0, std::sin(Om) }; // equatorial orbit circle + Vec3 C = rotateX(eq, m.inclination); // incline the orbit plane + return rotateY(C, -2.0 * M_PI * timeOfDay01); // planet spin +} + +// Ocean surface currents: a per-ocean-cell tangent velocity. Wind drags the surface, Coriolis +// deflects it (right in the N hemisphere, left in the S), and coasts block the across-shore +// component so the flow turns to follow the shore -- which closes into gyres. A few smoothing +// passes make the field coherent. Approximate at 223 km/cell, but it gives plausible streams + +// the warm/cold pattern the climate feedback reads. Needs sWind (computeClimate sets it). +void Planet::computeOceanCurrents() { + const int n = (int)cells.size(); + sCurrent.assign(n, Vec3{0, 0, 0}); + if ((int)sWind.size() != n) return; + const double sea = cfg.seaLevel; + auto isOcean = [&](int i) { return cells[i].elevation <= sea; }; + + // 1. Wind-driven base velocity, rotated by a Coriolis deflection about the local normal. + const double deflect = 35.0 * M_PI / 180.0; + std::vector v(n, Vec3{0, 0, 0}); + for (int i = 0; i < n; ++i) { + if (!isOcean(i)) continue; + const Vec3& w = sWind[i]; + if (w.length() < 1e-9) continue; + const Vec3& nrm = cells[i].unit; + double lat = std::asin(std::clamp(nrm.y, -1.0, 1.0)); + double th = deflect * (lat >= 0.0 ? -1.0 : 1.0); // deflect right (N) / left (S) + v[i] = w * std::cos(th) + nrm.cross(w) * std::sin(th); // rotate w about the normal + } + + // 2. Coast deflection: remove any component flowing INTO a land neighbour, so the stream + // bends to run along the coast (the seed of gyre circulation). + for (int i = 0; i < n; ++i) { + if (!isOcean(i) || v[i].length() < 1e-12) continue; + const Vec3& nrm = cells[i].unit; + for (int nb : cells[i].neighbors) { + if (isOcean(nb)) continue; + Vec3 d = cells[nb].unit - cells[i].unit; + d = d - nrm * d.dot(nrm); // tangent direction to the land + double dl = d.length(); if (dl < 1e-12) continue; + d = d * (1.0 / dl); + double into = v[i].dot(d); + if (into > 0.0) v[i] = v[i] - d * into; + } + } + + // 3. Smooth over the ocean (neighbour average, re-projected to the tangent plane). + std::vector tmp(n); + for (int p = 0; p < 3; ++p) { + for (int i = 0; i < n; ++i) { + if (!isOcean(i)) { tmp[i] = Vec3{0, 0, 0}; continue; } + Vec3 s = v[i]; int c = 1; + for (int nb : cells[i].neighbors) if (isOcean(nb)) { s = s + v[nb]; ++c; } + Vec3 a = s * (1.0 / c); + const Vec3& nrm = cells[i].unit; + tmp[i] = a - nrm * a.dot(nrm); // re-tangent + } + v.swap(tmp); + } + sCurrent.swap(v); +} + +// Orbit-plane normal (model space) for moon i at the current spin -- the orbit is the great +// circle perpendicular to this, used to draw the faint orbit ring. +Vec3 Planet::moonOrbitNormal(int moonIdx, double timeOfDay01) const { + Vec3 nrm = rotateX(Vec3{ 0.0, 1.0, 0.0 }, moons[moonIdx].inclination); + return rotateY(nrm, -2.0 * M_PI * timeOfDay01); +} + +// Equilibrium tide (meters) per cell from every tide-raising body (moons + sun). Each body +// raises two bulges -- toward it and its antipode -- via the (cos^2 - 1/3) tidal potential, so +// the field is zero-mean and a cell rises/falls ~twice a day as the bulges sweep past. +void Planet::computeTides(double dayOfYear01, double timeOfDay01, double timeDays) { + const int n = (int)cells.size(); + sTide.assign(n, 0.0); + + struct Body { Vec3 dir; double w; }; + std::vector bodies; + bodies.reserve(moons.size() + 1); + for (int i = 0; i < (int)moons.size(); ++i) + bodies.push_back({ moonDirection(i, timeOfDay01, timeDays), moons[i].tideWeight }); + bodies.push_back({ sunDirection(dayOfYear01, timeOfDay01), cfg.tideSunFactor }); + + const double amp = cfg.tideAmplitude; + for (int i = 0; i < n; ++i) { + double h = 0.0; + for (const auto& b : bodies) { + double c = cells[i].unit.dot(b.dir); + h += b.w * (c * c - 1.0 / 3.0); + } + sTide[i] = amp * h; + } +} diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index a1bda04..4c971d5 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -35,6 +35,20 @@ enum class Biome : uint8_t { Desert, Forest, Taiga, Tundra, Hills, Mountains }; +// A natural satellite (Live World). Geometry is fixed, so the moon is a world object +// (not a cell): it orbits on the live clock, raises tides, and renders as a small sphere. +// Generated 1-3 per world from a separate RNG (so it never perturbs tectonic determinism) +// and saved (v9). orbitRadius is in planet radii (render scale); tideWeight is the tide- +// raising mass proxy; inclination tilts the orbit plane off the equator. +struct Moon { + double orbitRadius = 16.0; // planet radii (visual orbit distance) + double periodDays = 20.0; // orbital period (planetary days) + double phase = 0.0; // orbital phase offset (radians) + double inclination = 0.0; // orbit-plane tilt off the equator (radians) + double tideWeight = 1.0; // tide-raising strength (Moon mass proxy) + double dispRadius = 0.10; // display sphere radius (visual size) +}; + struct Plate { int id = 0; PlateType type = PlateType::Oceanic; // initial crust type seeded onto cells @@ -200,6 +214,8 @@ struct PlanetConfig { double climateContinentality = 0.05; // moisture lost per land cell crossed (dries interiors) int climateWindPasses = 50; // moisture-advection iterations (steady state) int climateMoistureSmooth = 12; // precipitation diffusion passes (wet/dry transition zones) + double climateCurrentFactor = 4.0; // C: max coastal warming/cooling from ocean currents + // (warm poleward currents raise, cold equatorward lower) // --- Seasons (obliquity) -- see PlanetClimate.cpp ----------------------- // axialTilt (above) drives a per-cell seasonal temperature range around the annual @@ -230,4 +246,15 @@ struct PlanetConfig { int bioFloraPoints = 20; // flora point budget at full density (scaled by density) int bioFaunaPoints = 16; // fauna point budget at full density int bioFungaPoints = 14; // funga point budget at full density + + // --- Live World (slow real-time clock) -- see PlanetLive.cpp ------------- + // The finished planet can run on a slow real-time clock (hours -> weeks/months) with a + // moving day/night terminator, a live seasonal temperature cycle and a moving snow line. + // dayLengthHours/yearLengthDays set the calendar; snowTemp/seaIceTemp the freezing lines. + double dayLengthHours = 24.0; // hours in one planetary day (rotation -> day/night) + double yearLengthDays = 365.25; // days in one planetary year (orbit -> seasons) + double snowTemp = 0.0; // C: land below the live temperature shows snow + double seaIceTemp = -2.0; // C: ocean below the live temperature shows sea ice + double tideAmplitude = 0.6; // m: equilibrium-tide scale per unit tide-raising weight + double tideSunFactor = 0.46; // sun's tide weight relative to a unit moon (Earth ~0.46) }; diff --git a/test_live.cpp b/test_live.cpp new file mode 100644 index 0000000..c67091b --- /dev/null +++ b/test_live.cpp @@ -0,0 +1,137 @@ +// Headless test for the Live World stage (insolation + live seasonal temperature). +// No display / raylib needed. +// +// g++ -std=c++17 -O2 -Isrc/sim test_live.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \ +// src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tl && /tmp/tl +// +// Verifies: insolation range; the sub-solar hemisphere is lit and the night side dark; +// the solar declination tracks axialTilt (poles lit/dark at solstice, neutral at equinox); +// live temperature stays within the summer/winter band and is anti-phased across the +// hemispheres; the snow line advances in the winter hemisphere; and determinism. + +#include "Planet.hpp" +#include "Projection.hpp" +#include +#include +#include + +static int failures = 0; +static void check(bool cond, const char* what) { + std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what); + if (!cond) ++failures; +} + +// Index of the cell whose unit direction is closest to dir. +static int nearestCell(const Planet& p, const Vec3& dir) { + int best = 0; double bd = -2.0; + for (int i = 0; i < (int)p.cells.size(); ++i) { + double d = p.cells[i].unit.dot(dir); + if (d > bd) { bd = d; best = i; } + } + return best; +} + +int main() { + Planet planet; + PlanetConfig cfg; + cfg.subdivisions = 5; + cfg.seed = 1337; + planet.generate(cfg); + planet.computeClimate(); // live season needs the climate fields + const int n = (int)planet.cells.size(); + + // North/south pole cells (extreme latitude) for declination checks. + int north = 0, south = 0; + for (int i = 0; i < n; ++i) { + if (planet.cells[i].unit.y > planet.cells[north].unit.y) north = i; + if (planet.cells[i].unit.y < planet.cells[south].unit.y) south = i; + } + const double tilt = cfg.axialTilt * M_PI / 180.0; + + std::printf("Live World: insolation\n"); + // --- Range + lit/dark hemispheres (arbitrary time, summer solstice) ------- + planet.computeInsolation(0.25, 0.30); + const std::vector& sun = planet.insolation(); + bool inRange = true; int lit = 0; double mx = 0.0; + for (int i = 0; i < n; ++i) { + if (sun[i] < 0.0 || sun[i] > 1.0) inRange = false; + if (sun[i] > 0.0) ++lit; + mx = std::max(mx, sun[i]); + } + check(inRange, "insolation in [0,1]"); + check(lit > (int)(0.40 * n) && lit < (int)(0.60 * n), "about half the planet is in daylight"); + // Sub-solar cell is fully lit; its antipode is dark. + double decl = tilt * std::sin(2.0 * M_PI * 0.25); + double lon = M_PI * (1.0 - 2.0 * 0.30); + Vec3 sd = lonLatToDir(lon, decl); + int subsolar = nearestCell(planet, sd); + int antipode = nearestCell(planet, sd * -1.0); + check(sun[subsolar] > 0.99, "sub-solar cell is fully lit"); + check(sun[antipode] == 0.0, "antipodal (midnight) cell is dark"); + + std::printf("Live World: declination tracks axial tilt\n"); + // Equinox (decl=0): both poles near the terminator (~0). Solstice: summer pole lit, + // winter pole in polar night. + planet.computeInsolation(0.0, 0.0); // equinox + double npEq = planet.insolation()[north], spEq = planet.insolation()[south]; + planet.computeInsolation(0.25, 0.0); // northern summer solstice + double npSol = planet.insolation()[north], spSol = planet.insolation()[south]; + check(npEq < 0.1 && spEq < 0.1, "equinox: both poles near the terminator"); + check(npSol > 0.3, "summer solstice: summer pole sees the midnight sun"); + check(spSol == 0.0, "summer solstice: winter pole is in polar night"); + check(std::fabs(npSol - std::sin(tilt)) < 0.05, "polar insolation ~ sin(axialTilt)"); + + std::printf("Live World: seasonal temperature\n"); + // At northern summer (doy=0.25) live temp = summer in the north, winter in the south; + // everywhere it stays within the [winter, summer] band. + planet.computeLiveSeason(0.25); + const std::vector& lt = planet.liveTemp(); + const std::vector& summ = planet.summerTemp(); + const std::vector& wint = planet.winterTemp(); + bool inBand = true; double nLiveSummer = 0.0; int nN = 0; + for (int i = 0; i < n; ++i) { + if (lt[i] < wint[i] - 1e-6 || lt[i] > summ[i] + 1e-6) inBand = false; + if (planet.cells[i].unit.y > 0.3) { nLiveSummer += lt[i]; ++nN; } + } + nLiveSummer /= std::max(1, nN); + check(inBand, "live temp within [winter, summer] for every cell"); + check(std::fabs(lt[north] - summ[north]) < 0.5, "northern summer solstice -> north at its summer temp"); + check(std::fabs(lt[south] - wint[south]) < 0.5, "northern summer solstice -> south at its winter temp"); + // Half a year later the northern hemisphere is colder (anti-phase). + planet.computeLiveSeason(0.75); + double nLiveWinter = 0.0; nN = 0; + for (int i = 0; i < n; ++i) + if (planet.cells[i].unit.y > 0.3) { nLiveWinter += planet.liveTemp()[i]; ++nN; } + nLiveWinter /= std::max(1, nN); + check(nLiveWinter < nLiveSummer - 1.0, "northern hemisphere colder in its winter than its summer"); + + std::printf("Live World: snow line advances in winter\n"); + auto snowCountNorth = [&](double doy) { + planet.computeLiveSeason(doy); const std::vector& t = planet.liveTemp(); + int c = 0; + for (int i = 0; i < n; ++i) + if (planet.cells[i].unit.y > 0.0 && planet.cells[i].elevation > cfg.seaLevel + && t[i] < cfg.snowTemp) ++c; + return c; + }; + int snowSummer = snowCountNorth(0.25), snowWinter = snowCountNorth(0.75); + std::printf(" north land snow cells: summer %d, winter %d\n", snowSummer, snowWinter); + check(snowWinter > snowSummer, "more northern land under snow in winter than summer"); + + std::printf("Live World: determinism\n"); + Planet p2; p2.generate(cfg); p2.computeClimate(); + p2.computeInsolation(0.37, 0.61); p2.computeLiveSeason(0.37); + planet.computeInsolation(0.37, 0.61); planet.computeLiveSeason(0.37); + bool same = true; + for (int i = 0; i < n; ++i) + if (p2.insolation()[i] != planet.insolation()[i] || p2.liveTemp()[i] != planet.liveTemp()[i]) + same = false; + check(same, "same seed + time -> identical insolation & live temp"); + + std::printf(failures ? "\nSOME LIVE CHECKS FAILED (%d)\n" : "\nALL LIVE CHECKS PASSED\n", failures); + return failures ? 1 : 0; +} diff --git a/test_ocean.cpp b/test_ocean.cpp new file mode 100644 index 0000000..b01ada7 --- /dev/null +++ b/test_ocean.cpp @@ -0,0 +1,144 @@ +// Headless test for the Live World ocean/sky stage (moons + tides). No display needed. +// +// g++ -std=c++17 -O2 -Isrc/sim test_ocean.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \ +// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \ +// -o /tmp/to && /tmp/to +// +// Verifies: 1-3 moons generated deterministically; moon/sun directions are unit vectors that +// sweep with the clock; the equilibrium tide is a zero-mean two-bulge field (high under a body +// and its antipode, low at 90 deg); and save v9 round-trips the moons. + +#include "Planet.hpp" +#include "Projection.hpp" +#include +#include +#include +#include + +static int failures = 0; +static void check(bool cond, const char* what) { + std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what); + if (!cond) ++failures; +} +static int nearestCell(const Planet& p, const Vec3& dir) { + int best = 0; double bd = -2.0; + for (int i = 0; i < (int)p.cells.size(); ++i) { + double d = p.cells[i].unit.dot(dir); + if (d > bd) { bd = d; best = i; } + } + return best; +} + +int main() { + PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 1337; + Planet planet; planet.generate(cfg); + const int n = (int)planet.cells.size(); + + std::printf("Ocean: moons\n"); + int nm = (int)planet.getMoons().size(); + std::printf(" moon count: %d\n", nm); + check(nm >= 1 && nm <= 3, "1-3 moons generated"); + + // Determinism: same seed -> identical moons (and tectonic state unchanged by moon RNG). + Planet p2; p2.generate(cfg); + bool sameMoons = ((int)p2.getMoons().size() == nm); + for (int i = 0; i < nm && sameMoons; ++i) { + const Moon& a = planet.getMoons()[i]; const Moon& b = p2.getMoons()[i]; + if (a.orbitRadius != b.orbitRadius || a.periodDays != b.periodDays || a.phase != b.phase + || a.inclination != b.inclination || a.tideWeight != b.tideWeight) sameMoons = false; + } + check(sameMoons, "moon generation is deterministic for a seed"); + bool sameCells = true; + for (int i = 0; i < n; ++i) if (planet.cells[i].elevation != p2.cells[i].elevation) sameCells = false; + check(sameCells, "moon RNG does not perturb tectonic determinism"); + + std::printf("Ocean: sky directions\n"); + Vec3 s0 = planet.sunDirection(0.25, 0.10), s1 = planet.sunDirection(0.25, 0.60); + check(std::fabs(s0.length() - 1.0) < 1e-9, "sun direction is a unit vector"); + check((s0 - s1).length() > 0.1, "sun sweeps as the day advances (time of day)"); + if (nm > 0) { + Vec3 m0 = planet.moonDirection(0, 0.10, 3.0), m1 = planet.moonDirection(0, 0.60, 3.0); + check(std::fabs(m0.length() - 1.0) < 1e-9, "moon direction is a unit vector"); + check((m0 - m1).length() > 0.1, "moon sweeps across the sky with the clock"); + } + + std::printf("Ocean: tides (single equatorial moon, sun off)\n"); + // Isolate one equatorial moon so the two-bulge shape is unambiguous. + Planet t; t.generate(cfg); + t.cfg.tideSunFactor = 0.0; + t.moons.clear(); + Moon m; m.orbitRadius = 20; m.periodDays = 27; m.phase = 0; m.inclination = 0; m.tideWeight = 1; m.dispRadius = 0.1; + t.moons.push_back(m); + t.computeTides(0.0, 0.25, 5.0); + const std::vector tide = t.tide(); // copy (t.tide() is overwritten on recompute) + double sum = 0.0, mx = -1e30, mn = 1e30; + for (int i = 0; i < n; ++i) { sum += tide[i]; mx = std::max(mx, tide[i]); mn = std::min(mn, tide[i]); } + double mean = sum / n; + check(std::fabs(mean) < 0.02, "tide field is ~zero-mean"); + check(mx > 0.0 && mn < 0.0, "tide has highs and lows (bulges + troughs)"); + Vec3 md = t.moonDirection(0, 0.25, 5.0); + int sub = nearestCell(t, md); + int anti = nearestCell(t, md * -1.0); + int pole = nearestCell(t, Vec3{0, 1, 0}); // 90 deg from an equatorial moon + check(tide[sub] > 0.0 && tide[anti] > 0.0, "high tide under the moon AND its antipode"); + check(tide[pole] < 0.0, "low tide at 90 degrees from the moon"); + + // The bulge moves with the clock. (Use a quarter-ish step, not 0.5: half a day apart the + // moon is at the antipode and the tide -- being cos^2 symmetric -- is correctly identical.) + t.computeTides(0.0, 0.40, 5.0); + bool moved = false; + for (int i = 0; i < n; ++i) if (std::fabs(t.tide()[i] - tide[i]) > 1e-6) { moved = true; break; } + check(moved, "the tidal bulge moves as time of day advances"); + + std::printf("Ocean: currents\n"); + const std::vector& cur = planet.current(); // populated by generate()'s computeClimate + check((int)cur.size() == n, "current field sized to the grid"); + bool tangent = true, landZero = true; int flowing = 0; + for (int i = 0; i < n; ++i) { + if (planet.cells[i].elevation <= cfg.seaLevel) { + if (std::fabs(cur[i].dot(planet.cells[i].unit)) > 1e-6) tangent = false; + if (cur[i].length() > 1e-9) ++flowing; + } else if (cur[i].length() > 1e-12) landZero = false; + } + check(tangent, "ocean currents are tangent to the surface"); + check(landZero, "currents are zero on land"); + check(flowing > 50, "many ocean cells carry a current"); + + // Climate feedback: identical terrain, current factor on vs off. + PlanetConfig cfgOff = cfg; cfgOff.climateCurrentFactor = 0.0; + Planet conOn; conOn.generate(cfg); + Planet conOff; conOff.generate(cfgOff); + double maxAbs = 0.0, maxWarm = -1e9, maxCold = 1e9; + for (int i = 0; i < n; ++i) { + double d = conOn.temperature()[i] - conOff.temperature()[i]; + maxAbs = std::max(maxAbs, std::fabs(d)); + maxWarm = std::max(maxWarm, d); maxCold = std::min(maxCold, d); + } + check(maxAbs <= cfg.climateCurrentFactor + 1e-6, "current temp feedback bounded by climateCurrentFactor"); + check(maxWarm > 0.1 && maxCold < -0.1, "currents both warm and cool coasts"); + Planet conOn2; conOn2.generate(cfg); + bool det = true; + for (int i = 0; i < n; ++i) { + if (conOn2.temperature()[i] != conOn.temperature()[i]) det = false; + if ((conOn2.current()[i] - conOn.current()[i]).length() != 0.0) det = false; + } + check(det, "currents + feedback deterministic for a seed"); + + std::printf("Ocean: save v9\n"); + std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary); + planet.writeState(ss); + Planet q; + bool ok = q.readState(ss, true, true, true); + bool moonsRT = ok && (int)q.getMoons().size() == nm; + for (int i = 0; i < nm && moonsRT; ++i) + if (q.getMoons()[i].periodDays != planet.getMoons()[i].periodDays + || q.getMoons()[i].tideWeight != planet.getMoons()[i].tideWeight) moonsRT = false; + check(moonsRT, "save v9 round-trips the moons"); + + std::printf(failures ? "\nSOME OCEAN CHECKS FAILED (%d)\n" : "\nALL OCEAN CHECKS PASSED\n", failures); + return failures ? 1 : 0; +}