From 47fee0b25f3ea2cdf437144e4bd923f686623374 Mon Sep 17 00:00:00 2001 From: Jonas Reith Date: Sun, 28 Jun 2026 18:32:02 +0200 Subject: [PATCH] Live World viewer controls: storm follow-cam, 2D map zoom, clock stepper Three viewer features over the Live World sim: * Storm follow-cam (Y): the 3D camera locks onto a weather system and keeps it centred as it moves, by pointing along rotateZ(storm.pos,+axialTilt) (model->world) via camYaw/camPitch. Tracked by a new stable WeatherSystem.id (assigned at spawn from sStormNextId; transient, no RNG/determinism impact). Cycles by descending strength, off after the last; orbit-drag disabled while following; auto-releases if the storm dissipates; wheel still zooms. * 2D map zoom/pan: a virtual projection rect (mapViewRect = mapRect scaled about its centre + mapPanX/Y) routes every map projection call while the scissor/frame stay mapRect (drawMapTris now derives y from the rect, not the fixed m.pos, so both axes zoom). Wheel over the map zooms toward the cursor (1-8x); drag pans when zoomed, else rotates mapLon; 2D picking inverts the same rect. * Live-clock stepper: the stepSim live body is factored into liveAdvance(dtClock,dtWeather). '.' steps forward and ',' back by liveRate hours; backward rewinds the deterministic sky (day/night, tides, seasons, moon phases) but holds weather (not reversible). S in Live World aliases the forward step (no longer runs a stray tectonic tick). All five headless suites pass; GUI build clean. Docs updated. Co-Authored-By: Claude Opus 4.8 --- BUILD.md | 5 ++- CLAUDE.md | 21 +++++++++- docs/design-notes.md | 22 +++++++++++ src/render/Map2D.cpp | 12 ++++-- src/render/Viewer.cpp | 55 ++++++++++++++++---------- src/render/Viewer.hpp | 5 +++ src/render/ViewerInput.cpp | 78 +++++++++++++++++++++++++++++++++---- src/render/ViewerRender.cpp | 39 +++++++++++-------- src/sim/Planet.hpp | 3 +- src/sim/PlanetTypes.hpp | 1 + src/sim/PlanetWeather.cpp | 3 +- 11 files changed, 192 insertions(+), 52 deletions(-) diff --git a/BUILD.md b/BUILD.md index 74256bd..8a0a193 100644 --- a/BUILD.md +++ b/BUILD.md @@ -52,9 +52,12 @@ the full ~2.8x speedup; the default uses all cores for no extra gain: 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) + 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) + 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) - S single tectonic tick + S single tectonic tick (in Live World: step the clock forward, like '.') F fast-forward Phase-1 forming to settled (instant) R reseed planet (restart forming) + / - subdivision level (detail), 1..7 diff --git a/CLAUDE.md b/CLAUDE.md index 7adc79b..fb425b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -412,6 +412,19 @@ Working and verified (logic tested headless): 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. +- **Live World viewer controls — storm follow-cam, 2D map zoom, clock stepper:** (1) **`Y`** cycles + the 3D camera to **follow a storm** (by descending strength, off after the last). Tracked by a + stable `WeatherSystem.id` (assigned at spawn from `sStormNextId`; transient, not RNG); each frame + `handleInput` points the camera straight at it via `camYaw/camPitch` from `rotateZ(pos,+axialTilt)` + (model→world), orbit-drag disabled while following, auto-release if it dissipates. (2) **2D map + zoom**: `mapZoom`/`mapPanX`/`mapPanY` + `Viewer::mapViewRect()` (mapRect scaled about its centre + + pan); every map projection call routes through it while the **scissor/frame stay `mapRect`** + (`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). - 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 @@ -550,7 +563,10 @@ all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured co `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 · +`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) · +`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, on a settled world; re-press regenerates) · `W` enter/leave **Live World** (settled world) · `R` reseed · @@ -568,6 +584,9 @@ moving day/night terminator (`N`), a live seasonal temperature cycle and a movin line animate over whatever colour mode is active; the HUD shows a `Year/Day/HH:MM` calendar. **1–3 moons** orbit (sun-lit phases, orbit rings, solar/lunar eclipses) and, with the distant sun, raise tides — `T` colours the coastline by the live tide level (amber low ↔ cyan high). +`K` shows moving weather (clouds, rain, drifting storms / hurricanes). `Y` makes the 3D camera +**follow a storm** (cycles by strength, off after the last); `.`/`,` step the clock forward/back +by one rate-unit (back rewinds the sky only). Mouse-wheel over the 2D map zooms (drag pans). CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config file (both applied before the initial load/generate). diff --git a/docs/design-notes.md b/docs/design-notes.md index 564a9bf..b5986c8 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -202,6 +202,28 @@ recurve (`weatherSystemSpeed`), **intensify** over warm sea / **decay+cull** ove 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`. +## Live World viewer controls (follow-cam, 2D zoom, clock stepper) + +Three viewer-only controls over the Live World sim: +- **Storm follow-cam** (`Y`): the globe is at the origin and the camera orbits it, so to centre a + storm we point the camera **along the storm's world direction** — `rotateZ(storm.pos, +axialTilt)` + (model→world; `Picking.hpp`), then `camPitch=asin(d.y)`, `camYaw=atan2(d.x,d.z)`. Tracked by a + stable `WeatherSystem.id` (assigned at spawn; transient, no RNG/determinism impact). Orbit-drag is + disabled while following; wheel-zoom still works; cycles by descending strength, auto-releases if + the storm dissipates. +- **2D map zoom** (`mapZoom`/`mapPanX`/`mapPanY`): implemented as a **virtual projection rect**, + `Viewer::mapViewRect()` = `mapRect` scaled about its centre + pan. Every map projection call + (`drawMap2D`/`drawWeather2D`/`drawSegments2D`/graticule/markers/`mapScreen` + the 2D hover-pick) + takes this `vr` instead of `mapRect`, while the **scissor + frame stay `mapRect`** so it clips to + the panel — no Map2D signature changes. `drawMapTris` was changed to derive the y-coordinate from + the rect (not the fixed-to-mapRect `m.pos`) so both axes zoom. Wheel zooms toward the cursor (1–8×); + 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). + ## Headless testing Engine is raylib-free, so logic is tested without a display. Build/run: diff --git a/src/render/Map2D.cpp b/src/render/Map2D.cpp index 258d5b2..cb163fe 100644 --- a/src/render/Map2D.cpp +++ b/src/render/Map2D.cpp @@ -32,11 +32,17 @@ Vector2 mapScreen(const Map2D& m, int idx, 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(); + double hw = EqualEarth::halfWidth(), hh = EqualEarth::halfHeight(); auto px = [&](double lon, double lat) -> float { double x, y; EqualEarth::forward(lon, lat, x, y); return (float)(r.x + (x / hw * 0.5 + 0.5) * r.width); }; + // y depends only on latitude in Equal Earth; derive it from r (so zoom/pan via a virtual + // rect applies to both axes -- not from the precomputed m.pos, which is fixed to mapRect). + auto py = [&](double lat) -> float { + double x, y; EqualEarth::forward(0.0, lat, x, y); + return (float)(r.y + (0.5 - y / hh * 0.5) * r.height); + }; const std::vector& tri = p.triIndices(); rlDisableBackfaceCulling(); rlBegin(RL_TRIANGLES); @@ -50,7 +56,7 @@ static void drawMapTris(const Planet& p, const Map2D& m, Rectangle r, double lon for (int t = 0; t < 3; ++t) { 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]]), py(m.lat[v[t]])); } } else { // antimeridian seam double ul[3] = { lo[0], lo[1], lo[2] }; // unwrap around v0 @@ -64,7 +70,7 @@ static void drawMapTris(const Planet& p, const Map2D& m, Rectangle r, double lon for (int t = 0; t < 3; ++t) { 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]]), py(m.lat[v[t]])); } } } diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index 96ed0e3..59bccdf 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; // reseed/regen drops back to World Creation + liveWorld = false; followId = 0; // 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,25 +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 - liveTime += dtH; - double days = liveTime / planet.cfg.dayLengthHours; - double dayOfYear01 = days / planet.cfg.yearLengthDays; - dayOfYear01 -= std::floor(dayOfYear01); - double timeOfDay01 = days - std::floor(days); - planet.computeInsolation(dayOfYear01, timeOfDay01); - planet.computeLiveSeason(dayOfYear01); - planet.computeTides(dayOfYear01, timeOfDay01, days); - Vec3 s = planet.sunDirection(dayOfYear01, timeOfDay01); // shared sky geometry - sunDir = Vector3{ (float)s.x, (float)s.y, (float)s.z }; - moonDirs.clear(); moonNormals.clear(); - for (int m = 0; m < (int)planet.getMoons().size(); ++m) { - Vec3 md = planet.moonDirection(m, timeOfDay01, days); - Vec3 mn = planet.moonOrbitNormal(m, timeOfDay01); - moonDirs.push_back(Vector3{ (float)md.x, (float)md.y, (float)md.z }); - moonNormals.push_back(Vector3{ (float)mn.x, (float)mn.y, (float)mn.z }); - } - planet.stepWeather(dtH); // dynamic clouds & rain on the live clock - rebuildLiveOverlay(); + liveAdvance(dtH, dtH); return; } if (!paused && !settled) { @@ -357,6 +339,39 @@ void Viewer::stepSim() { } } +// Advance the Live World clock by dtClock hours and recompute the derived fields. Weather is an +// integrated, non-reversible path, so it advances by dtWeather (0 = hold, used for a backward +// step which still rewinds the deterministic sky: day/night, tides, seasons, moon phases). +void Viewer::liveAdvance(double dtClock, double dtWeather) { + liveTime = std::max(0.0, liveTime + dtClock); + double days = liveTime / planet.cfg.dayLengthHours; + double dayOfYear01 = days / planet.cfg.yearLengthDays; dayOfYear01 -= std::floor(dayOfYear01); + double timeOfDay01 = days - std::floor(days); + planet.computeInsolation(dayOfYear01, timeOfDay01); + planet.computeLiveSeason(dayOfYear01); + planet.computeTides(dayOfYear01, timeOfDay01, days); + Vec3 s = planet.sunDirection(dayOfYear01, timeOfDay01); + sunDir = Vector3{ (float)s.x, (float)s.y, (float)s.z }; + moonDirs.clear(); moonNormals.clear(); + for (int m = 0; m < (int)planet.getMoons().size(); ++m) { + Vec3 md = planet.moonDirection(m, timeOfDay01, days); + Vec3 mn = planet.moonOrbitNormal(m, timeOfDay01); + moonDirs.push_back(Vector3{ (float)md.x, (float)md.y, (float)md.z }); + moonNormals.push_back(Vector3{ (float)mn.x, (float)mn.y, (float)mn.z }); + } + planet.stepWeather(dtWeather); + rebuildLiveOverlay(); +} + +// 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 { + float w = (float)(mapRect.width * mapZoom), h = (float)(mapRect.height * mapZoom); + float x = mapRect.x + (mapRect.width - w) * 0.5f + (float)mapPanX; + float y = mapRect.y + (mapRect.height - h) * 0.5f + (float)mapPanY; + return Rectangle{ x, y, w, h }; +} + void Viewer::run() { while (!WindowShouldClose()) { handleInput(); diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index 807082e..91379cd 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -103,7 +103,10 @@ struct Viewer { // Input state. float dragDist = 0.0f; double mapLon = 0.0; // 2D map longitude pan (radians) + double mapZoom = 1.0; // 2D map zoom factor (1 = whole map; up to 8x) + double mapPanX = 0.0, mapPanY = 0.0; // 2D map screen-space pan (pixels, used when zoomed) bool pressInMap = false; // a drag that started on the map pans it + uint32_t followId = 0; // Live World: id of the storm the 3D camera follows (0 = none) // Per-frame picking state (set by handleInput, read by render). Vector2 mp{}; @@ -134,6 +137,8 @@ struct Viewer { void saveGame(const char* path); 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 + Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect) // ---- Input (ViewerInput.cpp) -------------------------------------------- void handleInput(); diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index eda3a60..aa2062f 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -34,19 +34,56 @@ void Viewer::handleInput() { if (onPause) pauseAction(); // clickable pause / re-evolve button } } + // Live World: is the camera following a storm? (look up by stable id; release if dissipated) + const WeatherSystem* followed = nullptr; + if (liveWorld && followId != 0) { + for (const auto& ws : planet.storms()) if (ws.id == followId) { followed = &ws; break; } + if (!followed) followId = 0; + } + bool following = (followed != nullptr); + if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !phase3Prompt) { Vector2 d = GetMouseDelta(); dragDist += fabsf(d.x) + fabsf(d.y); - if (in3D && !onPause) { // only orbit from the 3D quadrant + if (in3D && !onPause && !following) { // orbit (disabled while following a storm) camYaw += d.x * 0.005f; camPitch += d.y * 0.005f; camPitch = std::clamp(camPitch, -1.5f, 1.5f); } - if (pressInMap) // drag the map east/west - mapLon = wrapPi(mapLon + d.x * (2.0 * M_PI / mapRect.width)); + if (pressInMap) { // drag the map: pan when zoomed, else rotate lon + if (mapZoom > 1.0) { + double w = mapRect.width * mapZoom, h = mapRect.height * mapZoom; + mapPanX = std::clamp(mapPanX + d.x, -(w - mapRect.width) * 0.5, (w - mapRect.width) * 0.5); + mapPanY = std::clamp(mapPanY + d.y, -(h - mapRect.height) * 0.5, (h - mapRect.height) * 0.5); + } else { + mapLon = wrapPi(mapLon + d.x * (2.0 * M_PI / mapRect.width)); + } + } + } + + // Wheel: zoom the 2D map toward the cursor when hovering it, else zoom the camera. + float wheel = GetMouseWheelMove(); + if (inMap && wheel != 0.0f) { + Rectangle vr = mapViewRect(); + double u = (mp.x - vr.x) / vr.width, v = (mp.y - vr.y) / vr.height; // projection coord under cursor + double nz = std::clamp(mapZoom * (wheel > 0 ? 1.2 : 1.0 / 1.2), 1.0, 8.0); + if (nz <= 1.0001) { mapZoom = 1.0; mapPanX = mapPanY = 0.0; } // back to the whole map + else { + double w = mapRect.width * nz, h = mapRect.height * nz; + mapPanX = std::clamp(mp.x - u * w - mapRect.x - (mapRect.width - w) * 0.5, -(w - mapRect.width) * 0.5, (w - mapRect.width) * 0.5); + mapPanY = std::clamp(mp.y - v * h - mapRect.y - (mapRect.height - h) * 0.5, -(h - mapRect.height) * 0.5, (h - mapRect.height) * 0.5); + mapZoom = nz; + } + } else { + camDist -= wheel * 0.4f; + camDist = std::clamp(camDist, 2.6f, 14.0f); + } + + if (following) { // point the camera straight at the storm + Vec3 wd = rotateZ(followed->pos, planet.cfg.axialTilt); // model -> world (axial tilt) + camPitch = std::clamp((float)std::asin(std::clamp(wd.y, -1.0, 1.0)), -1.5f, 1.5f); + camYaw = (float)std::atan2(wd.x, wd.z); } - camDist -= GetMouseWheelMove() * 0.4f; - camDist = std::clamp(camDist, 2.6f, 14.0f); cam.position = { camDist * cosf(camPitch) * sinf(camYaw), camDist * sinf(camPitch), camDist * cosf(camPitch) * cosf(camYaw) }; @@ -75,7 +112,8 @@ void Viewer::handleInput() { hovered = nearestCell(planet, hitModel); } } else if (inMap) { - double nx = (mp.x - mapRect.x) / mapRect.width, ny = (mp.y - mapRect.y) / mapRect.height; + Rectangle vr = mapViewRect(); // account for 2D zoom/pan + double nx = (mp.x - vr.x) / vr.width, ny = (mp.y - vr.y) / vr.height; double X = (nx * 2.0 - 1.0) * EqualEarth::halfWidth(); double Y = (1.0 - 2.0 * ny) * EqualEarth::halfHeight(); double lon, lat; @@ -138,17 +176,41 @@ void Viewer::handleInput() { refreshView(); // fresh base colours; overlay builds in stepSim setStatus("Live World started"); } else { - paused = true; refreshView(); // back to World Creation (drift), paused + paused = true; followId = 0; refreshView(); // back to World Creation (drift), paused setStatus("Live World stopped"); } } + if (IsKeyPressed(KEY_Y) && liveWorld) { // cycle the 3D camera through active storms + const auto& st = planet.storms(); + if (st.empty()) { followId = 0; setStatus("No weather systems to follow"); } + else { + std::vector idx(st.size()); for (size_t i = 0; i < st.size(); ++i) idx[i] = (int)i; + std::sort(idx.begin(), idx.end(), [&](int a, int b){ return st[a].strength > st[b].strength; }); + int cur = -1; for (size_t k = 0; k < idx.size(); ++k) if (st[idx[k]].id == followId) { cur = (int)k; break; } + int next = (cur < 0) ? 0 : cur + 1; + if (next >= (int)idx.size()) { followId = 0; setStatus("Follow cam off"); } + else { + const WeatherSystem& ws = st[idx[next]]; + followId = ws.id; + bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr; + setStatus(hur ? "Following tropical cyclone" : "Following weather system"); + } + } + } 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) + if (IsKeyPressed(KEY_S)) { // one step + if (liveWorld) liveAdvance(liveRate, liveRate); // 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)"); } if (IsKeyPressed(KEY_F)) { // fast-forward to settled if (!settled) { while (!settled) stepOnce(); diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index a41a6a6..fed7cc3 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -243,45 +243,48 @@ void Viewer::renderGlobe3D() { // 2D Equal Earth map + its overlays (borders/drift/rivers/labels/markers). void Viewer::renderMap2D() { DrawRectangleRec(mapRect, Color{6, 8, 14, 255}); + Rectangle vr = mapViewRect(); // projection rect (zoom/pan); scissor stays mapRect BeginScissorMode((int)mapRect.x, (int)mapRect.y, (int)mapRect.width, (int)mapRect.height); - drawMap2D(planet, displayColors(), map2D, mapRect, mapLon); - if (showGrat) { drawGraticule2D(graticule, mapRect, mapLon); drawGraticuleLabels2D(mapRect, mapLon); } - if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f, mapRect, mapLon); - if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f, mapRect, mapLon); - if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, mapRect, mapLon); - if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, mapRect, mapLon); - if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, mapRect, mapLon); - if (liveWorld && showClouds && !planet.cloud().empty()) drawWeather2D(planet, planet.cloud(), planet.rain(), map2D, mapRect, mapLon); + drawMap2D(planet, displayColors(), map2D, vr, mapLon); + if (showGrat) { drawGraticule2D(graticule, vr, mapLon); drawGraticuleLabels2D(vr, mapLon); } + if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f, vr, mapLon); + if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f, vr, mapLon); + if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, vr, mapLon); + if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, vr, mapLon); + if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, vr, mapLon); + if (liveWorld && showClouds && !planet.cloud().empty()) drawWeather2D(planet, planet.cloud(), planet.rain(), map2D, vr, 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); + Vector2 sp = projLonLat(lon, lat, mapLon, vr); 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; + float rad = (5.0f + 10.0f * (float)ws.strength) * (float)std::min(2.0, mapZoom); 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) { - drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, mapRect, mapLon); - drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, mapRect, mapLon); + drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, vr, mapLon); + drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, vr, mapLon); } if (showDrift && !plateLabels.empty()) { for (const auto& lbl : plateLabels) { Vec3 u = Vec3{lbl.pos.x, lbl.pos.y, lbl.pos.z}.normalized(); double lon, lat; dirToLonLat(u, lon, lat); - Vector2 lp = projLonLat(lon, lat, mapLon, mapRect); + Vector2 lp = projLonLat(lon, lat, mapLon, vr); const char* txt = TextFormat("P%d", lbl.id); DrawText(txt, (int)lp.x + 4, (int)lp.y - 8, 12, RAYWHITE); } } - if (selectedCell >= 0) DrawCircleV(mapScreen(map2D, selectedCell, mapRect, mapLon), 5, ORANGE); - if (hovered >= 0) DrawCircleV(mapScreen(map2D, hovered, mapRect, mapLon), 4, YELLOW); + if (selectedCell >= 0) DrawCircleV(mapScreen(map2D, selectedCell, vr, mapLon), 5, ORANGE); + if (hovered >= 0) DrawCircleV(mapScreen(map2D, hovered, vr, mapLon), 4, YELLOW); EndScissorMode(); DrawRectangleLinesEx(mapRect, 1, Color{90, 90, 110, 255}); - DrawText("2D Equal Earth (hover, drag to pan)", (int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255}); + DrawText(mapZoom > 1.0 ? TextFormat("2D Equal Earth (zoom %.1fx, drag to pan, wheel to zoom)", mapZoom) + : "2D Equal Earth (hover, drag to pan, wheel to zoom)", + (int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255}); } // Live World "Sky & tides" panel in the freed space right of the (left-aligned) 2D map: @@ -445,7 +448,9 @@ void Viewer::renderHUD() { 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)); + line(TextFormat("weather systems: %d tropical cyclones: %d%s", nStorm, nHur, + followId ? " [following]" : "")); + line("Y follow storm · . / , step clock +/- · wheel-on-map zoom"); } else { line(TextFormat("%s %.1f My elapsed %.1f My/s%s", diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index 1c0a724..e069069 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -219,7 +219,8 @@ private: // Moving weather systems (transient agents; not saved). Separate RNG keeps tectonic // determinism intact (seeded from cfg.seed in initWeather). std::vector sStorms; - uint32_t sWeatherRng = 1; + uint32_t sWeatherRng = 1; + uint32_t sStormNextId = 1; // monotonic id for follow-cam tracking // 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/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index 5dd9a97..b66bd5e 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -30,6 +30,7 @@ enum class PlateType { Oceanic, Continental }; // 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 { + uint32_t id = 0; // stable id (for the viewer follow-cam; assigned at spawn) 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) diff --git a/src/sim/PlanetWeather.cpp b/src/sim/PlanetWeather.cpp index 9ac70f2..845f6c5 100644 --- a/src/sim/PlanetWeather.cpp +++ b/src/sim/PlanetWeather.cpp @@ -21,7 +21,7 @@ void Planet::initWeather() { 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(); + sStorms.clear(); sStormNextId = 1; sWeatherRng = cfg.seed ? (cfg.seed ^ 0x5701A123u) : 0x5701A123u; // separate RNG sHasWeather = true; } @@ -126,6 +126,7 @@ void Planet::stepWeather(double dtHours) { 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; + ws.id = sStormNextId++; sStorms.push_back(ws); } }