diff --git a/BUILD.md b/BUILD.md index 9fbd15c..a55723c 100644 --- a/BUILD.md +++ b/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) T toggle the tide-coloured coastline (Live World) 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) [ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month) 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; 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 v9: +moons; v8 +Live World clock; - v7 +biota): seed + config + full planet state; F5 writes it, F9 reloads and - resumes deterministically. As of v6 + planet.save binary snapshot (versioned, currently v10: +weather; 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 @@ -202,18 +203,31 @@ 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 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 + ## 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/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 + 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/t && /tmp/t - # Biota / Live World / Ocean suites: same source list, swap test_logic.cpp -> - # test_biota.cpp, test_live.cpp or test_ocean.cpp + # Biota / Live World / Ocean / Weather suites: same source list, swap test_logic.cpp -> + # test_biota.cpp, test_live.cpp, test_ocean.cpp or test_weather.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 7937093..9f13d7d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,6 +83,12 @@ 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 (`climateCurrentFactor`), so biomes shift naturally. Rendered as warm/cold **current arrows** over the sea (key `O`, 3D + 2D). This completes the Live World ocean/sky pass. +- **Weather — dynamic clouds & rain** *(done — see `PlanetWeather.cpp`)* — a per-cell + humidity/cloud/rain cycle advanced on the live clock: **evaporate** over warm sunlit seas → + **advect** humidity & cloud along the prevailing wind → **condense** into cloud (extra on + windward upslopes) → **rain** out → **dissipate**. Rendered as a translucent moving cloud + shell (white → dark storm where it rains) over the globe + 2D map (key `K`). Saved (v10). + **Next:** tropical cyclones (hurricanes/typhoons) as moving vortex agents on top of this field. ## Current state @@ -373,6 +379,19 @@ Working and verified (logic tested headless): in `refreshView`. New knob `climateCurrentFactor` (4 °C). `test_ocean.cpp` adds: currents tangent + zero on land + widespread, feedback bounded by the knob and produces both warming and cooling, deterministic. Live-World ocean/sky pass complete. +- **Live World — dynamic weather (clouds & rain):** `Planet::stepWeather(dtHours)` + (PlanetWeather.cpp) advances a per-cell humidity/cloud/rain cycle on the live clock: + **evaporate** over warm sunlit ocean (uses `sInsolation`+`sTemp`), **advect** humidity & cloud + downwind (upwind differencing along `sWind`/`sUpwind`, `weatherWindKmh`), **condense** the + supersaturated air into cloud — saturation `weatherSatBase + weatherSatTempCoef·T`, plus + windward **orographic** lift — **rain** out cloud above `weatherRainThresh`, then **dissipate**. + `initWeather()` spins the fields up from the moisture climatology; bounded exponential rate + forms keep it stable at any timestep. Runs each live frame in `stepSim` (dt = the same sim-hours + added to `liveTime`; held when paused). Render: a translucent **cloud shell** (white → dark + storm where it rains, alpha = cover) over the 3D globe + a `drawWeather2D` layer on the 2D map, + key `K` (default on); cell-info adds cloud/humidity/raining. **Saved v10** (humidity/cloud/rain, + flag-gated; older saves spin weather up live). Deterministic (no RNG). `test_weather.cpp`: + fields in range, clouds form + rain falls, oceans moister than land, determinism, v10 round-trip. - 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 @@ -405,7 +424,8 @@ src/ 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) + 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) PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity @@ -471,12 +491,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/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 + 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/t && /tmp/t ``` -(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.) +(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp` or +`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 the window (the engine lives in `src/sim` and is raylib-free, so it links without @@ -507,7 +527,8 @@ elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga active mode shown top-center of the globe) · `B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3, all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) · -`O` ocean-current arrows (warm/cold) · `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) · `S` single tick · `F` fast-forward Phase-1 forming to settled · `H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga, @@ -541,9 +562,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 key=value text block** instead of a raw POD dump, v7 appends the **biota population** block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World** -clock — a flag byte + `liveTime`, v9 appends the **moons** block); 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`); -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 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 — @@ -605,6 +628,13 @@ triangles (plates are fixed in phase 1). `climateCurrentFactor` (4 °C) is the max coastal warming/cooling from ocean currents (0 = off; ocean-current arrows toggle with `O`). Current deflection angle + smoothing passes are constants in `computeOceanCurrents()` (PlanetOcean.cpp), not config. +- **Weather (`weather*` in PlanetConfig / `planet.cfg`):** the Live World clouds/rain cycle — + `weatherEvapRate` (ocean evaporation speed), `weatherWindKmh` (advection speed of humidity/cloud), + `weatherSatBase`/`weatherSatTempCoef` (how much moisture the air holds vs temperature — lower + base = cloudier), `weatherCondense` (supersaturation→cloud rate), `weatherOrographic` (windward + rain on mountains), `weatherRainThresh`/`weatherRainRate` (when/how fast thick cloud rains), + `weatherCloudDissip` (cloud clearing). Toggle the overlay with `K`. Cloud render colours + (white→storm, alpha) are constants in ViewerRender/Map2D. - 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` diff --git a/CMakeLists.txt b/CMakeLists.txt index e66b037..d6c70cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ add_executable(planetsim 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 diff --git a/docs/design-notes.md b/docs/design-notes.md index 28e687f..86bcac2 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -34,7 +34,10 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`). - `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. + `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). - `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw + `computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga). @@ -171,6 +174,25 @@ 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 (`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). Future: tropical cyclones +(moving vortex agents) layered on this field. + ## Headless testing Engine is raylib-free, so logic is tested without a display. Build/run: @@ -178,7 +200,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/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/t && /tmp/t # test_biota.cpp uses the same source list (Biota suite). diff --git a/src/render/Map2D.cpp b/src/render/Map2D.cpp index 1e8dd80..258d5b2 100644 --- a/src/render/Map2D.cpp +++ b/src/render/Map2D.cpp @@ -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); } -void drawMap2D(const Planet& p, const std::vector& vc, - const Map2D& m, Rectangle r, double lonOffset) { +// Shared triangle rasterizer for the 2D map: calls colorAt(cellIndex) -> Color per vertex. +template +static void drawMapTris(const Planet& p, const Map2D& m, Rectangle r, double lonOffset, ColorFn colorAt) { double hw = EqualEarth::halfWidth(); auto px = [&](double lon, double lat) -> float { double x, y; EqualEarth::forward(lon, lat, x, y); @@ -47,7 +48,8 @@ void drawMap2D(const Planet& p, const std::vector& vc, double mx = std::max({lo[0], lo[1], lo[2]}); if (mx - mn <= M_PI) { // fast path (no wrap) 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); } } else { // antimeridian seam @@ -60,10 +62,28 @@ void drawMap2D(const Planet& p, const std::vector& vc, const double shift[3] = { 0.0, 2 * M_PI, -2 * M_PI }; // both edges; scissor clips for (double sh : shift) 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); } } } rlEnd(); } + +void drawMap2D(const Planet& p, const std::vector& 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& cloud, const std::vector& 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) }; + }); +} diff --git a/src/render/Map2D.hpp b/src/render/Map2D.hpp index 5ab508d..0a2f36a 100644 --- a/src/render/Map2D.hpp +++ b/src/render/Map2D.hpp @@ -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. void drawMap2D(const Planet& p, const std::vector& vc, 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& cloud, const std::vector& rain, + const Map2D& m, Rectangle r, double lonOffset); diff --git a/src/render/Panels.cpp b/src/render/Panels.cpp index acbc370..f22d144 100644 --- a/src/render/Panels.cpp +++ b/src/render/Panels.cpp @@ -64,6 +64,11 @@ static std::vector cellInfo(const Planet& p, int i, double elev, do if (sized(p.tide())) L.push_back(std::string(TextFormat("tide %+.2f m (%s)", p.tide()[i], 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()))); // Hydrology (derived; present once routeFlow()/hydrology() has run). if (sized(p.discharge()) && p.discharge()[i] > p.cfg.riverThreshold) diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index b91d6e8..96ed0e3 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -227,6 +227,7 @@ void Viewer::regenWorld() { // after generate(): geometry change buildMap2D(planet, mapRect, map2D); selectedCell = -1; subgrids.clear(); 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 phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy; rivers.clear(); bigRivers.clear(); @@ -278,7 +279,7 @@ void Viewer::loadGame(const char* path) { 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, 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 elapsedMy = em; settled = (st != 0); planet.drifting = settled; // resume drift boosts iff mid-drift @@ -302,7 +303,8 @@ void Viewer::loadGame(const char* path) { void Viewer::stepSim() { if (liveWorld) { // --- 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 dayOfYear01 = days / planet.cfg.yearLengthDays; dayOfYear01 -= std::floor(dayOfYear01); @@ -319,6 +321,7 @@ void Viewer::stepSim() { 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 }); } + planet.stepWeather(dtH); // dynamic clouds & rain on the live clock rebuildLiveOverlay(); return; } diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index f239ebb..807082e 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 = 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* SAVE_PATH = "planet.save"; 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) std::vector currentSegs; std::vector currentCols; // ocean-current arrows 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). int selectedCell = -1; diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index b0cc487..eda3a60 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -145,6 +145,7 @@ void Viewer::handleInput() { 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_K)) showClouds = !showClouds; // toggle weather cloud/rain cover 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) diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index 3397a5b..42d1ba9 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -103,6 +103,31 @@ void Viewer::renderGlobe3D() { } 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& cl = planet.cloud(); + const std::vector& rn = planet.rain(); + double maxR = 1e-6; for (double r : rn) maxR = std::max(maxR, r); + const std::vector& 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(); + } if (showGrat) drawGraticule3D(graticule, gratR); // Markers: selected (orange), hovered cell (yellow), hovered subcell (white). if (selectedCell >= 0) { @@ -195,6 +220,7 @@ void Viewer::renderMap2D() { 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 (liveWorld && showClouds && !planet.cloud().empty()) drawWeather2D(planet, planet.cloud(), planet.rain(), map2D, 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); @@ -374,8 +400,8 @@ 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] | 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("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", showClouds ? "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"); diff --git a/src/sim/Planet.cpp b/src/sim/Planet.cpp index 15abcb8..f3f7254 100644 --- a/src/sim/Planet.cpp +++ b/src/sim/Planet.cpp @@ -51,6 +51,8 @@ void Planet::buildGeometry() { } sBiota.assign(cells.size(), {}); // empty biota population until generateBiota() sHasBiota = false; + sHumidity.clear(); sCloud.clear(); sRain.clear(); // weather spins up on entering Live World + sHasWeather = false; } void Planet::assignPlates() { diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index b58e5a6..c6f6fbd 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -89,6 +89,17 @@ public: void computeOceanCurrents(); const std::vector& 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& humidity() const { return sHumidity; } + const std::vector& cloud() const { return sCloud; } + const std::vector& rain() const { return sRain; } + // 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. @@ -124,8 +135,10 @@ public: // older saves (v3) pass false -- biomes are reclassified after the cells load. // hasBiota: whether the stream carries the biota population block (save v7+). // 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); + // synthesize moons from the seed instead. hasWeather: the weather block (save v10+); + // 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. double cellWidthMeters() const; // approx lateral cell spacing @@ -199,6 +212,9 @@ private: // 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) + // Weather (Live World; saved v10). sHasWeather latches once spun up/loaded. + std::vector sHumidity, sCloud, sRain; + bool sHasWeather = false; // Biota: derived density scalars (0..1; recomputed each tick, not saved) and the // on-demand discrete population (saved). sHasBiota latches once generated/loaded. diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 2cc8708..24df8be 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -36,6 +36,9 @@ D(bioFungaTempMin) D(bioRegionBonus) \ D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \ D(tideAmplitude) D(tideSunFactor) \ + D(weatherEvapRate) D(weatherWindKmh) D(weatherSatBase) D(weatherSatTempCoef) \ + D(weatherCondense) D(weatherOrographic) D(weatherRainThresh) D(weatherRainRate) \ + D(weatherCloudDissip) \ I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \ I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \ I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) \ @@ -191,6 +194,15 @@ std::string validateConfig(const PlanetConfig& cfg) { 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(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(irng(cfg.subdivisions, 0, 7, "subdivisions")); E(irng(cfg.plateCount, 1, 100, "plateCount")); E(irng(cfg.beltWidth, 1, 12, "beltWidth")); @@ -279,9 +291,12 @@ void Planet::writeState(std::ostream& os) const { 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 // 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. @@ -327,6 +342,16 @@ 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. + sHasWeather = false; sHumidity.clear(); sCloud.clear(); sRain.clear(); + 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 return (bool)is; } diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index 4c971d5..9ac82fd 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -257,4 +257,18 @@ struct PlanetConfig { 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) + + // --- 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) }; diff --git a/src/sim/PlanetWeather.cpp b/src/sim/PlanetWeather.cpp new file mode 100644 index 0000000..2260b9a --- /dev/null +++ b/src/sim/PlanetWeather.cpp @@ -0,0 +1,96 @@ +#include "Planet.hpp" +#include +#include +#include + +// 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 + } + 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 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 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); + } +} diff --git a/test_weather.cpp b/test_weather.cpp new file mode 100644 index 0000000..e6937ca --- /dev/null +++ b/test_weather.cpp @@ -0,0 +1,92 @@ +// 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 +#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; +} + +// Run a fixed weather sequence on a planet (returns whether rain ever fell, max cloud seen). +static void runWeather(Planet& p, bool& everRained, double& maxCloud) { + p.initWeather(); + everRained = false; maxCloud = 0.0; + for (int k = 0; k < 200; ++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& rn = p.rain(); + const std::vector& 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]); + } + } +} + +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; + runWeather(p, rained, maxCloud); + + 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: determinism\n"); + Planet p2; p2.generate(cfg); + bool r2; double mc2; runWeather(p2, r2, mc2); + bool same = true; + 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"); + + 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; +}