planetsim/src/render/Viewer.hpp
Jonas Reith ab2c4024f8 Civ growth rebalance: urban crowding plateau, plagues, good-year cap
Cities used to climb a hugely stacked carrying capacity (K up to 20-40M
for the best trade hubs -- civMaxPopulation x habitability x siteQuality
x conditions x trade) at ~2%/yr for millennia: bounded in principle,
endless in practice. Three fixes, all population-only + derived vectors
(pure hashes, no RNG, no save-version bump, step-back exact):

- Urban crowding: mortality rises with the square of city size
  (civCrowdingLoss x (P/civMetropolisPop)^2 per year), so the best hubs
  PLATEAU at a historical metropolis scale (~1-1.5M) instead of chasing
  K; negligible below ~50k, not a clamp.
- Good-year cap (civCondBoomCap): a lucky harvest no longer inflates the
  K target by 70% (the logistic chased booms at full rate while famine
  corrected busts slowly -- an upward ratchet); droughts stay uncapped.
- Plagues (civPlague*): rare deterministic epidemics (1-3-year waves,
  20-40% deaths at full exposure) strike cities (exposure 0 below ~30k),
  harder when trade-connected -- contagion travels the routes, the
  historical check on big hubs. Derived sCivPlague + cell-info PLAGUE
  line + "Plague ravages X" / "Plague shrinks X" kind-3 events.

Retuned: civMaxPopulation 2e6 -> 1e6 (it is a capacity SCALE, not a
cap -- comment fixed), civEmpirePop 5e6 -> 2.5e6 for the new sizes.

test_civ gains a plateau/plague section: with the new model the largest
city settles ~1.1M vs 3.7M-and-climbing without it; villages never
plague; waves are deterministic, twin-identical and rewind exactly.
All 16 suites pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:52:27 +02:00

207 lines
11 KiB
C++

#pragma once
#include "raylib.h"
#include "Planet.hpp"
#include "Colors.hpp" // ColorMode
#include "Overlays.hpp" // PlateLabel
#include "Map2D.hpp" // Map2D
#include <vector>
#include <string>
#include <memory>
// The interactive viewer: owns all window/sim/view state and runs the frame
// loop. The old free-standing main() lived as one giant function with capturing
// lambdas; those lambdas are now methods and their captured locals are members,
// so the body splits cleanly across Viewer.cpp (setup + sim orchestration),
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
struct Viewer {
// ---- Files / save format ------------------------------------------------
static constexpr uint32_t SAVE_VERSION = 23; // v23: civ cultural evolution; v22: civ diplomacy; v21: civ conflict/wars; v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
static constexpr int EVENT_LOG_MAX = 200;
const char* CONFIG_PATH = "planet.cfg";
const char* SAVE_PATH = "planet.save";
std::string configPath = "planet.cfg"; // initial config (--config overrides)
// ---- Window / layout (set in init) --------------------------------------
int screenW = 1920, screenH = 1080;
int leftW = 0, rightX = 0, rightW = 0, rightH = 0;
int view3DW = 0, view3DH = 0;
RenderTexture2D rt3d{};
Rectangle mapRect{}, hoverRect{}, panelRect{}, gridRect{};
Rectangle liveInfoRect{}; // free space right of the (left-aligned) 2D map: moon/tide phase
Rectangle pauseBtn{}, p3ContinueBtn{}, p3StartBtn{};
float pbCx = 0.0f, pbCy = 0.0f;
const float visBase = 2.0f;
const float elevExagg = 0.00000004f;
const float borderR = visBase + 0.004f;
const float driftR = visBase + 0.006f;
const float riverR = visBase + 0.005f;
const float gratR = visBase + 0.003f;
const int subRes = 16;
// ---- Sim / world --------------------------------------------------------
Planet planet;
PlanetConfig cfg;
Camera3D cam{};
float camYaw = 0.4f, camPitch = 0.3f, camDist = 6.0f;
ColorMode mode = ColorMode::Elevation;
std::vector<Color> vcolors;
std::vector<Vector3> borders, ridgeBorders; bool showBorders = true;
std::vector<Vector3> driftArrows; bool showDrift = true;
std::vector<PlateLabel> plateLabels;
std::vector<Vector3> rivers, bigRivers; bool showRivers = true;
std::vector<std::vector<Vector2>> graticule; bool showGrat = false;
Map2D map2D;
// Phase-1 forming model.
bool paused = false, settled = false;
long long stepCount = 0;
double maxChange = 0.0;
int settleRun = 0;
const double settleThresh = 2.0; // m/tick at or below which it's "settled"
const int settleNeed = 3; // consecutive settled ticks before pausing
const double formRate = 55.0; // forming ticks per second (watchable)
double formAccum = 0.0;
double minE = 0.0, maxE = 0.0;
// Phase 2 drift.
double elapsedMy = 0.0, dtMy = 0.0, driftRate = 4.0, driftAccum = 0.0;
// Phase 3 hydrology.
bool phase3 = false, phase3Prompt = false;
double phase3PromptAt = 0.0;
// Live World: a slow real-time clock (hours -> weeks/months) over the finished planet.
// Geological drift freezes while it runs; a moving day/night terminator, a live seasonal
// temperature cycle and a moving snow line animate. liveRate is sim hours per real second.
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 -> ~20 years)
// Top clock speed: ~20 sim-years per real second (a century in ~5 s). Kept a code constant like the
// old 720 (1 month/s); the [ / ] ramp and the load-resume clamp both clamp to this.
double liveRateMax() const { return 20.0 * planet.cfg.dayLengthHours * planet.cfg.yearLengthDays; }
// 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<WxFrame> wxUndo;
static constexpr int wxUndoMax = 180;
std::vector<float> illum; // per-cell day/night brightness (1 = day, floor = night)
std::vector<Color> 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)
std::vector<Vector3> moonDirs; // model-space sub-lunar directions (one per moon, for render)
std::vector<Vector3> moonNormals;// model-space orbit-plane normals (one per moon, for the ring)
std::vector<Vector3> coast; // coastline segments (land/ocean boundary, rebuilt with terrain)
std::vector<int> coastOcean; // ocean cell per coast segment (to sample tide)
std::vector<Color> coastCols; // per-segment tide colour (filled each frame when showTides)
bool showTides = false; // colour the coastline by the live tide level (key T)
std::vector<Vector3> currentSegs; std::vector<Color> currentCols; // ocean-current arrows
bool showCurrents = false; // ocean current arrows, warm/cold (key O)
bool showClouds = true; // Live World cloud/rain cover overlay (key K)
bool showVolcanoes = true; // Live World volcano markers (cones + eruption glow, key V)
bool showNames = false; // geographic place-name labels (the atlas, key M)
bool showSettlements = true; // civilization settlement markers (key U seeds + toggles)
std::vector<int> atlasRowCells; // cell to focus per visible Atlas/Eco/Civ/Realms-tab row (parallel to the list)
std::vector<Vector3> nationBorders; // political border segments (rebuilt on year tick / placement / load)
bool showNationBorders = false; // draw nation/realm borders (on with the Territory view)
std::vector<Vector3> cultureBorders; // cultural-region border segments (civ Step 4, rebuilt with territory)
bool showCultureBorders = false; // draw cultural-region borders (on with the Culture view)
std::vector<Vector3> warFrontier; // red frontier segments between realms currently at war (civ Step 5)
std::vector<Vector3> allyLinks, rivalLinks; // civ Step 6: capital-to-capital arcs (allies green, rivals red)
std::vector<Vector3> tradeSea, tradeLand; // civ Step 7: trade routes (sea cyan / river+land amber)
bool showTradeRoutes = false; // draw trade routes (on with the Wealth view)
long lastTerritoryYear = -1; // sim year territory was last recomputed (recompute when it ticks)
// World event journal: currently Live World events, shaped to be reused by later phases.
struct WorldEvent {
uint32_t id = 0;
uint8_t kind = 0; // 1 weather, 2 volcano, later phases can append new kinds
uint8_t severity = 0; // 0 info, 1 notable, 2 severe
double timeHours = 0.0;
int cell = -1;
uint32_t sourceId = 0;
std::string title, detail;
};
std::vector<WorldEvent> events;
uint32_t nextEventId = 1;
int liveInfoTab = 0; // 0 Sky, 1 Tides, 2 Weather, 3 Events, 4 Atlas, 5 Eco
std::vector<Rectangle> liveInfoTabRects;
std::vector<Rectangle> eventRowRects;
std::vector<int> eventRowIndices; // indices into events for visible event rows
// Selection + subgrid (phase 4/5 preview).
int selectedCell = -1;
double selectedThresh = 0.06;
std::vector<std::shared_ptr<SubGrid>> subgrids;
// Transient on-screen status line.
std::string statusMsg; double statusUntil = 0.0;
// 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{};
bool onPause = false;
int hovered = -1;
bool hasHoverSub = false;
SubCell hoverSub;
int hoveredSubIdx = -1;
// ---- Lifecycle ----------------------------------------------------------
bool init(int argc, char** argv); // window, layout, config, first world
void run(); // the frame loop (until window closes)
// ---- Sim orchestration (Viewer.cpp) -------------------------------------
void rebuildSub();
void selectCell(int idx);
void recolor();
void refreshView();
void rebuildTerritory(); // recompute nations/territory + nation-border segments
void rebuildLiveOverlay(); // Live World: fill illum + shadedColors from sim fields
// Colors the 3D globe + 2D map actually draw: the live overlay when in Live World, else the
// plain per-cell colours.
const std::vector<Color>& displayColors() const { return liveWorld ? shadedColors : vcolors; }
void regenWorld(); // after generate(): geometry changed
void regen(); // generate(cfg) + regenWorld()
void stepOnce(); // one tick + settle bookkeeping
void pauseAction();
void setStatus(const std::string& m);
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
void liveStepForward(); // step the clock forward one rate-unit (snapshots for undo)
void liveStepBack(); // step everything back one frame (restores weather/storms)
void wxPushSnapshot(); // push the current weather state onto the step-back ring
Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect)
void appendEvent(uint8_t kind, uint8_t severity, double timeHours, int cell, uint32_t sourceId,
const std::string& title, const std::string& detail);
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
const std::vector<Volcano>& beforeVolcanoes,
const std::vector<Settlement>& beforeSettlements,
const std::vector<double>& beforePlague);
void detectNationEvents(const std::vector<Nation>& beforeNations);
void focusCell(int idx, const std::string& status = "");
// ---- Input (ViewerInput.cpp) --------------------------------------------
void handleInput();
// ---- Render (ViewerRender.cpp) ------------------------------------------
void renderFrame();
void renderGlobe3D();
void renderMap2D();
void renderLiveInfo(); // Live World: moon phases + (coastal) tidal phase, beside the 2D map
void renderPanels();
void renderHUD();
void renderPrompt();
};