448 lines
24 KiB
C++
448 lines
24 KiB
C++
#include "Viewer.hpp"
|
|
#include "Picking.hpp" // angBetween (rebuildSub)
|
|
#include "Projection.hpp" // EqualEarth (layout)
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
|
|
bool Viewer::init(int argc, char** argv) {
|
|
uint32_t cliSeed = 0; // 0 = no --seed given
|
|
for (int a = 1; a < argc; ++a) {
|
|
if (!std::strcmp(argv[a], "--seed") && a + 1 < argc)
|
|
cliSeed = (uint32_t)std::strtoul(argv[++a], nullptr, 10);
|
|
else if (!std::strcmp(argv[a], "--config") && a + 1 < argc)
|
|
configPath = argv[++a];
|
|
}
|
|
|
|
SetConfigFlags(FLAG_MSAA_4X_HINT);
|
|
InitWindow(screenW, screenH, "Planet Sim - Phase 1: Tectonics");
|
|
SetTargetFPS(60);
|
|
|
|
// Layout: left column 70% wide (3D globe 60% h on top, 2D map 40% h below);
|
|
// right column 30% wide (cell info 50% h on top, subareas 50% below).
|
|
leftW = (int)(screenW * 0.70f); // 1344
|
|
rightX = leftW; rightW = screenW - leftW; // 576
|
|
rightH = screenH / 2; // 540
|
|
|
|
// Top-left: 3D globe (render texture, its own aspect).
|
|
view3DW = leftW; view3DH = (int)(screenH * 0.60f); // 1344 x 648
|
|
rt3d = LoadRenderTexture(view3DW, view3DH);
|
|
SetTextureFilter(rt3d.texture, TEXTURE_FILTER_BILINEAR);
|
|
|
|
// Bottom-left: 2D Equal Earth map, fit (keep aspect) into the 40% strip.
|
|
const int mapAreaY = view3DH, mapAreaH = screenH - view3DH; // (0,648) 1344 x 432
|
|
int mapH = mapAreaH - 30;
|
|
int mapW = (int)(mapH * (EqualEarth::halfWidth() / EqualEarth::halfHeight()));
|
|
if (mapW > leftW - 30) { mapW = leftW - 30; mapH = (int)(mapW / (EqualEarth::halfWidth() / EqualEarth::halfHeight())); }
|
|
// Left-align the 2D map (was centered) so the freed space at right holds the live sky panel.
|
|
const float mapMargin = 16.0f;
|
|
mapRect = Rectangle{ mapMargin,
|
|
(float)(mapAreaY + (mapAreaH - mapH) / 2),
|
|
(float)mapW, (float)mapH };
|
|
float liveX = mapRect.x + mapRect.width + 16.0f;
|
|
liveInfoRect = Rectangle{ liveX, mapRect.y, (float)leftW - liveX - 8.0f, mapRect.height };
|
|
|
|
// Right column.
|
|
hoverRect = Rectangle{ (float)rightX + 8, 8.0f, (float)rightW - 16, (float)rightH - 16 };
|
|
panelRect = Rectangle{ (float)rightX + 8, (float)rightH + 8, (float)rightW - 16, (float)rightH - 16 };
|
|
const float panelHeader = 120.0f;
|
|
const float gridSide = std::min(panelRect.width - 40.0f, panelRect.height - panelHeader - 56.0f);
|
|
gridRect = Rectangle{ panelRect.x + (panelRect.width - gridSide) / 2.0f,
|
|
panelRect.y + panelHeader, gridSide, gridSide };
|
|
|
|
// Buttons (pause + Phase-3 prompt, centered in the 3D viewport).
|
|
pauseBtn = Rectangle{ 16.0f, (float)view3DH - 44.0f, 160.0f, 32.0f };
|
|
const float pbW = 220.0f, pbH = 42.0f, pbGap = 24.0f;
|
|
pbCx = view3DW * 0.5f; pbCy = view3DH * 0.5f;
|
|
p3ContinueBtn = Rectangle{ pbCx - pbW - pbGap * 0.5f, pbCy + 8.0f, pbW, pbH };
|
|
p3StartBtn = Rectangle{ pbCx + pbGap * 0.5f, pbCy + 8.0f, pbW, pbH };
|
|
|
|
cfg.subdivisions = 5;
|
|
if (!loadConfig(configPath, cfg)) saveConfig(configPath, cfg); // load, or create a default
|
|
if (cliSeed != 0) cfg.seed = cliSeed; // CLI --seed overrides config
|
|
std::string cfgErr = validateConfig(cfg);
|
|
if (!cfgErr.empty()) cfg = PlanetConfig{}; // revert to safe defaults
|
|
planet.generate(cfg);
|
|
|
|
cam.position = {0, 0, 6}; cam.target = {0, 0, 0}; cam.up = {0, 1, 0};
|
|
cam.fovy = 45; cam.projection = CAMERA_PERSPECTIVE;
|
|
|
|
buildBorders(planet, borderR, borders, ridgeBorders);
|
|
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
|
graticule = buildGraticule();
|
|
buildMap2D(planet, mapRect, map2D);
|
|
phase3PromptAt = planet.cfg.phase3AfterMy;
|
|
|
|
refreshView(); // colour the freshly generated (flat) world
|
|
return true;
|
|
}
|
|
|
|
void Viewer::rebuildSub() {
|
|
subgrids.clear();
|
|
if (selectedCell < 0) return;
|
|
subgrids.push_back(planet.makeSubGrid(selectedCell, subRes));
|
|
const Cell& c = planet.cells[selectedCell];
|
|
for (int nb : c.neighbors) subgrids.push_back(planet.makeSubGrid(nb, subRes));
|
|
double ma = 0.0;
|
|
for (int nb : c.neighbors) ma += angBetween(c.unit, planet.cells[nb].unit);
|
|
ma /= std::max<size_t>(1, c.neighbors.size());
|
|
selectedThresh = ma * 1.4;
|
|
}
|
|
|
|
void Viewer::selectCell(int idx) {
|
|
if (idx < 0) return;
|
|
if (idx == selectedCell) { selectedCell = -1; subgrids.clear(); return; }
|
|
selectedCell = idx; rebuildSub();
|
|
}
|
|
|
|
// Recolor the mesh + refresh the elevation range, read straight from cells.
|
|
void Viewer::recolor() {
|
|
double maxAge = 1.0; for (const auto& c : planet.cells) maxAge = std::max(maxAge, c.geoAge);
|
|
const std::vector<double>& temp = planet.temperature();
|
|
const std::vector<double>& summer = planet.summerTemp();
|
|
const std::vector<double>& winter = planet.winterTemp();
|
|
const std::vector<double>& moist = planet.moisture(); // 0..1, already robustly normalized
|
|
const std::vector<double>& flora = planet.floraDensity();
|
|
const std::vector<double>& fauna = planet.faunaDensity();
|
|
const std::vector<double>& funga = planet.fungaDensity();
|
|
vcolors.resize(planet.cells.size());
|
|
for (size_t i = 0; i < planet.cells.size(); ++i) {
|
|
switch (mode) {
|
|
case ColorMode::Plate: {
|
|
int pid = planet.cells[i].plateId;
|
|
vcolors[i] = (pid >= 0 && pid < (int)planet.plates.size() && planet.plates[pid].baby)
|
|
? Color{70, 80, 95, 255} // young spreading-ridge crust
|
|
: plateColor(pid);
|
|
break;
|
|
}
|
|
case ColorMode::Age: vcolors[i] = ageColor(planet.cells[i].geoAge, maxAge); break;
|
|
case ColorMode::Crust: vcolors[i] = crustColor(planet.cells[i].oceanic); break;
|
|
case ColorMode::Biome: vcolors[i] = biomeColor(planet.cells[i].biome); break;
|
|
case ColorMode::Temperature: vcolors[i] = temp.empty() ? Color{90,90,90,255} : tempColor(temp[i]); break;
|
|
case ColorMode::TempSummer: vcolors[i] = summer.empty()? Color{90,90,90,255} : tempColor(summer[i]); break;
|
|
case ColorMode::TempWinter: vcolors[i] = winter.empty()? Color{90,90,90,255} : tempColor(winter[i]); break;
|
|
case ColorMode::Seasonality: vcolors[i] = (summer.empty()||winter.empty()) ? Color{90,90,90,255}
|
|
: seasonColor(summer[i] - winter[i]); break;
|
|
case ColorMode::Precip: vcolors[i] = moist.empty() ? Color{90,90,90,255} : precipColor(moist[i]); break;
|
|
case ColorMode::FloraDensity: vcolors[i] = flora.empty() ? Color{90,90,90,255} : floraColor(flora[i]); break;
|
|
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255} : faunaColor(fauna[i]); break;
|
|
case ColorMode::FungaDensity: vcolors[i] = funga.empty() ? Color{90,90,90,255} : fungaColor(funga[i]); break;
|
|
default: vcolors[i] = elevationColor(planet.cells[i].elevation, planet.cfg.seaLevel);
|
|
}
|
|
}
|
|
// Phase 3: shade filled basins above sea level as inland water (lakes).
|
|
const std::vector<double>& lk = planet.lakeDepth();
|
|
if (phase3 && !lk.empty())
|
|
for (size_t i = 0; i < planet.cells.size(); ++i)
|
|
if (lk[i] > 20.0 && planet.cells[i].elevation > planet.cfg.seaLevel)
|
|
vcolors[i] = lakeColor();
|
|
minE = planet.minElevation(); maxE = planet.maxElevation();
|
|
}
|
|
|
|
// Live World: from the sim's insolation + live-temperature fields, build the per-cell
|
|
// day/night brightness (illum) and the shaded draw colours (base colour -> snow/ice tint ->
|
|
// day/night dim). Cheap O(n); called every frame while in Live World.
|
|
void Viewer::rebuildLiveOverlay() {
|
|
const size_t n = planet.cells.size();
|
|
const std::vector<double>& sun = planet.insolation();
|
|
const std::vector<double>& lt = planet.liveTemp();
|
|
const double sea = planet.cfg.seaLevel;
|
|
const double snowT = planet.cfg.snowTemp;
|
|
const double iceT = planet.cfg.seaIceTemp;
|
|
const float nightFloor = 0.18f; // night side dim (not black) so colours read
|
|
|
|
illum.assign(n, 1.0f);
|
|
shadedColors.resize(n);
|
|
auto smoothstep = [](double e0, double e1, double x) {
|
|
double t = (e1 > e0) ? (x - e0) / (e1 - e0) : 0.0;
|
|
t = t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t);
|
|
return t * t * (3.0 - 2.0 * t);
|
|
};
|
|
// Solar eclipse: a moon roughly between the sun and the planet (its model-space direction
|
|
// near the sun's) casts a shadow around the sub-solar point. Strength ramps with alignment.
|
|
const Vec3 sd{ sunDir.x, sunDir.y, sunDir.z };
|
|
const double eclipseReach = 0.13; // rad: how close a moon must be to the sun to eclipse
|
|
const double umbra = 0.10; // rad: angular radius of the shadow spot
|
|
double eclipseStrength = 0.0;
|
|
for (const auto& md : moonDirs) {
|
|
double d = std::acos(std::clamp((double)(md.x*sd.x + md.y*sd.y + md.z*sd.z), -1.0, 1.0));
|
|
if (d < eclipseReach) eclipseStrength = std::max(eclipseStrength, 1.0 - d / eclipseReach);
|
|
}
|
|
auto blend = [](unsigned char c, unsigned char to, double a) {
|
|
return (unsigned char)(c + (to - c) * a);
|
|
};
|
|
for (size_t i = 0; i < n; ++i) {
|
|
// Day/night: soft sunrise band over the clamped cosine incidence.
|
|
float f = nightFloor;
|
|
if (!sun.empty()) f = nightFloor + (1.0f - nightFloor) * (float)smoothstep(0.0, 0.12, sun[i]);
|
|
// Eclipse shadow: darken cells near the sub-solar point while a moon transits the sun.
|
|
if (eclipseStrength > 0.0 && !sun.empty()) {
|
|
double dd = std::acos(std::clamp(planet.cells[i].unit.x*sd.x + planet.cells[i].unit.y*sd.y
|
|
+ planet.cells[i].unit.z*sd.z, -1.0, 1.0));
|
|
double sh = eclipseStrength * std::exp(-(dd / umbra) * (dd / umbra));
|
|
f *= (float)std::max(0.10, 1.0 - 0.85 * sh);
|
|
}
|
|
illum[i] = f;
|
|
|
|
Color c = vcolors[i];
|
|
// Snow on cold land, sea ice on cold ocean (live seasonal temperature).
|
|
if (!lt.empty()) {
|
|
double e = planet.cells[i].elevation;
|
|
if (e > sea) {
|
|
double a = (snowT - lt[i]) / 8.0; // fully snow ~8 C below freezing
|
|
if (a > 0.0) { a = a > 0.85 ? 0.85 : a;
|
|
c = Color{ blend(c.r, 242, a), blend(c.g, 246, a), blend(c.b, 250, a), 255 }; }
|
|
} else {
|
|
double a = (iceT - lt[i]) / 6.0; // sea ice
|
|
if (a > 0.0) { a = a > 0.9 ? 0.9 : a;
|
|
c = Color{ blend(c.r, 212, a), blend(c.g, 226, a), blend(c.b, 236, a), 255 }; }
|
|
}
|
|
}
|
|
// Day/night dimming over the (possibly snow-tinted) colour.
|
|
if (dayNightOn) c = Color{ (unsigned char)(c.r * f), (unsigned char)(c.g * f), (unsigned char)(c.b * f), 255 };
|
|
shadedColors[i] = c;
|
|
}
|
|
}
|
|
|
|
void Viewer::refreshView() {
|
|
if (phase3) planet.computeHydrology(); // refresh lakes/rivers for the view
|
|
planet.computeClimate(); // temperature + precipitation fields
|
|
planet.classifyBiomes(); // keep cell.biome current (reads the climate)
|
|
planet.computeBiotaDensity(); // flora/fauna/funga density (population is on-demand)
|
|
recolor();
|
|
if (settled) { // Phase 2: plates moved -> boundaries moved
|
|
buildBorders(planet, borderR, borders, ridgeBorders);
|
|
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
|
}
|
|
if (phase3) buildRivers(planet, riverR, rivers, bigRivers);
|
|
buildCoastline(planet, riverR, coast, coastOcean); // land/ocean boundary (for tide lines)
|
|
buildCurrents(planet, driftR, currentSegs, currentCols); // ocean current arrows (warm/cold)
|
|
if (selectedCell >= 0) rebuildSub();
|
|
}
|
|
|
|
void Viewer::regenWorld() { // after generate(): geometry changed
|
|
buildBorders(planet, borderR, borders, ridgeBorders);
|
|
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
|
buildMap2D(planet, mapRect, map2D);
|
|
selectedCell = -1; subgrids.clear();
|
|
settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false;
|
|
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();
|
|
elapsedMy = 0.0; dtMy = 0.0; driftAccum = 0.0;
|
|
refreshView();
|
|
}
|
|
|
|
void Viewer::regen() { planet.generate(cfg); regenWorld(); }
|
|
|
|
void Viewer::stepOnce() { // one tick + settle bookkeeping
|
|
maxChange = planet.step(); ++stepCount;
|
|
if (maxChange < settleThresh) { if (++settleRun >= settleNeed) settled = true; }
|
|
else settleRun = 0;
|
|
}
|
|
|
|
void Viewer::pauseAction() { paused = !paused; } // pause/resume forming or drift
|
|
|
|
void Viewer::setStatus(const std::string& m) { statusMsg = m; statusUntil = GetTime() + 3.0; }
|
|
|
|
// F5: write seed + config + full planet state. F9: read it back and resume.
|
|
void Viewer::saveGame(const char* path) {
|
|
std::ofstream os(path, std::ios::binary);
|
|
if (!os) { setStatus("Save failed"); return; }
|
|
uint32_t ver = SAVE_VERSION; uint8_t st = settled ? 1 : 0; uint8_t p3 = phase3 ? 1 : 0;
|
|
uint8_t lw = liveWorld ? 1 : 0;
|
|
os.write("PLSV", 4);
|
|
os.write(reinterpret_cast<const char*>(&ver), sizeof ver);
|
|
os.write(reinterpret_cast<const char*>(&elapsedMy), sizeof elapsedMy);
|
|
os.write(reinterpret_cast<const char*>(&st), sizeof st);
|
|
os.write(reinterpret_cast<const char*>(&driftRate), sizeof driftRate);
|
|
os.write(reinterpret_cast<const char*>(&p3), sizeof p3); // v3: Phase-3 flag
|
|
os.write(reinterpret_cast<const char*>(&lw), sizeof lw); // v8: Live World flag
|
|
os.write(reinterpret_cast<const char*>(&liveTime), sizeof liveTime); // v8: live clock (hours)
|
|
planet.writeState(os);
|
|
// v12: persist the most recent step-back frames so a load can rewind storms past the moment.
|
|
auto wD = [&](const std::vector<double>& v){ uint64_t m = v.size(); os.write((char*)&m, 8); if (m) os.write((const char*)v.data(), (std::streamsize)(m * sizeof(double))); };
|
|
uint32_t hn = (uint32_t)std::min<size_t>(wxUndo.size(), (size_t)wxSaveMax);
|
|
os.write((char*)&hn, 4);
|
|
for (size_t i = wxUndo.size() - hn; i < wxUndo.size(); ++i) {
|
|
const WxFrame& f = wxUndo[i];
|
|
os.write((char*)&f.t, 8);
|
|
wD(f.w.humidity); wD(f.w.cloud); wD(f.w.rain);
|
|
uint64_t sc = f.w.storms.size(); os.write((char*)&sc, 8);
|
|
if (sc) os.write((const char*)f.w.storms.data(), (std::streamsize)(sc * sizeof(WeatherSystem)));
|
|
os.write((char*)&f.w.rng, 4); os.write((char*)&f.w.nextId, 4);
|
|
}
|
|
setStatus(os ? std::string("Saved ") + path : "Save failed");
|
|
}
|
|
|
|
void Viewer::loadGame(const char* path) {
|
|
std::ifstream is(path, std::ios::binary);
|
|
if (!is) { setStatus(std::string("No ") + path); return; }
|
|
char magic[4] = {0}; uint32_t ver = 0; double em = 0; uint8_t st = 0; double dr = 4.0; uint8_t p3 = 0;
|
|
uint8_t lw = 0; double lh = 0.0;
|
|
is.read(magic, 4);
|
|
is.read(reinterpret_cast<char*>(&ver), sizeof ver);
|
|
is.read(reinterpret_cast<char*>(&em), sizeof em);
|
|
is.read(reinterpret_cast<char*>(&st), sizeof st);
|
|
if (ver >= 2) is.read(reinterpret_cast<char*>(&dr), sizeof dr);
|
|
if (ver >= 3) is.read(reinterpret_cast<char*>(&p3), sizeof p3);
|
|
if (ver >= 8) { is.read(reinterpret_cast<char*>(&lw), sizeof lw);
|
|
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
|
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
|
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms
|
|
cfg = planet.cfg; // adopt the loaded config
|
|
elapsedMy = em; settled = (st != 0);
|
|
planet.drifting = settled; // resume drift boosts iff mid-drift
|
|
phase3 = (p3 != 0); phase3Prompt = false;
|
|
phase3PromptAt = phase3 ? elapsedMy : (elapsedMy + planet.cfg.phase3AfterMy);
|
|
driftRate = dr;
|
|
liveWorld = (lw != 0); liveTime = lh; // v8: resume the Live World clock
|
|
settleRun = settleNeed; // keep the settled latch consistent
|
|
dtMy = settled ? planet.cflDtMy() : 0.0;
|
|
driftAccum = 0.0; formAccum = 0.0;
|
|
wxUndo.clear(); followId = 0; // drop stale step-back history / follow target
|
|
if (ver >= 12) { // v12: restore the saved step-back frames (rewind past load)
|
|
auto rD = [&](std::vector<double>& v){ uint64_t m = 0; is.read((char*)&m, 8);
|
|
if (!is || m > 4000000ull) { v.clear(); return; } v.resize((size_t)m);
|
|
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(double))); };
|
|
uint32_t hn = 0; is.read((char*)&hn, 4);
|
|
for (uint32_t k = 0; k < hn && is; ++k) {
|
|
WxFrame f; is.read((char*)&f.t, 8);
|
|
rD(f.w.humidity); rD(f.w.cloud); rD(f.w.rain);
|
|
uint64_t sc = 0; is.read((char*)&sc, 8); if (sc > 1000000ull) sc = 0;
|
|
f.w.storms.resize((size_t)sc);
|
|
if (sc) is.read((char*)f.w.storms.data(), (std::streamsize)(sc * sizeof(WeatherSystem)));
|
|
is.read((char*)&f.w.rng, 4); is.read((char*)&f.w.nextId, 4);
|
|
if (is) wxUndo.push_back(std::move(f));
|
|
}
|
|
}
|
|
paused = true; selectedCell = -1; subgrids.clear();
|
|
buildBorders(planet, borderR, borders, ridgeBorders);
|
|
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
|
buildMap2D(planet, mapRect, map2D);
|
|
refreshView();
|
|
setStatus(std::string("Loaded ") + path);
|
|
}
|
|
|
|
// Advance the simulation this frame: Phase-1 forming (paced ticks toward
|
|
// equilibrium), or Phase-2 drift / Phase-3 drift+hydrology at a finer dt.
|
|
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.empty() || liveTime - wxUndo.back().t >= liveRate - 1e-9))
|
|
wxPushSnapshot(); // throttled history during a continuous run (~1 snapshot/sec)
|
|
liveAdvance(dtH, dtH);
|
|
return;
|
|
}
|
|
if (!paused && !settled) {
|
|
// --- Phase 1: forming, paced ticks toward equilibrium -------------
|
|
formAccum += GetFrameTime() * formRate;
|
|
int budget = 0;
|
|
while (formAccum >= 1.0 && budget < 2000) {
|
|
stepOnce(); formAccum -= 1.0; ++budget;
|
|
if (settled) break;
|
|
}
|
|
if (settled) { dtMy = planet.cflDtMy(); planet.drifting = true; } // entering Phase 2
|
|
refreshView(); // live update so you watch the terrain rise
|
|
} else if (!paused && settled) {
|
|
// --- Phase 2 drift (and Phase 3 = drift + hydrology at a finer dt) --
|
|
// Drift never stops; Phase 3 just uses a smaller timestep so each step
|
|
// advances fewer My (more steps before plates visibly move) while
|
|
// rivers/lakes/fluvial erosion resolve.
|
|
double dt = planet.cflDtMy() * (phase3 ? planet.cfg.phase3DtScale : 1.0);
|
|
dtMy = dt;
|
|
driftAccum += driftRate * GetFrameTime(); // accumulate across frames
|
|
const int guardMax = phase3 ? 60 : 500; // Phase-3 ticks are heavier
|
|
int guard = 0; bool advanced = false;
|
|
while (driftAccum >= dt && guard < guardMax) {
|
|
planet.advect(dt); planet.step(); planet.erode(dt);
|
|
if (phase3) planet.hydrology(dt);
|
|
elapsedMy += dt; driftAccum -= dt; ++guard; advanced = true;
|
|
// Timed Phase-3 invitation: pause + prompt once we cross the mark.
|
|
if (!phase3 && elapsedMy >= phase3PromptAt) { phase3Prompt = true; paused = true; break; }
|
|
}
|
|
if (driftAccum > 2.0 * dt) driftAccum = 2.0 * dt; // drop backlog (don't runaway)
|
|
if (advanced) refreshView(); // live: watch the world evolve
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// Push the current (pre-advance) weather state onto the bounded step-back ring.
|
|
void Viewer::wxPushSnapshot() {
|
|
if ((int)wxUndo.size() >= wxUndoMax) wxUndo.erase(wxUndo.begin());
|
|
wxUndo.push_back(WxFrame{ liveTime, planet.captureWeather() });
|
|
}
|
|
|
|
// Step the live clock forward one rate-unit. Auto-pauses (like a video frame-step); always records
|
|
// the pre-step snapshot first (rate-independent) so the backward step restores weather + storms.
|
|
void Viewer::liveStepForward() {
|
|
paused = true;
|
|
wxPushSnapshot();
|
|
liveAdvance(liveRate, liveRate);
|
|
}
|
|
|
|
// Step everything back: restore the newest snapshot at or before the current time (clock +
|
|
// weather + storms) -- so storms reverse whether they were born while stepping or during a run. If
|
|
// the history is exhausted, fall back to rewinding the deterministic sky only.
|
|
void Viewer::liveStepBack() {
|
|
paused = true;
|
|
while (!wxUndo.empty() && wxUndo.back().t > liveTime + 1e-6) wxUndo.pop_back(); // drop only true future frames
|
|
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)
|
|
setStatus("Step back");
|
|
} else {
|
|
liveAdvance(-liveRate, 0.0); // no recorded past (e.g. right after a load): sky rewinds, weather holds
|
|
setStatus("Step back (sky only - no earlier weather; play/step forward first)");
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
stepSim();
|
|
// A regenerate this frame may have shrunk the planet; keep indices valid.
|
|
if (hovered >= (int)planet.cells.size()) hovered = -1;
|
|
renderFrame();
|
|
}
|
|
UnloadRenderTexture(rt3d);
|
|
CloseWindow();
|
|
}
|