diff --git a/BUILD.md b/BUILD.md index 8a0a193..ed64dcd 100644 --- a/BUILD.md +++ b/BUILD.md @@ -53,7 +53,8 @@ the full ~2.8x speedup; the default uses all cores for no extra gain: O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue) K toggle weather clouds/rain cover (Live World) Y follow-cam: cycle the 3D camera through active storms (Live World; off after last) - . / , step the live clock forward / back by one rate-unit (back rewinds sky only) + . / , step the live clock forward / back by one rate-unit (auto-pauses; back also + rewinds weather + storms via an undo history) wheel over the 2D map: zoom toward cursor (1-8x); drag pans when zoomed 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) diff --git a/CLAUDE.md b/CLAUDE.md index fb425b0..7334538 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -422,9 +422,13 @@ Working and verified (logic tested headless): (`drawMapTris` now derives y from the rect, not the fixed `m.pos`). Mouse-wheel over the map zooms toward the cursor (1–8×); drag pans when zoomed, else rotates `mapLon`; 2D picking inverts the same rect. (3) **Clock stepper**: the `stepSim` live body is factored into `Viewer::liveAdvance(dtClock, - dtWeather)`; **`.`** steps forward and **`,`** back by `liveRate` hours — backward rewinds the - deterministic sky (day/night, tides, seasons, moon phases) but holds weather (`dtWeather=0`, not - reversible). `S` in Live World aliases the forward step (no longer runs a stray tectonic tick). + dtWeather)`; **`.`** steps forward and **`,`** back by `liveRate` hours (both auto-pause, like a + video frame-step). Weather is an integrated path (not analytically reversible), so a forward step + snapshots the full state — `Planet::captureWeather()`/`restoreWeather()` (humidity/cloud/rain/ + storms/RNG) into a bounded `wxUndo` history — and **`,` restores the previous snapshot**, so the + step really reverses *everything* (clouds, rain, storms) plus the deterministic sky. A continuous + run (unpause) clears the undo history (so `,` then falls back to a sky-only rewind). `S` in Live + World aliases the forward step. - 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 @@ -564,7 +568,8 @@ all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured co `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) · `S` single tick (in **Live World** steps the clock forward) · `.`/`,` step the live clock -forward/back by one rate-unit (`,` rewinds the sky only — weather can't reverse) · +forward/back by one rate-unit (auto-pauses; `,` steps **everything** back incl. weather/storms via +an undo history) · `Y` cycle the 3D camera to **follow a storm** (off after the last) · mouse-wheel **over the 2D map** zooms toward the cursor (drag pans when zoomed) · `F` fast-forward Phase-1 forming to settled · `H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga, diff --git a/docs/design-notes.md b/docs/design-notes.md index b5986c8..a471aad 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -220,9 +220,12 @@ Three viewer-only controls over the Live World sim: drag pans when zoomed, else keeps the `mapLon` longitude rotation. - **Clock stepper**: the `stepSim` Live-World body is factored into `Viewer::liveAdvance(dtClock, dtWeather)` (clamps `liveTime≥0`, recomputes insolation/season/tides/moons, `stepWeather`, - overlay). `.`/`,` step ±`liveRate` hours; backward passes `dtWeather=0` because weather is an - integrated, non-reversible path (the deterministic sky — day/night, tides, seasons, moon phases — - still rewinds fine). + overlay). `.`/`,` step ±`liveRate` hours and **auto-pause** (frame-step). Weather is integrated + and not analytically reversible, so a forward step snapshots the full weather state + (`Planet::captureWeather`/`restoreWeather` — humidity/cloud/rain/storms/RNG) into a bounded + `wxUndo` ring; **`,` restores the previous snapshot**, reversing clouds/rain/storms exactly as well + as the deterministic sky. A continuous run clears `wxUndo` (then `,` rewinds the sky only). The + snapshot also reseeds the storm RNG so re-stepping forward replays deterministically. ## Headless testing diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index 59bccdf..55815b3 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -227,7 +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; followId = 0; // reseed/regen drops back to World Creation + liveWorld = false; followId = 0; wxUndo.clear(); // 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(); @@ -304,6 +304,7 @@ void Viewer::stepSim() { if (liveWorld) { // --- Live World: advance the slow clock; geology is frozen -------- double dtH = (!paused) ? liveRate * GetFrameTime() : 0.0; // simulated hours this frame + if (dtH > 0.0) wxUndo.clear(); // a continuous run invalidates the manual step-back history liveAdvance(dtH, dtH); return; } @@ -363,6 +364,29 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) { rebuildLiveOverlay(); } +// Step the live clock forward one rate-unit. Auto-pauses (like a video frame-step) and snapshots +// the pre-step state so the backward step can restore weather + storms exactly. +void Viewer::liveStepForward() { + paused = true; + if ((int)wxUndo.size() >= wxUndoMax) wxUndo.erase(wxUndo.begin()); + wxUndo.push_back(WxFrame{ liveTime, planet.captureWeather() }); + liveAdvance(liveRate, liveRate); +} + +// Step everything back one frame: restore the last snapshot (clock + weather + storms) if we have +// one; otherwise fall back to rewinding the deterministic sky only (day/night, tides, seasons). +void Viewer::liveStepBack() { + paused = true; + if (!wxUndo.empty()) { + WxFrame f = wxUndo.back(); wxUndo.pop_back(); + liveTime = f.t; + planet.restoreWeather(f.w); + liveAdvance(0.0, 0.0); // recompute the sky/overlay at the restored time (weather held) + } else { + liveAdvance(-liveRate, 0.0); // no history: deterministic sky rewinds, weather holds + } +} + // The 2D map's projection rect after zoom/pan: mapRect scaled about its centre by mapZoom and // shifted by the screen-space pan. The scissor + frame stay the real mapRect, so it clips cleanly. Rectangle Viewer::mapViewRect() const { diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index 91379cd..5f9072f 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -79,6 +79,11 @@ struct Viewer { bool liveWorld = false, dayNightOn = true; double liveTime = 0.0; // hours since the live clock started double liveRate = 1.0; // sim hours advanced per real second (ramps hour->month) + // Step-back undo history: each forward step snapshots the clock + full weather state so a + // backward step restores everything (weather is an integrated path, not analytically reversible). + struct WxFrame { double t; WeatherSnapshot w; }; + std::vector wxUndo; + static constexpr int wxUndoMax = 180; std::vector illum; // per-cell day/night brightness (1 = day, floor = night) std::vector shadedColors;// vcolors + snow/ice tint + day/night dim (live overlay) Vector3 sunDir{0.0f, 0.0f, 1.0f}; // model-space sub-solar direction (for the 3D sun marker) @@ -138,6 +143,8 @@ struct Viewer { void loadGame(const char* path); void stepSim(); // advance forming / drift+hydrology this frame void liveAdvance(double dtClock, double dtWeather); // advance the Live World clock + fields + void liveStepForward(); // step the clock forward one rate-unit (snapshots for undo) + void liveStepBack(); // step everything back one frame (restores weather/storms) Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect) // ---- Input (ViewerInput.cpp) -------------------------------------------- diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index aa2062f..56ed915 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -176,7 +176,7 @@ void Viewer::handleInput() { refreshView(); // fresh base colours; overlay builds in stepSim setStatus("Live World started"); } else { - paused = true; followId = 0; refreshView(); // back to World Creation (drift), paused + paused = true; followId = 0; wxUndo.clear(); refreshView(); // back to World Creation, paused setStatus("Live World stopped"); } } @@ -204,13 +204,14 @@ void Viewer::handleInput() { if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); } if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); } if (IsKeyPressed(KEY_S)) { // one step - if (liveWorld) liveAdvance(liveRate, liveRate); // Live World: step the clock forward + if (liveWorld) liveStepForward(); // Live World: step the clock forward else { stepOnce(); refreshView(); } // forming/drift: one tectonic tick } - // Live World clock stepper: step by one rate-unit (liveRate hours). Backward rewinds the - // deterministic sky (day/night, tides, seasons, moon phases); weather holds (can't reverse). - if (IsKeyPressed(KEY_PERIOD) && liveWorld) { liveAdvance(liveRate, liveRate); setStatus("Step forward"); } - if (IsKeyPressed(KEY_COMMA) && liveWorld) { liveAdvance(-liveRate, 0.0); setStatus("Step back (sky only)"); } + // Live World clock stepper: step by one rate-unit (liveRate hours). Forward integrates weather; + // backward restores the snapshot from the last forward step -> everything (incl. weather + + // storms) steps back, within the current paused stepping session. + if (IsKeyPressed(KEY_PERIOD) && liveWorld) { liveStepForward(); setStatus("Step forward"); } + if (IsKeyPressed(KEY_COMMA) && liveWorld) { liveStepBack(); setStatus(wxUndo.empty() ? "Step back (sky only)" : "Step back"); } if (IsKeyPressed(KEY_F)) { // fast-forward to settled if (!settled) { while (!settled) stepOnce(); diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index e069069..41e7b01 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -100,6 +100,10 @@ public: const std::vector& cloud() const { return sCloud; } const std::vector& rain() const { return sRain; } const std::vector& storms() const { return sStorms; } + // Snapshot / restore the full weather state (humidity/cloud/rain/storms/RNG) for the viewer's + // step-back undo history -- weather is an integrated path, so backward stepping restores a frame. + WeatherSnapshot captureWeather() const; + void restoreWeather(const WeatherSnapshot& s); // Phase 3 (biomes): classify every cell into a Biome from elevation + the climate // fields (temperature + normalized precipitation). Derived + written back into diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index b66bd5e..7af9ca9 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -64,6 +64,14 @@ struct Moon { double dispRadius = 0.10; // display sphere radius (visual size) }; +// A full snapshot of the (integrated, non-analytic) weather state, for the viewer's step-back +// undo history -- weather can't be reversed in closed form, so we restore a saved frame instead. +struct WeatherSnapshot { + std::vector humidity, cloud, rain; + std::vector storms; + uint32_t rng = 0, nextId = 0; +}; + struct Plate { int id = 0; PlateType type = PlateType::Oceanic; // initial crust type seeded onto cells diff --git a/src/sim/PlanetWeather.cpp b/src/sim/PlanetWeather.cpp index 845f6c5..1efe8a6 100644 --- a/src/sim/PlanetWeather.cpp +++ b/src/sim/PlanetWeather.cpp @@ -26,6 +26,19 @@ void Planet::initWeather() { sHasWeather = true; } +WeatherSnapshot Planet::captureWeather() const { + WeatherSnapshot s; + s.humidity = sHumidity; s.cloud = sCloud; s.rain = sRain; + s.storms = sStorms; s.rng = sWeatherRng; s.nextId = sStormNextId; + return s; +} + +void Planet::restoreWeather(const WeatherSnapshot& s) { + sHumidity = s.humidity; sCloud = s.cloud; sRain = s.rain; + sStorms = s.storms; sWeatherRng = s.rng; sStormNextId = s.nextId; + sHasWeather = !sHumidity.empty(); +} + 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) diff --git a/test_weather.cpp b/test_weather.cpp index 756a6bf..481d906 100644 --- a/test_weather.cpp +++ b/test_weather.cpp @@ -119,6 +119,20 @@ int main() { rt = false; check(rt, "save v10 round-trips the weather state"); + std::printf("Weather: snapshot round-trip (step-back undo)\n"); + { + Planet wc; wc.generate(cfg); wc.initWeather(); + for (int k = 0; k < 60; ++k) { wc.computeInsolation(0.25, std::fmod(0.3 + 0.01 * k, 1.0)); wc.stepWeather(1.0); } + WeatherSnapshot snap = wc.captureWeather(); + int s0 = (int)wc.storms().size(); + for (int k = 0; k < 30; ++k) { wc.computeInsolation(0.25, std::fmod(0.9 + 0.01 * k, 1.0)); wc.stepWeather(1.0); } + wc.restoreWeather(snap); // step back to the saved frame + bool rt = ((int)wc.storms().size() == s0); + for (int i = 0; i < n && rt; ++i) + if (wc.cloud()[i] != snap.cloud[i] || wc.humidity()[i] != snap.humidity[i] || wc.rain()[i] != snap.rain[i]) rt = false; + check(rt, "captureWeather/restoreWeather round-trips the full weather state"); + } + std::printf(failures ? "\nSOME WEATHER CHECKS FAILED (%d)\n" : "\nALL WEATHER CHECKS PASSED\n", failures); return failures ? 1 : 0; }