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 <noreply@anthropic.com>
This commit is contained in:
parent
ca844a5d3f
commit
47fee0b25f
5
BUILD.md
5
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
|
||||
|
||||
21
CLAUDE.md
21
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).
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 <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(), 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<int>& 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]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<int> 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();
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<WeatherSystem> 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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user