Compare commits
2 Commits
d4b46afe00
...
ca844a5d3f
| Author | SHA1 | Date | |
|---|---|---|---|
| ca844a5d3f | |||
| f84e06507a |
38
BUILD.md
38
BUILD.md
@ -51,6 +51,7 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
|||||||
N toggle the day/night terminator (Live World)
|
N toggle the day/night terminator (Live World)
|
||||||
T toggle the tide-coloured coastline (Live World)
|
T toggle the tide-coloured coastline (Live World)
|
||||||
O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue)
|
O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue)
|
||||||
|
K toggle weather clouds/rain cover (Live World)
|
||||||
SPACE pause while forming / re-evolve once settled (or the on-screen button)
|
SPACE pause while forming / re-evolve once settled (or the on-screen button)
|
||||||
[ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month)
|
[ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month)
|
||||||
S single tectonic tick
|
S single tectonic tick
|
||||||
@ -71,9 +72,9 @@ CLI flags (applied before the first load/generate):
|
|||||||
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
||||||
auto-created on first run, reload live with F2. Range-checked on
|
auto-created on first run, reload live with F2. Range-checked on
|
||||||
load; an invalid file reverts to safe defaults (not overwritten).
|
load; an invalid file reverts to safe defaults (not overwritten).
|
||||||
planet.save binary snapshot (versioned, currently v9: +moons; v8 +Live World clock;
|
planet.save binary snapshot (versioned, currently v10: +weather; v9 +moons; v8 +Live
|
||||||
v7 +biota): seed + config + full planet state; F5 writes it, F9 reloads and
|
World clock; v7 +biota): seed + config + full planet state; F5 writes it, F9
|
||||||
resumes deterministically. As of v6
|
reloads and resumes deterministically. As of v6
|
||||||
the config is stored as a self-describing key=value block (like
|
the config is stored as a self-describing key=value block (like
|
||||||
planet.cfg), so adding/removing config fields no longer breaks saves
|
planet.cfg), so adding/removing config fields no longer breaks saves
|
||||||
(unknown keys ignored, missing keys default). v6 cannot load pre-v6
|
(unknown keys ignored, missing keys default). v6 cannot load pre-v6
|
||||||
@ -202,18 +203,39 @@ moving snow line. axialTilt (above) drives the seasonal declination. Press W to
|
|||||||
Moons (1-3, randomized in generateMoons() + saved v9): orbit on the live clock, raise the
|
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.
|
tides with the sun, and render as small lit spheres with phases, orbit rings and eclipses.
|
||||||
|
|
||||||
|
Weather (PlanetConfig, Live World): a dynamic clouds/rain cycle on the live clock (key K),
|
||||||
|
saved v10. Evaporate over warm seas -> advect along the wind -> condense -> rain -> dissipate.
|
||||||
|
|
||||||
|
weatherEvapRate 0.4 /h ocean evaporation toward marine saturation
|
||||||
|
weatherWindKmh 45 km/h wind speed advecting humidity/cloud
|
||||||
|
weatherSatBase 0.4 air saturation at 0 C (lower = cloudier)
|
||||||
|
weatherSatTempCoef 0.025 saturation rise per +1 C
|
||||||
|
weatherCondense 0.6 /h supersaturation -> cloud
|
||||||
|
weatherOrographic 0.0009 extra condensation per m of windward upslope
|
||||||
|
weatherRainThresh 0.5 cloud cover above this rains
|
||||||
|
weatherRainRate 0.5 /h rain rate from excess cloud
|
||||||
|
weatherCloudDissip 0.12 /h cloud clearing
|
||||||
|
weatherSystemMax 8 max concurrent moving weather systems
|
||||||
|
weatherSpawnRate 0.06 /h genesis probability scale
|
||||||
|
weatherSystemSpeed 28 km/h drift speed of systems
|
||||||
|
weatherTropicalSST 26 C min sea-surface temp for tropical genesis
|
||||||
|
weatherSystemRadius 0.16 rad angular radius of a system's cloud/rain shield
|
||||||
|
weatherSystemCloud 1.2 /h cloud stamped at a system core
|
||||||
|
weatherSystemRain 1.6 /h rain at a system core
|
||||||
|
weatherHurricaneStr 0.6 strength above which a tropical system is a hurricane/typhoon
|
||||||
|
|
||||||
## Headless logic test (no display)
|
## Headless logic test (no display)
|
||||||
|
|
||||||
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
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/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
|
||||||
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
||||||
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.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/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetBiota.cpp \
|
||||||
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
||||||
-o /tmp/t && /tmp/t
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
|
|
||||||
# Biota / Live World / Ocean suites: same source list, swap test_logic.cpp ->
|
# Biota / Live World / Ocean / Weather suites: same source list, swap test_logic.cpp ->
|
||||||
# test_biota.cpp, test_live.cpp or test_ocean.cpp
|
# test_biota.cpp, test_live.cpp, test_ocean.cpp or test_weather.cpp
|
||||||
|
|
||||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||||
determinism. Run after changing Planet::step().
|
determinism. Run after changing Planet::step().
|
||||||
|
|||||||
73
CLAUDE.md
73
CLAUDE.md
@ -83,6 +83,18 @@ the fixed-grid Eulerian model + the climate fields are the groundwork for it.
|
|||||||
**warm (poleward) / cold (equatorward)** currents back into `sTemp` as a bounded coastal anomaly
|
**warm (poleward) / cold (equatorward)** currents back into `sTemp` as a bounded coastal anomaly
|
||||||
(`climateCurrentFactor`), so biomes shift naturally. Rendered as warm/cold **current arrows**
|
(`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.
|
over the sea (key `O`, 3D + 2D). This completes the Live World ocean/sky pass.
|
||||||
|
- **Weather — dynamic clouds & rain** *(done — see `PlanetWeather.cpp`)* — a per-cell
|
||||||
|
humidity/cloud/rain cycle advanced on the live clock: **evaporate** over warm sunlit seas →
|
||||||
|
**advect** humidity & cloud along the prevailing wind → **condense** into cloud (extra on
|
||||||
|
windward upslopes) → **rain** out → **dissipate**. Rendered as a translucent moving cloud
|
||||||
|
shell (white → dark storm where it rains) over the globe + 2D map (key `K`). Saved (v10).
|
||||||
|
- **Weather — moving systems (lows, hurricanes & typhoons)** *(done — see `PlanetWeather.cpp`)* —
|
||||||
|
drifting low-pressure **agents** (`WeatherSystem`) spawn over warm tropical seas / mid-latitude
|
||||||
|
oceans, travel with the steering wind (poleward recurve), intensify over warm water, decay over
|
||||||
|
land, and **stamp** travelling cloud/rain onto the grid — so the sky visibly evolves. The intense
|
||||||
|
tropical ones are **hurricanes/typhoons** (spin by hemisphere, eye + animated spiral marker).
|
||||||
|
Transient (not saved; respawn from the seed). This makes the weather visibly move (the base field
|
||||||
|
alone relaxes to a static pattern).
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
@ -373,6 +385,33 @@ Working and verified (logic tested headless):
|
|||||||
in `refreshView`. New knob `climateCurrentFactor` (4 °C). `test_ocean.cpp` adds: currents
|
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
|
tangent + zero on land + widespread, feedback bounded by the knob and produces both warming and
|
||||||
cooling, deterministic. Live-World ocean/sky pass complete.
|
cooling, deterministic. Live-World ocean/sky pass complete.
|
||||||
|
- **Live World — dynamic weather (clouds & rain):** `Planet::stepWeather(dtHours)`
|
||||||
|
(PlanetWeather.cpp) advances a per-cell humidity/cloud/rain cycle on the live clock:
|
||||||
|
**evaporate** over warm sunlit ocean (uses `sInsolation`+`sTemp`), **advect** humidity & cloud
|
||||||
|
downwind (upwind differencing along `sWind`/`sUpwind`, `weatherWindKmh`), **condense** the
|
||||||
|
supersaturated air into cloud — saturation `weatherSatBase + weatherSatTempCoef·T`, plus
|
||||||
|
windward **orographic** lift — **rain** out cloud above `weatherRainThresh`, then **dissipate**.
|
||||||
|
`initWeather()` spins the fields up from the moisture climatology; bounded exponential rate
|
||||||
|
forms keep it stable at any timestep. Runs each live frame in `stepSim` (dt = the same sim-hours
|
||||||
|
added to `liveTime`; held when paused). Render: a translucent **cloud shell** (white → dark
|
||||||
|
storm where it rains, alpha = cover) over the 3D globe + a `drawWeather2D` layer on the 2D map,
|
||||||
|
key `K` (default on); cell-info adds cloud/humidity/raining. **Saved v10** (humidity/cloud/rain,
|
||||||
|
flag-gated; older saves spin weather up live). Deterministic (no RNG). `test_weather.cpp`:
|
||||||
|
fields in range, clouds form + rain falls, oceans moister than land, determinism, v10 round-trip.
|
||||||
|
- **Live World — moving weather systems (lows / hurricanes / typhoons):** the base cloud/rain
|
||||||
|
field relaxes to a *static* pattern under fixed forcing, so `stepWeather` now also runs a
|
||||||
|
population of drifting **`WeatherSystem`** agents (PlanetTypes; transient, not saved; separate
|
||||||
|
`sWeatherRng` seeded from `cfg.seed` → tectonic determinism intact). Each step: **spawn** over
|
||||||
|
warm tropical ocean (5–25°, SST ≥ `weatherTropicalSST`) or a mid-latitude (30–62°) ocean low
|
||||||
|
(capped at `weatherSystemMax`, prob ∝ `weatherSpawnRate`); **move** along the steering wind
|
||||||
|
(`sWind` at the nearest cell) + a poleward recurve at `weatherSystemSpeed`; **intensify** over
|
||||||
|
warm sea / **decay+cull** over land/cold; **stamp** a Gaussian cloud/rain shield
|
||||||
|
(`weatherSystemCloud`/`Rain`, scaled by strength × local humidity) — so cloud clusters travel
|
||||||
|
and dissipate behind the system. A tropical system past `weatherHurricaneStr` is a
|
||||||
|
hurricane/typhoon. Render: an animated cyclonic **spiral marker** per system (red + eye for
|
||||||
|
cyclones, blue lows; spins with `liveTime`·hemisphere) in 3D + 2D, HUD system/cyclone counts,
|
||||||
|
and a storm list (basin-named) in the Sky & tides panel — all under `K`. `test_weather.cpp`
|
||||||
|
adds: systems spawn, move between steps, thicken cloud, RNG isolation, determinism.
|
||||||
- Mouse hover (in either view) shows per-cell info. Clicking a tile opens a
|
- 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
|
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
|
flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res
|
||||||
@ -405,7 +444,8 @@ src/
|
|||||||
PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3)
|
PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3)
|
||||||
PlanetClimate.cpp computeClimate() (temperature + orographic precipitation)
|
PlanetClimate.cpp computeClimate() (temperature + orographic precipitation)
|
||||||
PlanetLive.cpp computeInsolation/computeLiveSeason (Live World: day/night + live seasons)
|
PlanetLive.cpp computeInsolation/computeLiveSeason (Live World: day/night + live seasons)
|
||||||
PlanetOcean.cpp moons (generate/orbit) + computeTides (Live World sky & tides)
|
PlanetOcean.cpp moons (generate/orbit) + computeTides + computeOceanCurrents
|
||||||
|
PlanetWeather.cpp stepWeather (Live World dynamic clouds & rain cycle)
|
||||||
PlanetBiomes.cpp classifyBiomes() (per-cell Cell.biome from elev + climate)
|
PlanetBiomes.cpp classifyBiomes() (per-cell Cell.biome from elev + climate)
|
||||||
PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls
|
PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls
|
||||||
PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity
|
PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity
|
||||||
@ -471,12 +511,12 @@ 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/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
|
||||||
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
||||||
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.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/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetBiota.cpp \
|
||||||
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
||||||
-o /tmp/t && /tmp/t
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
```
|
```
|
||||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp` or `test_ocean.cpp` to run the
|
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp` or
|
||||||
Biota / Live World / Ocean suites — same source list.)
|
`test_weather.cpp` to run the Biota / Live World / Ocean / Weather suites — same source list.)
|
||||||
|
|
||||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
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
|
the window (the engine lives in `src/sim` and is raylib-free, so it links without
|
||||||
@ -507,7 +547,8 @@ elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga
|
|||||||
active mode shown top-center of the globe) ·
|
active mode shown top-center of the globe) ·
|
||||||
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
||||||
all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) ·
|
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 ·
|
`O` ocean-current arrows (warm/cold) · `K` weather clouds/rain (Live World) ·
|
||||||
|
`SPACE` or on-screen button pause ·
|
||||||
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) ·
|
`[`/`]` 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 ·
|
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
|
||||||
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
||||||
@ -541,9 +582,11 @@ 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
|
`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**
|
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
||||||
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
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
|
clock — a flag byte + `liveTime`, v9 appends the **moons** block, v10 appends the **weather** block —
|
||||||
|
humidity/cloud/rain, flag-gated); newer-than-supported is
|
||||||
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
||||||
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed.
|
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
||||||
|
saves spin weather up live.
|
||||||
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
**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),
|
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 —
|
written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves —
|
||||||
@ -605,6 +648,18 @@ triangles (plates are fixed in phase 1).
|
|||||||
`climateCurrentFactor` (4 °C) is the max coastal warming/cooling from ocean currents (0 = off;
|
`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
|
ocean-current arrows toggle with `O`). Current deflection angle + smoothing passes are
|
||||||
constants in `computeOceanCurrents()` (PlanetOcean.cpp), not config.
|
constants in `computeOceanCurrents()` (PlanetOcean.cpp), not config.
|
||||||
|
- **Weather (`weather*` in PlanetConfig / `planet.cfg`):** the Live World clouds/rain cycle —
|
||||||
|
`weatherEvapRate` (ocean evaporation speed), `weatherWindKmh` (advection speed of humidity/cloud),
|
||||||
|
`weatherSatBase`/`weatherSatTempCoef` (how much moisture the air holds vs temperature — lower
|
||||||
|
base = cloudier), `weatherCondense` (supersaturation→cloud rate), `weatherOrographic` (windward
|
||||||
|
rain on mountains), `weatherRainThresh`/`weatherRainRate` (when/how fast thick cloud rains),
|
||||||
|
`weatherCloudDissip` (cloud clearing). Toggle the overlay with `K`. Cloud render colours
|
||||||
|
(white→storm, alpha) are constants in ViewerRender/Map2D.
|
||||||
|
- **Weather systems (`weather*` storm knobs, `planet.cfg`):** `weatherSystemMax` (concurrent
|
||||||
|
cap), `weatherSpawnRate` (genesis frequency), `weatherSystemSpeed` (km/h drift),
|
||||||
|
`weatherTropicalSST` (min SST for tropical genesis), `weatherSystemRadius` (cloud-shield size),
|
||||||
|
`weatherSystemCloud`/`weatherSystemRain` (stamp strength), `weatherHurricaneStr` (strength to
|
||||||
|
count as a hurricane/typhoon). Markers show under `K`. Tune these for a stormier or calmer world.
|
||||||
- Seasons (`season*` + `axialTilt` + `biomeSeasonWeight`, `planet.cfg`) — `axialTilt` is the
|
- Seasons (`season*` + `axialTilt` + `biomeSeasonWeight`, `planet.cfg`) — `axialTilt` is the
|
||||||
master driver (0 = no seasons); `seasonAmpMax` (18 °C max seasonal half-range at full
|
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`
|
tilt/lat/interior), `seasonLatExp` (1.2, push swing toward poles), `seasonContinentRings`
|
||||||
|
|||||||
@ -28,6 +28,7 @@ add_executable(planetsim
|
|||||||
src/sim/PlanetClimate.cpp
|
src/sim/PlanetClimate.cpp
|
||||||
src/sim/PlanetLive.cpp
|
src/sim/PlanetLive.cpp
|
||||||
src/sim/PlanetOcean.cpp
|
src/sim/PlanetOcean.cpp
|
||||||
|
src/sim/PlanetWeather.cpp
|
||||||
src/sim/PlanetBiota.cpp
|
src/sim/PlanetBiota.cpp
|
||||||
src/sim/PlanetFloraGen.cpp
|
src/sim/PlanetFloraGen.cpp
|
||||||
src/sim/PlanetFaunaGen.cpp
|
src/sim/PlanetFaunaGen.cpp
|
||||||
|
|||||||
@ -34,7 +34,10 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
|||||||
- `PlanetLive.cpp` — `computeInsolation()`/`computeLiveSeason()` (Live World: day/night + live
|
- `PlanetLive.cpp` — `computeInsolation()`/`computeLiveSeason()` (Live World: day/night + live
|
||||||
seasonal temperature; derived, not saved).
|
seasonal temperature; derived, not saved).
|
||||||
- `PlanetOcean.cpp` — moons (`generateMoons`, `moonDirection`/`sunDirection`/`moonOrbitNormal`) +
|
- `PlanetOcean.cpp` — moons (`generateMoons`, `moonDirection`/`sunDirection`/`moonOrbitNormal`) +
|
||||||
`computeTides()` (Live World sky & equilibrium tides). Moons are saved (v9); tides derived.
|
`computeTides()` + `computeOceanCurrents()` (Live World sky, tides & currents). Moons saved
|
||||||
|
(v9); tides/currents derived.
|
||||||
|
- `PlanetWeather.cpp` — `initWeather`/`stepWeather` (Live World dynamic humidity/cloud/rain cycle;
|
||||||
|
saved v10).
|
||||||
- `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate).
|
- `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate).
|
||||||
- `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw +
|
- `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw +
|
||||||
`computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga).
|
`computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga).
|
||||||
@ -171,6 +174,34 @@ back into `sTemp` as a bounded coastal anomaly (`climateCurrentFactor`, smoothed
|
|||||||
applied before seasons → biomes shift with it). Rendered as warm/cold arrows over the sea
|
applied before seasons → biomes shift with it). Rendered as warm/cold arrows over the sea
|
||||||
(`buildCurrents`, key `O`). Currents/feedback are derived (not saved).
|
(`buildCurrents`, key `O`). Currents/feedback are derived (not saved).
|
||||||
|
|
||||||
|
## Weather (Live World dynamic clouds & rain)
|
||||||
|
|
||||||
|
`PlanetWeather.cpp` advances a per-cell **humidity / cloud / rain** cycle on the live clock
|
||||||
|
(`stepWeather(dtHours)`), time-varying unlike the static climate. One step: **evaporate** over
|
||||||
|
warm sunlit ocean (relax humidity toward a marine target scaled by `sTemp` warmth + `sInsolation`
|
||||||
|
daytime), **advect** humidity & cloud downwind (upwind differencing along `sWind`/`sUpwind`, speed
|
||||||
|
`weatherWindKmh`), **condense** the supersaturated air into cloud (saturation
|
||||||
|
`weatherSatBase + weatherSatTempCoef·T`, plus windward orographic lift), **rain** out cloud above
|
||||||
|
`weatherRainThresh`, then **dissipate** (half returns to humidity). All rate terms use bounded
|
||||||
|
`1−exp(−rate·dt)` forms so it's stable at any timestep (the clock can run hours→months/sec).
|
||||||
|
`initWeather()` seeds it from the moisture climatology. Deterministic (no RNG). Driven each live
|
||||||
|
frame from `Viewer::stepSim` with dt = the sim-hours added to `liveTime` (0 when paused).
|
||||||
|
|
||||||
|
Render: a translucent **cloud shell** over the 3D globe (white → dark slate where it rains, alpha
|
||||||
|
= cover, a second triangle layer at `visBase+0.03`) and a matching `drawWeather2D` layer on the
|
||||||
|
2D map (shared `drawMapTris` rasterizer), toggled with `K`. Saved as **v10** (humidity/cloud/rain,
|
||||||
|
flag-gated; pre-v10 saves spin weather up on entering Live World).
|
||||||
|
|
||||||
|
**Moving weather systems** (same `stepWeather`): the base field above relaxes to a *static*
|
||||||
|
pattern under fixed forcing, so a population of drifting `WeatherSystem` **agents** (world objects,
|
||||||
|
not cells — like moons; transient/not saved; separate `sWeatherRng` seeded from `cfg.seed`)
|
||||||
|
provides the motion. Each step they **spawn** over warm tropical ocean (5–25°) or a mid-latitude
|
||||||
|
(30–62°) ocean low, **move** along the steering wind (`sWind` at the nearest cell) + a poleward
|
||||||
|
recurve (`weatherSystemSpeed`), **intensify** over warm sea / **decay+cull** over land/cold, and
|
||||||
|
**stamp** a Gaussian cloud/rain shield onto the grid — so cloud clusters travel and dissipate
|
||||||
|
behind them. Tropical systems past `weatherHurricaneStr` are hurricanes/typhoons; rendered as
|
||||||
|
animated cyclonic spiral markers (eye for cyclones) spinning by hemisphere, in 3D + 2D, under `K`.
|
||||||
|
|
||||||
## Headless testing
|
## Headless testing
|
||||||
|
|
||||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||||
@ -178,7 +209,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 \
|
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/PlanetTectonics.cpp src/sim/PlanetDrift.cpp src/sim/PlanetErosion.cpp \
|
||||||
src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||||
src/sim/PlanetLive.cpp src/sim/PlanetOcean.cpp \
|
src/sim/PlanetLive.cpp src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp \
|
||||||
src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.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
|
src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
# test_biota.cpp uses the same source list (Biota suite).
|
# test_biota.cpp uses the same source list (Biota suite).
|
||||||
|
|||||||
@ -29,8 +29,9 @@ Vector2 mapScreen(const Map2D& m, int idx, Rectangle r, double lonOffset) {
|
|||||||
return projLonLat(m.lon[idx], m.lat[idx], lonOffset, r);
|
return projLonLat(m.lon[idx], m.lat[idx], lonOffset, r);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawMap2D(const Planet& p, const std::vector<Color>& vc,
|
// Shared triangle rasterizer for the 2D map: calls colorAt(cellIndex) -> Color per vertex.
|
||||||
const Map2D& m, Rectangle r, double lonOffset) {
|
template <typename ColorFn>
|
||||||
|
static void drawMapTris(const Planet& p, const Map2D& m, Rectangle r, double lonOffset, ColorFn colorAt) {
|
||||||
double hw = EqualEarth::halfWidth();
|
double hw = EqualEarth::halfWidth();
|
||||||
auto px = [&](double lon, double lat) -> float {
|
auto px = [&](double lon, double lat) -> float {
|
||||||
double x, y; EqualEarth::forward(lon, lat, x, y);
|
double x, y; EqualEarth::forward(lon, lat, x, y);
|
||||||
@ -47,7 +48,8 @@ void drawMap2D(const Planet& p, const std::vector<Color>& vc,
|
|||||||
double mx = std::max({lo[0], lo[1], lo[2]});
|
double mx = std::max({lo[0], lo[1], lo[2]});
|
||||||
if (mx - mn <= M_PI) { // fast path (no wrap)
|
if (mx - mn <= M_PI) { // fast path (no wrap)
|
||||||
for (int t = 0; t < 3; ++t) {
|
for (int t = 0; t < 3; ++t) {
|
||||||
rlColor4ub(vc[v[t]].r, vc[v[t]].g, vc[v[t]].b, 255);
|
Color c = colorAt(v[t]);
|
||||||
|
rlColor4ub(c.r, c.g, c.b, c.a);
|
||||||
rlVertex2f(px(lo[t], m.lat[v[t]]), m.pos[v[t]].y);
|
rlVertex2f(px(lo[t], m.lat[v[t]]), m.pos[v[t]].y);
|
||||||
}
|
}
|
||||||
} else { // antimeridian seam
|
} else { // antimeridian seam
|
||||||
@ -60,10 +62,28 @@ void drawMap2D(const Planet& p, const std::vector<Color>& vc,
|
|||||||
const double shift[3] = { 0.0, 2 * M_PI, -2 * M_PI }; // both edges; scissor clips
|
const double shift[3] = { 0.0, 2 * M_PI, -2 * M_PI }; // both edges; scissor clips
|
||||||
for (double sh : shift)
|
for (double sh : shift)
|
||||||
for (int t = 0; t < 3; ++t) {
|
for (int t = 0; t < 3; ++t) {
|
||||||
rlColor4ub(vc[v[t]].r, vc[v[t]].g, vc[v[t]].b, 255);
|
Color c = colorAt(v[t]);
|
||||||
|
rlColor4ub(c.r, c.g, c.b, c.a);
|
||||||
rlVertex2f(px(ul[t] + sh, m.lat[v[t]]), m.pos[v[t]].y);
|
rlVertex2f(px(ul[t] + sh, m.lat[v[t]]), m.pos[v[t]].y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rlEnd();
|
rlEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void drawMap2D(const Planet& p, const std::vector<Color>& vc,
|
||||||
|
const Map2D& m, Rectangle r, double lonOffset) {
|
||||||
|
drawMapTris(p, m, r, lonOffset, [&](int i) { return Color{ vc[i].r, vc[i].g, vc[i].b, 255 }; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawWeather2D(const Planet& p, const std::vector<double>& cloud, const std::vector<double>& rain,
|
||||||
|
const Map2D& m, Rectangle r, double lonOffset) {
|
||||||
|
if (cloud.empty()) return;
|
||||||
|
double maxR = 1e-6; for (double v : rain) maxR = std::max(maxR, v);
|
||||||
|
drawMapTris(p, m, r, lonOffset, [&](int i) -> Color {
|
||||||
|
double c = std::clamp(cloud[i], 0.0, 1.0);
|
||||||
|
double rain01 = std::clamp(rain[i] / maxR, 0.0, 1.0);
|
||||||
|
return Color{ (unsigned char)(245 - 150 * rain01), (unsigned char)(245 - 130 * rain01),
|
||||||
|
(unsigned char)(250 - 95 * rain01), (unsigned char)(c * 205.0) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -21,3 +21,8 @@ Vector2 mapScreen(const Map2D& m, int idx, Rectangle r, double lonOffset);
|
|||||||
// lonOffset pans the map east/west (radians); y is unchanged by the pan.
|
// lonOffset pans the map east/west (radians); y is unchanged by the pan.
|
||||||
void drawMap2D(const Planet& p, const std::vector<Color>& vc,
|
void drawMap2D(const Planet& p, const std::vector<Color>& vc,
|
||||||
const Map2D& m, Rectangle r, double lonOffset);
|
const Map2D& m, Rectangle r, double lonOffset);
|
||||||
|
|
||||||
|
// Translucent Live World cloud/rain layer over the 2D map (white -> dark storm where it rains;
|
||||||
|
// alpha = cloud cover). Same triangle iteration as drawMap2D but blended on top.
|
||||||
|
void drawWeather2D(const Planet& p, const std::vector<double>& cloud, const std::vector<double>& rain,
|
||||||
|
const Map2D& m, Rectangle r, double lonOffset);
|
||||||
|
|||||||
@ -64,6 +64,11 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
|||||||
if (sized(p.tide()))
|
if (sized(p.tide()))
|
||||||
L.push_back(std::string(TextFormat("tide %+.2f m (%s)", p.tide()[i],
|
L.push_back(std::string(TextFormat("tide %+.2f m (%s)", p.tide()[i],
|
||||||
p.tide()[i] >= 0.0 ? "high" : "low")));
|
p.tide()[i] >= 0.0 ? "high" : "low")));
|
||||||
|
if (sized(p.cloud()))
|
||||||
|
L.push_back(std::string(TextFormat("weather: cloud %.0f%% humidity %.0f%%%s",
|
||||||
|
p.cloud()[i] * 100.0,
|
||||||
|
sized(p.humidity()) ? p.humidity()[i] * 100.0 : 0.0,
|
||||||
|
(sized(p.rain()) && p.rain()[i] > 0.02) ? " raining" : "")));
|
||||||
L.push_back(std::string(TextFormat("geoAge %.0f My neighbors %d", age, (int)c.neighbors.size())));
|
L.push_back(std::string(TextFormat("geoAge %.0f My neighbors %d", age, (int)c.neighbors.size())));
|
||||||
// Hydrology (derived; present once routeFlow()/hydrology() has run).
|
// Hydrology (derived; present once routeFlow()/hydrology() has run).
|
||||||
if (sized(p.discharge()) && p.discharge()[i] > p.cfg.riverThreshold)
|
if (sized(p.discharge()) && p.discharge()[i] > p.cfg.riverThreshold)
|
||||||
|
|||||||
@ -227,6 +227,7 @@ void Viewer::regenWorld() { // after generate(): geometry change
|
|||||||
buildMap2D(planet, mapRect, map2D);
|
buildMap2D(planet, mapRect, map2D);
|
||||||
selectedCell = -1; subgrids.clear();
|
selectedCell = -1; subgrids.clear();
|
||||||
settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false;
|
settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false;
|
||||||
|
liveWorld = false; // reseed/regen drops back to World Creation
|
||||||
planet.drifting = false; // Phase 1: original forming behavior
|
planet.drifting = false; // Phase 1: original forming behavior
|
||||||
phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy;
|
phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy;
|
||||||
rivers.clear(); bigRivers.clear();
|
rivers.clear(); bigRivers.clear();
|
||||||
@ -278,7 +279,7 @@ void Viewer::loadGame(const char* path) {
|
|||||||
if (ver >= 8) { is.read(reinterpret_cast<char*>(&lw), sizeof lw);
|
if (ver >= 8) { is.read(reinterpret_cast<char*>(&lw), sizeof lw);
|
||||||
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
is.read(reinterpret_cast<char*>(&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 (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
||||||
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota, v9: moons
|
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather
|
||||||
cfg = planet.cfg; // adopt the loaded config
|
cfg = planet.cfg; // adopt the loaded config
|
||||||
elapsedMy = em; settled = (st != 0);
|
elapsedMy = em; settled = (st != 0);
|
||||||
planet.drifting = settled; // resume drift boosts iff mid-drift
|
planet.drifting = settled; // resume drift boosts iff mid-drift
|
||||||
@ -302,7 +303,8 @@ void Viewer::loadGame(const char* path) {
|
|||||||
void Viewer::stepSim() {
|
void Viewer::stepSim() {
|
||||||
if (liveWorld) {
|
if (liveWorld) {
|
||||||
// --- Live World: advance the slow clock; geology is frozen --------
|
// --- Live World: advance the slow clock; geology is frozen --------
|
||||||
if (!paused) liveTime += liveRate * GetFrameTime(); // hours
|
double dtH = (!paused) ? liveRate * GetFrameTime() : 0.0; // simulated hours this frame
|
||||||
|
liveTime += dtH;
|
||||||
double days = liveTime / planet.cfg.dayLengthHours;
|
double days = liveTime / planet.cfg.dayLengthHours;
|
||||||
double dayOfYear01 = days / planet.cfg.yearLengthDays;
|
double dayOfYear01 = days / planet.cfg.yearLengthDays;
|
||||||
dayOfYear01 -= std::floor(dayOfYear01);
|
dayOfYear01 -= std::floor(dayOfYear01);
|
||||||
@ -319,6 +321,7 @@ void Viewer::stepSim() {
|
|||||||
moonDirs.push_back(Vector3{ (float)md.x, (float)md.y, (float)md.z });
|
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 });
|
moonNormals.push_back(Vector3{ (float)mn.x, (float)mn.y, (float)mn.z });
|
||||||
}
|
}
|
||||||
|
planet.stepWeather(dtH); // dynamic clouds & rain on the live clock
|
||||||
rebuildLiveOverlay();
|
rebuildLiveOverlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||||
struct Viewer {
|
struct Viewer {
|
||||||
// ---- Files / save format ------------------------------------------------
|
// ---- Files / save format ------------------------------------------------
|
||||||
static constexpr uint32_t SAVE_VERSION = 9; // v9: +moons; v8: +Live World clock; v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3
|
static constexpr uint32_t SAVE_VERSION = 10; // v10: +weather; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
||||||
const char* CONFIG_PATH = "planet.cfg";
|
const char* CONFIG_PATH = "planet.cfg";
|
||||||
const char* SAVE_PATH = "planet.save";
|
const char* SAVE_PATH = "planet.save";
|
||||||
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
||||||
@ -90,6 +90,7 @@ struct Viewer {
|
|||||||
bool showTides = false; // colour the coastline by the live tide level (key T)
|
bool showTides = false; // colour the coastline by the live tide level (key T)
|
||||||
std::vector<Vector3> currentSegs; std::vector<Color> currentCols; // ocean-current arrows
|
std::vector<Vector3> currentSegs; std::vector<Color> currentCols; // ocean-current arrows
|
||||||
bool showCurrents = false; // ocean current arrows, warm/cold (key O)
|
bool showCurrents = false; // ocean current arrows, warm/cold (key O)
|
||||||
|
bool showClouds = true; // Live World cloud/rain cover overlay (key K)
|
||||||
|
|
||||||
// Selection + subgrid (phase 4/5 preview).
|
// Selection + subgrid (phase 4/5 preview).
|
||||||
int selectedCell = -1;
|
int selectedCell = -1;
|
||||||
|
|||||||
@ -145,6 +145,7 @@ void Viewer::handleInput() {
|
|||||||
if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator
|
if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator
|
||||||
if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline
|
if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline
|
||||||
if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows
|
if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows
|
||||||
|
if (IsKeyPressed(KEY_K)) showClouds = !showClouds; // toggle weather cloud/rain cover
|
||||||
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
|
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
|
||||||
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
||||||
if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled)
|
if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled)
|
||||||
|
|||||||
@ -103,6 +103,62 @@ void Viewer::renderGlobe3D() {
|
|||||||
}
|
}
|
||||||
rlEnd(); rlSetLineWidth(1.0f);
|
rlEnd(); rlSetLineWidth(1.0f);
|
||||||
}
|
}
|
||||||
|
// Live World weather: a translucent cloud shell over the globe (white -> dark storm where it
|
||||||
|
// rains), alpha = cloud cover. Drawn as a second triangle layer just above the terrain.
|
||||||
|
if (liveWorld && showClouds && !planet.cloud().empty()) {
|
||||||
|
const std::vector<double>& cl = planet.cloud();
|
||||||
|
const std::vector<double>& rn = planet.rain();
|
||||||
|
double maxR = 1e-6; for (double r : rn) maxR = std::max(maxR, r);
|
||||||
|
const std::vector<int>& ctri = planet.triIndices();
|
||||||
|
const float cr = visBase + 0.03f;
|
||||||
|
rlBegin(RL_TRIANGLES);
|
||||||
|
for (size_t k = 0; k + 2 < ctri.size(); k += 3) {
|
||||||
|
for (int j = 0; j < 3; ++j) {
|
||||||
|
int idx = ctri[k + j];
|
||||||
|
double c = std::clamp(cl[idx], 0.0, 1.0);
|
||||||
|
double rain01 = std::clamp(rn[idx] / maxR, 0.0, 1.0);
|
||||||
|
unsigned char R = (unsigned char)(245 - 150 * rain01); // white -> slate
|
||||||
|
unsigned char G = (unsigned char)(245 - 130 * rain01);
|
||||||
|
unsigned char B = (unsigned char)(250 - 95 * rain01);
|
||||||
|
unsigned char A = (unsigned char)(std::clamp(c, 0.0, 1.0) * 205.0);
|
||||||
|
const Vec3& u = planet.cells[idx].unit;
|
||||||
|
rlColor4ub(R, G, B, A);
|
||||||
|
rlVertex3f((float)(u.x * cr), (float)(u.y * cr), (float)(u.z * cr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rlEnd();
|
||||||
|
}
|
||||||
|
// Live World storm markers: an animated cyclonic spiral per weather system (hurricanes red
|
||||||
|
// with an eye; lows blue), spinning with the live clock by the system's hemisphere sense.
|
||||||
|
if (liveWorld && showClouds && !planet.storms().empty()) {
|
||||||
|
const float SR = visBase + 0.05f;
|
||||||
|
for (const auto& ws : planet.storms()) {
|
||||||
|
Vec3 p{ ws.pos.x, ws.pos.y, ws.pos.z };
|
||||||
|
Vec3 u = p.cross(Vec3{0, 1, 0}); if (u.length() < 1e-6) u = p.cross(Vec3{1, 0, 0});
|
||||||
|
u = u.normalized(); Vec3 v = p.cross(u).normalized();
|
||||||
|
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
||||||
|
unsigned char cR = hur ? 240 : 150, cG = hur ? 60 : 200, cB = hur ? 60 : 235;
|
||||||
|
unsigned char A = (unsigned char)(110 + 140 * std::clamp(ws.strength, 0.0, 1.0));
|
||||||
|
double rmax = 0.04 + 0.10 * ws.strength;
|
||||||
|
double phase = liveTime * ws.spin * 0.4;
|
||||||
|
rlSetLineWidth(2.0f); rlBegin(RL_LINES); rlColor4ub(cR, cG, cB, A);
|
||||||
|
const int N = 36; const double turns = 2.2;
|
||||||
|
for (int arm = 0; arm < 2; ++arm) {
|
||||||
|
double a0 = phase + arm * M_PI; Vec3 prev{};
|
||||||
|
for (int k = 0; k <= N; ++k) {
|
||||||
|
double t = (double)k / N;
|
||||||
|
double a = a0 + t * turns * 2.0 * M_PI * ws.spin;
|
||||||
|
Vec3 dir = u * std::cos(a) + v * std::sin(a);
|
||||||
|
Vec3 wp = (p + dir * (rmax * t)).normalized() * (double)SR;
|
||||||
|
if (k > 0) { rlVertex3f((float)prev.x, (float)prev.y, (float)prev.z);
|
||||||
|
rlVertex3f((float)wp.x, (float)wp.y, (float)wp.z); }
|
||||||
|
prev = wp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rlEnd(); rlSetLineWidth(1.0f);
|
||||||
|
if (hur) { Vec3 e = p * (double)SR; DrawSphere(Vector3{(float)e.x,(float)e.y,(float)e.z}, 0.02f, Color{255,240,200,255}); }
|
||||||
|
}
|
||||||
|
}
|
||||||
if (showGrat) drawGraticule3D(graticule, gratR);
|
if (showGrat) drawGraticule3D(graticule, gratR);
|
||||||
// Markers: selected (orange), hovered cell (yellow), hovered subcell (white).
|
// Markers: selected (orange), hovered cell (yellow), hovered subcell (white).
|
||||||
if (selectedCell >= 0) {
|
if (selectedCell >= 0) {
|
||||||
@ -195,6 +251,19 @@ void Viewer::renderMap2D() {
|
|||||||
if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 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 (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, mapRect, mapLon);
|
||||||
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, mapRect, mapLon);
|
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, mapRect, mapLon);
|
||||||
|
if (liveWorld && showClouds && !planet.cloud().empty()) drawWeather2D(planet, planet.cloud(), planet.rain(), map2D, mapRect, mapLon);
|
||||||
|
if (liveWorld && showClouds && !planet.storms().empty()) {
|
||||||
|
for (const auto& ws : planet.storms()) {
|
||||||
|
double lon, lat; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
||||||
|
Vector2 sp = projLonLat(lon, lat, mapLon, mapRect);
|
||||||
|
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
||||||
|
Color c = hur ? Color{240, 60, 60, 255} : Color{150, 200, 235, 255};
|
||||||
|
float rad = 5.0f + 10.0f * (float)ws.strength;
|
||||||
|
DrawCircleLines((int)sp.x, (int)sp.y, rad, c);
|
||||||
|
if (hur) DrawCircleLines((int)sp.x, (int)sp.y, rad * 0.55f, c);
|
||||||
|
DrawCircleV(sp, 2.0f, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (phase3 && showRivers) {
|
if (phase3 && showRivers) {
|
||||||
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, mapRect, mapLon);
|
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, mapRect, mapLon);
|
||||||
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, mapRect, mapLon);
|
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, mapRect, mapLon);
|
||||||
@ -304,6 +373,24 @@ void Viewer::renderLiveInfo() {
|
|||||||
} else {
|
} else {
|
||||||
DrawText("click a coastal tile", x, y, 15, Color{150, 155, 170, 255});
|
DrawText("click a coastal tile", x, y, 15, Color{150, 155, 170, 255});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Active weather systems (lows / tropical cyclones), named by basin.
|
||||||
|
y += 12;
|
||||||
|
DrawText("Weather systems", x, y, 18, Color{200, 205, 220, 255}); y += 26;
|
||||||
|
const auto& storms = planet.storms();
|
||||||
|
if (storms.empty()) DrawText("(calm — none active)", x, y, 15, Color{150, 155, 170, 255});
|
||||||
|
int shown = 0;
|
||||||
|
for (const auto& ws : storms) {
|
||||||
|
if (shown >= 6 || y > (int)(r.y + r.height) - 22) break;
|
||||||
|
double lon, lat; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
||||||
|
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
||||||
|
const char* kind = hur ? (lon > -0.5 && lon < 2.4 ? "Typhoon" : "Hurricane") // W Pacific vs rest
|
||||||
|
: ws.tropical ? "Tropical low" : "Low";
|
||||||
|
Color c = hur ? Color{240, 90, 80, 255} : Color{170, 200, 230, 255};
|
||||||
|
DrawText(TextFormat("%s %.0f%% @ %+.0f,%+.0f", kind, ws.strength * 100.0,
|
||||||
|
lat * 180.0 / M_PI, lon * 180.0 / M_PI), x, y, 15, c);
|
||||||
|
y += 21; ++shown;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
||||||
@ -356,6 +443,9 @@ void Viewer::renderHUD() {
|
|||||||
else { rl = "h/s"; rv = liveRate; }
|
else { rl = "h/s"; rv = liveRate; }
|
||||||
line(TextFormat("rate %.1f %s day/night %s ([ / ] speed, N toggle, W exit)",
|
line(TextFormat("rate %.1f %s day/night %s ([ / ] speed, N toggle, W exit)",
|
||||||
rv, rl, dayNightOn ? "on" : "off"));
|
rv, rl, dayNightOn ? "on" : "off"));
|
||||||
|
int nStorm = 0, nHur = 0;
|
||||||
|
for (const auto& ws : planet.storms()) { ++nStorm; if (ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr) ++nHur; }
|
||||||
|
line(TextFormat("weather systems: %d tropical cyclones: %d", nStorm, nHur));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
line(TextFormat("%s %.1f My elapsed %.1f My/s%s",
|
line(TextFormat("%s %.1f My elapsed %.1f My/s%s",
|
||||||
@ -374,8 +464,8 @@ void Viewer::renderHUD() {
|
|||||||
y += 8;
|
y += 8;
|
||||||
line("hover: cell info | click tile: open detail panel | C close");
|
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("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] | N day/night [%s] | T tides [%s] | O currents [%s]",
|
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s] | K clouds [%s]",
|
||||||
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
|
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off", showClouds ? "on" : "off"));
|
||||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
|
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"));
|
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
|
||||||
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
||||||
|
|||||||
@ -51,6 +51,8 @@ void Planet::buildGeometry() {
|
|||||||
}
|
}
|
||||||
sBiota.assign(cells.size(), {}); // empty biota population until generateBiota()
|
sBiota.assign(cells.size(), {}); // empty biota population until generateBiota()
|
||||||
sHasBiota = false;
|
sHasBiota = false;
|
||||||
|
sHumidity.clear(); sCloud.clear(); sRain.clear(); // weather spins up on entering Live World
|
||||||
|
sHasWeather = false; sStorms.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Planet::assignPlates() {
|
void Planet::assignPlates() {
|
||||||
|
|||||||
@ -89,6 +89,18 @@ public:
|
|||||||
void computeOceanCurrents();
|
void computeOceanCurrents();
|
||||||
const std::vector<Vec3>& current() const { return sCurrent; }
|
const std::vector<Vec3>& current() const { return sCurrent; }
|
||||||
|
|
||||||
|
// Weather (Live World): dynamic per-cell humidity / cloud cover / rain advanced on the live
|
||||||
|
// clock. initWeather() spins the fields up from the climatology; stepWeather(dtHours) runs
|
||||||
|
// one cycle (evaporate over warm seas -> advect along the wind -> condense into cloud, with
|
||||||
|
// orographic lift -> rain out -> dissipate). Reads sInsolation/sTemp/sWind/sUpwind/sMoist
|
||||||
|
// (computeClimate + computeInsolation set those). Saved (v10).
|
||||||
|
void initWeather();
|
||||||
|
void stepWeather(double dtHours);
|
||||||
|
const std::vector<double>& humidity() const { return sHumidity; }
|
||||||
|
const std::vector<double>& cloud() const { return sCloud; }
|
||||||
|
const std::vector<double>& rain() const { return sRain; }
|
||||||
|
const std::vector<WeatherSystem>& storms() const { return sStorms; }
|
||||||
|
|
||||||
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
|
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
|
||||||
// fields (temperature + normalized precipitation). Derived + written back into
|
// fields (temperature + normalized precipitation). Derived + written back into
|
||||||
// cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves.
|
// cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves.
|
||||||
@ -124,8 +136,10 @@ public:
|
|||||||
// older saves (v3) pass false -- biomes are reclassified after the cells load.
|
// older saves (v3) pass false -- biomes are reclassified after the cells load.
|
||||||
// hasBiota: whether the stream carries the biota population block (save v7+).
|
// hasBiota: whether the stream carries the biota population block (save v7+).
|
||||||
// hasMoons: whether the stream carries the moons block (save v9+); older saves
|
// hasMoons: whether the stream carries the moons block (save v9+); older saves
|
||||||
// synthesize moons from the seed instead.
|
// synthesize moons from the seed instead. hasWeather: the weather block (save v10+);
|
||||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true, bool hasMoons = true);
|
// older saves leave weather to spin up on entering Live World.
|
||||||
|
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
||||||
|
bool hasMoons = true, bool hasWeather = true);
|
||||||
|
|
||||||
// Helpers for rendering / info.
|
// Helpers for rendering / info.
|
||||||
double cellWidthMeters() const; // approx lateral cell spacing
|
double cellWidthMeters() const; // approx lateral cell spacing
|
||||||
@ -199,6 +213,13 @@ private:
|
|||||||
// sTide is the equilibrium tidal height (m) from the moons + sun.
|
// sTide is the equilibrium tidal height (m) from the moons + sun.
|
||||||
std::vector<double> sInsolation, sLiveTemp, sTide;
|
std::vector<double> sInsolation, sLiveTemp, sTide;
|
||||||
std::vector<Vec3> sCurrent; // ocean surface current velocity (tangent; zero on land)
|
std::vector<Vec3> sCurrent; // ocean surface current velocity (tangent; zero on land)
|
||||||
|
// Weather (Live World; saved v10). sHasWeather latches once spun up/loaded.
|
||||||
|
std::vector<double> sHumidity, sCloud, sRain;
|
||||||
|
bool sHasWeather = false;
|
||||||
|
// Moving weather systems (transient agents; not saved). Separate RNG keeps tectonic
|
||||||
|
// determinism intact (seeded from cfg.seed in initWeather).
|
||||||
|
std::vector<WeatherSystem> sStorms;
|
||||||
|
uint32_t sWeatherRng = 1;
|
||||||
|
|
||||||
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||||
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
||||||
|
|||||||
@ -36,9 +36,14 @@
|
|||||||
D(bioFungaTempMin) D(bioRegionBonus) \
|
D(bioFungaTempMin) D(bioRegionBonus) \
|
||||||
D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \
|
D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \
|
||||||
D(tideAmplitude) D(tideSunFactor) \
|
D(tideAmplitude) D(tideSunFactor) \
|
||||||
|
D(weatherEvapRate) D(weatherWindKmh) D(weatherSatBase) D(weatherSatTempCoef) \
|
||||||
|
D(weatherCondense) D(weatherOrographic) D(weatherRainThresh) D(weatherRainRate) \
|
||||||
|
D(weatherCloudDissip) \
|
||||||
|
D(weatherSpawnRate) D(weatherSystemSpeed) D(weatherTropicalSST) D(weatherSystemRadius) \
|
||||||
|
D(weatherSystemCloud) D(weatherSystemRain) D(weatherHurricaneStr) \
|
||||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) \
|
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
||||||
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||||
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) \
|
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) \
|
||||||
U(seed)
|
U(seed)
|
||||||
@ -191,6 +196,22 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.seaIceTemp, -60.0, 20.0, "seaIceTemp"));
|
E(rng(cfg.seaIceTemp, -60.0, 20.0, "seaIceTemp"));
|
||||||
E(rng(cfg.tideAmplitude, 0.0, 100.0, "tideAmplitude"));
|
E(rng(cfg.tideAmplitude, 0.0, 100.0, "tideAmplitude"));
|
||||||
E(rng(cfg.tideSunFactor, 0.0, 5.0, "tideSunFactor"));
|
E(rng(cfg.tideSunFactor, 0.0, 5.0, "tideSunFactor"));
|
||||||
|
E(rng(cfg.weatherEvapRate, 0.0, 50.0, "weatherEvapRate"));
|
||||||
|
E(rng(cfg.weatherWindKmh, 0.0, 1000.0, "weatherWindKmh"));
|
||||||
|
E(rng(cfg.weatherSatBase, 0.01, 5.0, "weatherSatBase"));
|
||||||
|
E(rng(cfg.weatherSatTempCoef, 0.0, 1.0, "weatherSatTempCoef"));
|
||||||
|
E(rng(cfg.weatherCondense, 0.0, 50.0, "weatherCondense"));
|
||||||
|
E(rng(cfg.weatherOrographic, 0.0, 1.0, "weatherOrographic"));
|
||||||
|
E(rng(cfg.weatherRainThresh, 0.0, 1.5, "weatherRainThresh"));
|
||||||
|
E(rng(cfg.weatherRainRate, 0.0, 50.0, "weatherRainRate"));
|
||||||
|
E(rng(cfg.weatherCloudDissip, 0.0, 50.0, "weatherCloudDissip"));
|
||||||
|
E(rng(cfg.weatherSpawnRate, 0.0, 10.0, "weatherSpawnRate"));
|
||||||
|
E(rng(cfg.weatherSystemSpeed, 0.0, 500.0, "weatherSystemSpeed"));
|
||||||
|
E(rng(cfg.weatherTropicalSST, -10.0, 40.0, "weatherTropicalSST"));
|
||||||
|
E(rng(cfg.weatherSystemRadius, 0.01, 1.5, "weatherSystemRadius"));
|
||||||
|
E(rng(cfg.weatherSystemCloud, 0.0, 20.0, "weatherSystemCloud"));
|
||||||
|
E(rng(cfg.weatherSystemRain, 0.0, 20.0, "weatherSystemRain"));
|
||||||
|
E(rng(cfg.weatherHurricaneStr, 0.0, 1.0, "weatherHurricaneStr"));
|
||||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||||
@ -203,6 +224,7 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses"));
|
E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses"));
|
||||||
E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth"));
|
E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth"));
|
||||||
E(irng(cfg.seasonContinentRings, 1, 100, "seasonContinentRings"));
|
E(irng(cfg.seasonContinentRings, 1, 100, "seasonContinentRings"));
|
||||||
|
E(irng(cfg.weatherSystemMax, 0, 1000, "weatherSystemMax"));
|
||||||
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
||||||
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
||||||
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
||||||
@ -279,9 +301,12 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
writeVec(os, cb.flora); writeVec(os, cb.fauna); writeVec(os, cb.funga);
|
writeVec(os, cb.flora); writeVec(os, cb.fauna); writeVec(os, cb.funga);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// v10: Live World weather (humidity/cloud/rain). Flag-gated like biota.
|
||||||
|
uint8_t hasWx = (sHasWeather && sHumidity.size() == cells.size()) ? 1 : 0; writePod(os, hasWx);
|
||||||
|
if (hasWx) { writeVec(os, sHumidity); writeVec(os, sCloud); writeVec(os, sRain); }
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons) {
|
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons, bool hasWeather) {
|
||||||
// Read the length-prefixed key=value config block (see writeState). A default
|
// 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
|
// 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.
|
// current defaults. The length guard rejects pre-v6 (raw-POD-config) saves.
|
||||||
@ -327,6 +352,18 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// v10: Live World weather. Older saves leave it to spin up on entering Live World. The moving
|
||||||
|
// weather systems are transient (not saved): clear them + reseed the weather RNG from the seed.
|
||||||
|
sHasWeather = false; sHumidity.clear(); sCloud.clear(); sRain.clear();
|
||||||
|
sStorms.clear(); sWeatherRng = cfg.seed ? (cfg.seed ^ 0x5701A123u) : 0x5701A123u;
|
||||||
|
if (hasWeather) {
|
||||||
|
uint8_t hasWx = 0; readPod(is, hasWx);
|
||||||
|
if (hasWx) {
|
||||||
|
readVec(is, sHumidity); readVec(is, sCloud); readVec(is, sRain);
|
||||||
|
if (!is || (int)sHumidity.size() != (int)cells.size()) return false;
|
||||||
|
sHasWeather = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
computeBiotaDensity(); // derived density scalars for the colour views
|
computeBiotaDensity(); // derived density scalars for the colour views
|
||||||
return (bool)is;
|
return (bool)is;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,20 @@ struct SubGrid {
|
|||||||
|
|
||||||
enum class PlateType { Oceanic, Continental };
|
enum class PlateType { Oceanic, Continental };
|
||||||
|
|
||||||
|
// A moving weather system (Live World): a drifting low-pressure disturbance that travels with the
|
||||||
|
// steering wind and stamps clouds & rain onto the weather fields. Geometry is fixed, so this is a
|
||||||
|
// world-object agent (a point on the sphere, like a moon), not a cell. The intense tropical ones
|
||||||
|
// (strength past weatherHurricaneStrength) are hurricanes/typhoons. Transient -- not saved.
|
||||||
|
struct WeatherSystem {
|
||||||
|
Vec3 pos; // unit position on the sphere
|
||||||
|
double strength = 0.0; // intensity 0..1 (drives cloud/rain boost + marker size)
|
||||||
|
double radius = 0.15; // angular radius (radians)
|
||||||
|
double age = 0.0; // hours alive
|
||||||
|
double life = 120.0; // total lifetime (hours)
|
||||||
|
double spin = 1.0; // cyclonic sense: +1 CCW (N hemisphere) / -1 CW (S)
|
||||||
|
bool tropical = false; // warm-core tropical (can become a cyclone) vs extratropical low
|
||||||
|
};
|
||||||
|
|
||||||
// Phase-3 (climate & biomes) classification of a cell, derived from elevation,
|
// Phase-3 (climate & biomes) classification of a cell, derived from elevation,
|
||||||
// latitude (temperature) and hydrology/coast (moisture). Stored per cell (uint8,
|
// latitude (temperature) and hydrology/coast (moisture). Stored per cell (uint8,
|
||||||
// serialized) so Phase-4 civilization can read it. Keep Ocean == 0 so a default-
|
// serialized) so Phase-4 civilization can read it. Keep Ocean == 0 so a default-
|
||||||
@ -257,4 +271,30 @@ struct PlanetConfig {
|
|||||||
double seaIceTemp = -2.0; // C: ocean below the live temperature shows sea ice
|
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 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)
|
double tideSunFactor = 0.46; // sun's tide weight relative to a unit moon (Earth ~0.46)
|
||||||
|
|
||||||
|
// --- Weather (Live World dynamic clouds & rain) -- see PlanetWeather.cpp -----
|
||||||
|
// A per-cell humidity/cloud/rain cycle advanced on the live clock: evaporate over warm
|
||||||
|
// sunlit seas, advect along the prevailing wind, condense into cloud (more on windward
|
||||||
|
// upslopes), rain out, and dissipate. Rates are per simulated hour.
|
||||||
|
double weatherEvapRate = 0.4; // /h: ocean evaporation toward marine saturation
|
||||||
|
double weatherWindKmh = 45.0; // km/h: prevailing wind speed for advecting humidity/cloud
|
||||||
|
double weatherSatBase = 0.4; // air saturation humidity at 0 C (warmer air holds more)
|
||||||
|
double weatherSatTempCoef = 0.025; // saturation rise per +1 C
|
||||||
|
double weatherCondense = 0.6; // /h: fraction of supersaturation that becomes cloud
|
||||||
|
double weatherOrographic = 0.0009; // extra condensation per m of windward upslope
|
||||||
|
double weatherRainThresh = 0.5; // cloud cover above this precipitates
|
||||||
|
double weatherRainRate = 0.5; // /h: rain rate from excess cloud
|
||||||
|
double weatherCloudDissip = 0.12; // /h: cloud clearing (half returns to humidity)
|
||||||
|
|
||||||
|
// --- Weather systems (moving lows / hurricanes / typhoons) -- PlanetWeather.cpp ---
|
||||||
|
// Drifting low-pressure disturbances travel with the steering wind and stamp cloud/rain onto
|
||||||
|
// the grid, so the sky visibly evolves; the intense tropical ones become tropical cyclones.
|
||||||
|
int weatherSystemMax = 8; // max concurrent weather systems
|
||||||
|
double weatherSpawnRate = 0.06; // /h: genesis probability scale (when below the cap)
|
||||||
|
double weatherSystemSpeed = 28.0; // km/h: steering speed at which systems drift
|
||||||
|
double weatherTropicalSST = 26.0; // C: min sea-surface temp for tropical genesis
|
||||||
|
double weatherSystemRadius= 0.16; // rad: angular radius of a system's cloud/rain shield
|
||||||
|
double weatherSystemCloud = 1.2; // /h: cloud stamped at a system's core (scaled by strength)
|
||||||
|
double weatherSystemRain = 1.6; // /h: rain intensity at a system's core
|
||||||
|
double weatherHurricaneStr= 0.6; // strength above which a tropical system is a hurricane/typhoon
|
||||||
};
|
};
|
||||||
|
|||||||
186
src/sim/PlanetWeather.cpp
Normal file
186
src/sim/PlanetWeather.cpp
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
#include "Planet.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Live World weather: a dynamic per-cell humidity / cloud / rain cycle advanced on the live
|
||||||
|
// clock (geometry fixed -- these are fields flowed over the grid, like climate, but time-varying).
|
||||||
|
// One step: evaporate over warm sunlit seas -> advect humidity & cloud along the prevailing wind
|
||||||
|
// -> condense the supersaturated air into cloud (extra on windward upslopes) -> rain out the
|
||||||
|
// thick cloud -> dissipate. Reads the static climate scaffolding (sTemp/sWind/sUpwind/sMoist set
|
||||||
|
// by computeClimate) and the live sInsolation (computeInsolation). Deterministic; saved (v10).
|
||||||
|
|
||||||
|
void Planet::initWeather() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
sHumidity.assign(n, 0.0);
|
||||||
|
sCloud.assign(n, 0.0);
|
||||||
|
sRain.assign(n, 0.0);
|
||||||
|
const double sea = cfg.seaLevel;
|
||||||
|
const bool haveM = ((int)sMoist.size() == n);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].elevation <= sea) sHumidity[i] = 0.9; // saturated marine air
|
||||||
|
else sHumidity[i] = haveM ? (0.2 + 0.5 * sMoist[i]) : 0.3; // land: from climatology
|
||||||
|
}
|
||||||
|
sStorms.clear();
|
||||||
|
sWeatherRng = cfg.seed ? (cfg.seed ^ 0x5701A123u) : 0x5701A123u; // separate RNG
|
||||||
|
sHasWeather = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::stepWeather(double dtHours) {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if (!sHasWeather || (int)sHumidity.size() != n || (int)sCloud.size() != n || (int)sRain.size() != n)
|
||||||
|
initWeather();
|
||||||
|
if (dtHours <= 0.0) return; // paused: hold the current sky
|
||||||
|
if ((int)sTemp.size() != n) return; // need the climate fields
|
||||||
|
|
||||||
|
const double sea = cfg.seaLevel;
|
||||||
|
auto isOcean = [&](int i) { return cells[i].elevation <= sea; };
|
||||||
|
const double cw = std::max(1.0, cellWidthMeters());
|
||||||
|
double advFrac = std::clamp(cfg.weatherWindKmh * 1000.0 * dtHours / cw, 0.0, 1.0);
|
||||||
|
const bool haveSun = ((int)sInsolation.size() == n);
|
||||||
|
const bool haveUp = ((int)sUpwind.size() == n);
|
||||||
|
|
||||||
|
// 1. Advect humidity downwind (upwind differencing) + evaporate over warm sunlit ocean.
|
||||||
|
std::vector<double> nh(n);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
double hUp = (haveUp && sUpwind[i] >= 0) ? sHumidity[sUpwind[i]] : sHumidity[i];
|
||||||
|
double h = sHumidity[i] * (1.0 - advFrac) + hUp * advFrac;
|
||||||
|
if (isOcean(i)) {
|
||||||
|
double tf = std::clamp((sTemp[i] + 2.0) / 30.0, 0.0, 1.0); // warm seas evaporate more
|
||||||
|
double sun = haveSun ? (0.5 + 0.5 * sInsolation[i]) : 0.7; // daytime boost
|
||||||
|
double target = 0.55 + 0.45 * tf; // marine humidity target
|
||||||
|
double rate = 1.0 - std::exp(-cfg.weatherEvapRate * sun * dtHours);
|
||||||
|
if (target > h) h += (target - h) * rate;
|
||||||
|
}
|
||||||
|
nh[i] = h;
|
||||||
|
}
|
||||||
|
sHumidity.swap(nh);
|
||||||
|
|
||||||
|
// 2. Advect cloud (it drifts with the wind too).
|
||||||
|
std::vector<double> nc(n);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
double cUp = (haveUp && sUpwind[i] >= 0) ? sCloud[sUpwind[i]] : sCloud[i];
|
||||||
|
nc[i] = sCloud[i] * (1.0 - advFrac) + cUp * advFrac;
|
||||||
|
}
|
||||||
|
sCloud.swap(nc);
|
||||||
|
|
||||||
|
// 3. Condense (saturation + orographic lift) -> rain -> dissipate, per cell.
|
||||||
|
const double condR = 1.0 - std::exp(-cfg.weatherCondense * dtHours);
|
||||||
|
const double rainR = 1.0 - std::exp(-cfg.weatherRainRate * dtHours);
|
||||||
|
const double dissR = 1.0 - std::exp(-cfg.weatherCloudDissip * dtHours);
|
||||||
|
const double invDt = 1.0 / dtHours;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
double sat = std::max(0.05, cfg.weatherSatBase + cfg.weatherSatTempCoef * std::max(0.0, sTemp[i]));
|
||||||
|
double cond = 0.0;
|
||||||
|
double excess = sHumidity[i] - sat;
|
||||||
|
if (excess > 0.0) cond += excess * condR; // convective/thermal
|
||||||
|
if (haveUp && sUpwind[i] >= 0) { // orographic (windward)
|
||||||
|
double up = cells[i].elevation - cells[sUpwind[i]].elevation;
|
||||||
|
if (up > 0.0) cond += sHumidity[i] * std::min(1.0, up * cfg.weatherOrographic) * condR;
|
||||||
|
}
|
||||||
|
cond = std::min(cond, sHumidity[i]);
|
||||||
|
sHumidity[i] -= cond;
|
||||||
|
sCloud[i] += cond;
|
||||||
|
|
||||||
|
double rain = 0.0;
|
||||||
|
if (sCloud[i] > cfg.weatherRainThresh) {
|
||||||
|
rain = (sCloud[i] - cfg.weatherRainThresh) * rainR;
|
||||||
|
sCloud[i] -= rain;
|
||||||
|
}
|
||||||
|
double diss = sCloud[i] * dissR;
|
||||||
|
sCloud[i] -= diss;
|
||||||
|
sHumidity[i] += diss * 0.5; // half re-evaporates
|
||||||
|
|
||||||
|
sRain[i] = rain * invDt; // intensity (per hour)
|
||||||
|
if (sHumidity[i] < 0.0) sHumidity[i] = 0.0;
|
||||||
|
sCloud[i] = std::clamp(sCloud[i], 0.0, 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Moving weather systems (lows / hurricanes / typhoons). Drifting agents that travel with
|
||||||
|
// the steering wind and stamp cloud/rain onto the grid, so the sky visibly evolves.
|
||||||
|
const bool haveWind = ((int)sWind.size() == n);
|
||||||
|
auto wrnd = [&]() { uint32_t x = sWeatherRng; x ^= x << 13; x ^= x >> 17; x ^= x << 5; sWeatherRng = x; return x; };
|
||||||
|
auto wrf = [&]() { return (wrnd() & 0xFFFFFFu) / double(0x1000000); };
|
||||||
|
const Vec3 worldUp{0, 1, 0};
|
||||||
|
const double D2R = M_PI / 180.0;
|
||||||
|
|
||||||
|
// 4a. Genesis: over warm tropical ocean (5..25 deg) or a mid-latitude (30..62 deg) ocean low.
|
||||||
|
if ((int)sStorms.size() < cfg.weatherSystemMax) {
|
||||||
|
double pSpawn = 1.0 - std::exp(-cfg.weatherSpawnRate * dtHours);
|
||||||
|
if (wrf() < pSpawn) {
|
||||||
|
int bestIdx = -1; double bestScore = 0.0; bool bestTrop = false;
|
||||||
|
for (int t = 0; t < 8; ++t) {
|
||||||
|
int ci = (int)(wrnd() % (uint32_t)n);
|
||||||
|
if (cells[ci].elevation > sea) continue;
|
||||||
|
double absdeg = std::fabs(std::asin(std::clamp(cells[ci].unit.y, -1.0, 1.0))) / D2R;
|
||||||
|
double score = 0.0; bool trop = false;
|
||||||
|
if (absdeg > 5.0 && absdeg < 25.0 && sTemp[ci] >= cfg.weatherTropicalSST) { score = 0.6 + 0.4 * wrf(); trop = true; }
|
||||||
|
else if (absdeg >= 30.0 && absdeg <= 62.0) { score = 0.3 + 0.3 * wrf(); }
|
||||||
|
if (score > bestScore) { bestScore = score; bestIdx = ci; bestTrop = trop; }
|
||||||
|
}
|
||||||
|
if (bestIdx >= 0) {
|
||||||
|
WeatherSystem ws;
|
||||||
|
ws.pos = cells[bestIdx].unit;
|
||||||
|
ws.strength = 0.15;
|
||||||
|
ws.radius = cfg.weatherSystemRadius * (bestTrop ? 0.8 : 1.25);
|
||||||
|
ws.life = bestTrop ? (120.0 + 180.0 * wrf()) : (60.0 + 90.0 * wrf());
|
||||||
|
ws.spin = (cells[bestIdx].unit.y >= 0.0) ? 1.0 : -1.0;
|
||||||
|
ws.tropical = bestTrop;
|
||||||
|
sStorms.push_back(ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4b. Move, intensify and cull each system.
|
||||||
|
for (size_t s = 0; s < sStorms.size(); ) {
|
||||||
|
WeatherSystem& ws = sStorms[s];
|
||||||
|
int nc = 0; double nd = -2.0; // nearest cell to the system
|
||||||
|
for (int i = 0; i < n; ++i) { double d = cells[i].unit.dot(ws.pos); if (d > nd) { nd = d; nc = i; } }
|
||||||
|
const Vec3& nrm = ws.pos;
|
||||||
|
Vec3 steer = (haveWind && sWind[nc].length() > 1e-9) ? sWind[nc].normalized() : Vec3{0, 0, 0};
|
||||||
|
Vec3 northT = worldUp - nrm * worldUp.dot(nrm); double nl = northT.length();
|
||||||
|
if (nl > 1e-9) northT = northT * (1.0 / nl);
|
||||||
|
double poleSign = (nrm.y >= 0.0) ? 1.0 : -1.0;
|
||||||
|
Vec3 vel = steer + northT * (poleSign * 0.35); // steering + poleward recurve
|
||||||
|
vel = vel - nrm * vel.dot(nrm); // keep tangent
|
||||||
|
double vl = vel.length();
|
||||||
|
if (vl > 1e-9) {
|
||||||
|
double dAng = cfg.weatherSystemSpeed * 1000.0 * dtHours / std::max(1.0, cfg.radius);
|
||||||
|
Vec3 vdir = vel * (1.0 / vl);
|
||||||
|
ws.pos = (nrm * std::cos(dAng) + vdir * std::sin(dAng)).normalized();
|
||||||
|
}
|
||||||
|
bool overWarmSea = (cells[nc].elevation <= sea) && (sTemp[nc] >= cfg.weatherTropicalSST - 4.0);
|
||||||
|
if (ws.tropical) {
|
||||||
|
if (overWarmSea) ws.strength += (1.0 - ws.strength) * (1.0 - std::exp(-0.05 * dtHours));
|
||||||
|
else ws.strength -= ws.strength * (1.0 - std::exp(-0.15 * dtHours));
|
||||||
|
} else {
|
||||||
|
double frac = std::min(1.0, ws.age / std::max(1.0, ws.life));
|
||||||
|
ws.strength = 0.2 + 0.6 * std::sin(frac * M_PI); // rise then fade
|
||||||
|
if (cells[nc].elevation > sea) ws.strength *= 0.7; // weaker over land
|
||||||
|
}
|
||||||
|
ws.strength = std::clamp(ws.strength, 0.0, 1.0);
|
||||||
|
ws.age += dtHours;
|
||||||
|
if (ws.age > ws.life || (ws.tropical && ws.strength < 0.05 && cells[nc].elevation > sea)) {
|
||||||
|
sStorms[s] = sStorms.back(); sStorms.pop_back(); // swap-remove dead system
|
||||||
|
} else ++s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4c. Stamp each system's cloud/rain shield onto the grid (Gaussian-ish core falloff).
|
||||||
|
if (!sStorms.empty()) {
|
||||||
|
std::vector<double> cosR(sStorms.size());
|
||||||
|
for (size_t s = 0; s < sStorms.size(); ++s) cosR[s] = std::cos(std::min(M_PI, sStorms[s].radius));
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
double moist = 0.3 + 0.7 * std::clamp(sHumidity[i], 0.0, 1.0);
|
||||||
|
for (size_t s = 0; s < sStorms.size(); ++s) {
|
||||||
|
double dot = cells[i].unit.dot(sStorms[s].pos);
|
||||||
|
if (dot < cosR[s]) continue; // outside the system radius
|
||||||
|
double d = std::acos(std::clamp(dot, -1.0, 1.0));
|
||||||
|
double fall = 1.0 - d / sStorms[s].radius; fall *= fall;
|
||||||
|
double st = sStorms[s].strength;
|
||||||
|
sCloud[i] += st * fall * cfg.weatherSystemCloud * dtHours * moist;
|
||||||
|
sRain[i] += st * fall * cfg.weatherSystemRain * moist;
|
||||||
|
}
|
||||||
|
sCloud[i] = std::clamp(sCloud[i], 0.0, 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
test_weather.cpp
Normal file
124
test_weather.cpp
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// Headless test for the Live World weather cycle (humidity / cloud / rain). No display needed.
|
||||||
|
//
|
||||||
|
// g++ -std=c++17 -O2 -Isrc/sim test_weather.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \
|
||||||
|
// src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp src/sim/PlanetErosion.cpp \
|
||||||
|
// src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||||
|
// src/sim/PlanetLive.cpp src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp \
|
||||||
|
// src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
|
||||||
|
// src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tw && /tmp/tw
|
||||||
|
//
|
||||||
|
// Verifies: fields stay in range; oceans (the evaporation source) end up moister than land;
|
||||||
|
// clouds form and rain falls somewhere; the cycle is deterministic; and save v10 round-trips it.
|
||||||
|
|
||||||
|
#include "Planet.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
static void check(bool cond, const char* what) {
|
||||||
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||||
|
if (!cond) ++failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a fixed weather sequence on a planet (returns whether rain ever fell, max cloud + storms).
|
||||||
|
static void runWeather(Planet& p, bool& everRained, double& maxCloud, int& maxStorms) {
|
||||||
|
p.initWeather();
|
||||||
|
everRained = false; maxCloud = 0.0; maxStorms = 0;
|
||||||
|
for (int k = 0; k < 300; ++k) {
|
||||||
|
p.computeInsolation(0.25, std::fmod(0.3 + 0.01 * k, 1.0)); // sun advances
|
||||||
|
p.stepWeather(1.0); // 1-hour steps
|
||||||
|
const std::vector<double>& rn = p.rain();
|
||||||
|
const std::vector<double>& cl = p.cloud();
|
||||||
|
for (size_t i = 0; i < rn.size(); ++i) {
|
||||||
|
if (rn[i] > 0.0) everRained = true;
|
||||||
|
maxCloud = std::max(maxCloud, cl[i]);
|
||||||
|
}
|
||||||
|
maxStorms = std::max(maxStorms, (int)p.storms().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 1337;
|
||||||
|
Planet p; p.generate(cfg);
|
||||||
|
const int n = (int)p.cells.size();
|
||||||
|
|
||||||
|
std::printf("Weather: cycle\n");
|
||||||
|
bool rained = false; double maxCloud = 0.0; int maxStorms = 0;
|
||||||
|
runWeather(p, rained, maxCloud, maxStorms);
|
||||||
|
|
||||||
|
bool inRange = true;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (p.humidity()[i] < -1e-9) inRange = false;
|
||||||
|
if (p.cloud()[i] < -1e-9 || p.cloud()[i] > 1.5 + 1e-9) inRange = false;
|
||||||
|
if (p.rain()[i] < -1e-9) inRange = false;
|
||||||
|
}
|
||||||
|
check(inRange, "humidity/cloud/rain stay in range");
|
||||||
|
check(maxCloud > 0.05, "clouds form");
|
||||||
|
check(rained, "rain falls somewhere");
|
||||||
|
|
||||||
|
// Oceans are the moisture source -> moister than land on average.
|
||||||
|
double oh = 0, lh = 0; int oc = 0, lc = 0;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (p.cells[i].elevation <= cfg.seaLevel) { oh += p.humidity()[i]; ++oc; }
|
||||||
|
else { lh += p.humidity()[i]; ++lc; }
|
||||||
|
}
|
||||||
|
oh /= std::max(1, oc); lh /= std::max(1, lc);
|
||||||
|
std::printf(" mean humidity: ocean %.3f, land %.3f\n", oh, lh);
|
||||||
|
check(oh > lh, "oceans end up moister than land");
|
||||||
|
|
||||||
|
std::printf("Weather: moving systems\n");
|
||||||
|
std::printf(" max concurrent systems: %d\n", maxStorms);
|
||||||
|
check(maxStorms > 0, "weather systems spawn over a run");
|
||||||
|
if (!p.storms().empty()) { // cloud shield (no mutation of p)
|
||||||
|
const auto& ws = p.storms()[0];
|
||||||
|
double inSum = 0, allSum = 0; int inN = 0;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
allSum += p.cloud()[i];
|
||||||
|
double d = std::acos(std::clamp(p.cells[i].unit.dot(ws.pos), -1.0, 1.0));
|
||||||
|
if (d < ws.radius) { inSum += p.cloud()[i]; ++inN; }
|
||||||
|
}
|
||||||
|
check(inN > 0 && inSum / inN > allSum / n, "cloud is thicker inside a weather system");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Weather: RNG isolation\n");
|
||||||
|
Planet z; z.generate(cfg);
|
||||||
|
std::vector<double> elev0(n); for (int i = 0; i < n; ++i) elev0[i] = z.cells[i].elevation;
|
||||||
|
z.initWeather();
|
||||||
|
for (int k = 0; k < 60; ++k) { z.computeInsolation(0.25, std::fmod(0.3 + 0.01 * k, 1.0)); z.stepWeather(1.0); }
|
||||||
|
bool terrainSame = true; for (int i = 0; i < n; ++i) if (z.cells[i].elevation != elev0[i]) terrainSame = false;
|
||||||
|
check(terrainSame, "weather + storm RNG never perturb the terrain");
|
||||||
|
|
||||||
|
std::printf("Weather: determinism\n");
|
||||||
|
Planet p2; p2.generate(cfg);
|
||||||
|
bool r2; double mc2; int ms2; runWeather(p2, r2, mc2, ms2); // p and p2 both at 300 steps
|
||||||
|
bool same = ((int)p2.storms().size() == (int)p.storms().size());
|
||||||
|
for (int i = 0; i < n; ++i)
|
||||||
|
if (p2.humidity()[i] != p.humidity()[i] || p2.cloud()[i] != p.cloud()[i]
|
||||||
|
|| p2.rain()[i] != p.rain()[i]) same = false;
|
||||||
|
check(same, "same seed + sequence -> identical weather + systems");
|
||||||
|
|
||||||
|
std::printf("Weather: systems move\n");
|
||||||
|
if (!p.storms().empty()) { // one more step -> a system shifts position
|
||||||
|
Vec3 before = p.storms()[0].pos;
|
||||||
|
p.computeInsolation(0.25, 0.61); p.stepWeather(1.0);
|
||||||
|
double best = -2.0; for (const auto& ws : p.storms()) best = std::max(best, before.dot(ws.pos));
|
||||||
|
double ang = std::acos(std::clamp(best, -1.0, 1.0));
|
||||||
|
check(ang > 1e-4 && ang < 0.3, "a weather system moves between steps");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Weather: save v10\n");
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(ss);
|
||||||
|
Planet q;
|
||||||
|
bool ok = q.readState(ss, true, true, true, true);
|
||||||
|
bool rt = ok && (int)q.cloud().size() == n;
|
||||||
|
for (int i = 0; i < n && rt; ++i)
|
||||||
|
if (q.humidity()[i] != p.humidity()[i] || q.cloud()[i] != p.cloud()[i] || q.rain()[i] != p.rain()[i])
|
||||||
|
rt = false;
|
||||||
|
check(rt, "save v10 round-trips the weather state");
|
||||||
|
|
||||||
|
std::printf(failures ? "\nSOME WEATHER CHECKS FAILED (%d)\n" : "\nALL WEATHER CHECKS PASSED\n", failures);
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user