#pragma once #include "Vec3.hpp" #include "IcoSphere.hpp" #include "PlanetTypes.hpp" // Cell, Plate, SubGrid/SubCell, PlanetConfig #include "PlanetBiota.hpp" // BiotaKind, Organism, CellBiota #include "PlanetGeography.hpp" // FeatureKind, GeoFeature #include "PlanetCiv.hpp" // Settlement, SettleTier, CivUpdate #include "PlanetNation.hpp" // Nation, NationTier, GovType #include "PlanetCulture.hpp" // Culture, CultureEthos, Faith #include "PlanetConflict.hpp" // WarEvent, ConflictUpdate (War is in PlanetTypes.hpp) #include "PlanetTrade.hpp" // TradeKind, TradeLink #include "PlanetEcoregions.hpp" // Ecoregion #include #include #include #include #include class Planet { public: PlanetConfig cfg; std::vector cells; std::vector plates; std::vector moons; // Live World: 1-3 natural satellites (generated + saved) std::vector volcanoes; // Live World: stateful lifecycle volcanoes (saved v15) std::vector geoFeatures; // named geographic features / the atlas (saved v17+) std::vector ecoRegions; // named ecological provinces (saved v19+) std::vector settlements; // civilization: settlements placed once, grow/decline (saved v20+) std::vector nations; // realms grouped from settlements (derived each computeTerritory, not saved) std::vector cultures; // peoples/cultures, one per inhabited continent (derived, not saved) std::vector wars; // civ Step 5: active wars between realms (stateful, saved v21) std::vector diplomacy; // civ Step 6: standing realm relations (stateful, saved v22) // Phase flag: false during Phase-1 forming (modest, original tectonics that // settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts // (collision/arc uplift + isostatic persistence) so Phase 1 stays unchanged // and tall persistent mountains only grow during drift (where erode() limits // them). Not serialized -- the orchestrator sets it from the phase. bool drifting = false; void generate(const PlanetConfig& c); double step(); // one tectonic tick; returns max |elevation change| (m) this tick // Phase 2: stable timestep (My) from the fastest plate (CFL ~ half a cell). double cflDtMy() const; // Phase 2: advect plate membership + carried crust (plateId, elevation, // oceanic, geoAge) over the fixed grid by dt My. Opening gaps become new // young oceanic crust (spreading); overlaps subduct. void advect(double dtMy); // Phase 2: erode the elevation field by dt My (highs wear down, sediment // deposits downhill in basins / below sea level) and, every seaLevelEvery // calls, nudge seaLevel toward landFractionTarget. Crust type is untouched. void erode(double dtMy); // Phase 3: one hydrology tick over the fixed grid -- recompute the drainage // network (depression-fill -> lakes, steepest-descent routing -> rivers, // flow accumulation -> discharge) and apply mass-conserving fluvial erosion // (stream-power incision + downstream sediment transport/deposition) to the // elevation field by dt My. computeHydrology() does the routing only (no // erosion) so the viewer can show rivers/lakes when paused / after load. void hydrology(double dtMy); void computeHydrology(); // Phase 3 (climate): compute per-cell temperature + precipitation fields from // elevation, latitude and prevailing-wind orographic moisture transport (windward // rain, leeward rain shadow, dry continental interiors). Derived (not saved); // call before classifyBiomes(), which consumes these fields. void computeClimate(); const std::vector& temperature() const { return sTemp; } // deg C, annual mean const std::vector& precipitation() const { return sPrecip; } // relative units const std::vector& moisture() const { return sMoist; } // 0..1 (median land -> 0.5) const std::vector& summerTemp() const { return sTempSummer; } // deg C, warmest month const std::vector& winterTemp() const { return sTempWinter; } // deg C, coldest month // Live World stage (PlanetLive.cpp): the slow real-time clock's derived fields. // computeInsolation() = instantaneous solar incidence cos(sun angle), 0..1 -- the // physical foundation for live weather + the day/night terminator. computeLiveSeason() // = the live temperature cycling between winterTemp/summerTemp over the year (drives the // live temperature view + the moving snow line). Both derived/not-saved; call after // computeClimate(). dayOfYear01/timeOfDay01 are fractions in [0,1). void computeInsolation(double dayOfYear01, double timeOfDay01); void computeLiveSeason(double dayOfYear01); const std::vector& insolation() const { return sInsolation; } // 0..1 cos incidence const std::vector& liveTemp() const { return sLiveTemp; } // deg C, current season // Live World sky + oceans (PlanetOcean.cpp). Sub-solar / sub-lunar directions in model // space at a clock fraction (drive insolation, tides and the 3D sun/moons -- one source of // truth). generateMoons() seeds 1-3 moons from a separate RNG (tectonic determinism intact). Vec3 sunDirection(double dayOfYear01, double timeOfDay01) const; Vec3 moonDirection(int moonIdx, double timeOfDay01, double timeDays) const; Vec3 moonOrbitNormal(int moonIdx, double timeOfDay01) const; // orbit-plane normal (for the ring) const std::vector& getMoons() const { return moons; } void generateMoons(); // Tides: equilibrium tidal height (m) per cell from the moons + sun at the given clock. // Derived/not saved; recomputed each frame like insolation. void computeTides(double dayOfYear01, double timeOfDay01, double timeDays); const std::vector& tide() const { return sTide; } // Ocean surface currents (PlanetOcean.cpp): a per-ocean-cell tangent velocity from wind // stress + Coriolis deflection + coast-following (gyres). Derived/not saved; needs sWind // (computeClimate() computes it, then calls this and feeds warm/cold currents back into // sTemp -- see climateCurrentFactor). Zero on land cells. void computeOceanCurrents(); const std::vector& current() const { return sCurrent; } // Weather (Live World): dynamic per-cell humidity / cloud cover / rain advanced on the live // clock. initWeather() spins the fields up from the climatology; stepWeather(dtHours) runs // one cycle (evaporate over warm seas -> advect along the wind -> condense into cloud, with // orographic lift -> rain out -> dissipate). Reads sInsolation/sTemp/sWind/sUpwind/sMoist // (computeClimate + computeInsolation set those). Saved (v10). void initWeather(); void stepWeather(double dtHours); const std::vector& humidity() const { return sHumidity; } const std::vector& cloud() const { return sCloud; } const std::vector& rain() const { return sRain; } const std::vector& storms() const { return sStorms; } // Snapshot / restore the full weather state (humidity/cloud/rain/storms/RNG) for the viewer's // step-back undo history -- weather is an integrated path, so backward stepping restores a frame. WeatherSnapshot captureWeather() const; void restoreWeather(const WeatherSnapshot& s); // Volcanoes (Live World, PlanetVolcano.cpp). placeVolcanoes() runs once on entering Live World: // it seeds a set by tectonic context (high on young ridges, medium on borders, low elsewhere) // from a separate RNG (tectonic determinism intact), capturing each vent's baseElev and initial // built height. stepVolcanoes() integrates growth/dormancy/explosions forward, reasserts vent // elevations, breaches submarine vents into islands, and injects ash cloud + local cooling. // Saved (v15); step-back restores them via WeatherSnapshot. void placeVolcanoes(double liveTime = 0.0); VolcanoUpdate stepVolcanoes(double dtHours); double volcanoBuilt(const Volcano& v) const; // m built above baseElev double volcanoErupting(const Volcano& v) const; // 0..1 current visual eruption intensity // Phase 3 (biomes): classify every cell into a Biome from elevation + the climate // fields (temperature + normalized precipitation). Derived + written back into // cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves. void classifyBiomes(); // Biota stage (flora/fauna/funga). computeBiotaDensity() builds the derived // per-cell density scalars (0..1) each tick (like climate; not saved); call it // after classifyBiomes(). generateBiota() does the on-demand slot/point fill of // the discrete population into sBiota (saved) -- NOT called per tick. See // PlanetBiota.cpp + PlanetFloraGen/FaunaGen/FungiGen.cpp. void computeBiotaDensity(); void generateBiota(); bool biotaPopulated() const; const std::vector& floraDensity() const { return sFloraDensity; } const std::vector& faunaDensity() const { return sFaunaDensity; } const std::vector& fungaDensity() const { return sFungaDensity; } const std::vector& biota() const { return sBiota; } // Derived hydrology fields (recomputed each route; not saved). Empty until // the first computeHydrology()/hydrology() call. const std::vector& lakeDepth() const { return sLakeDepth; } const std::vector& discharge() const { return sDischarge; } const std::vector& flowTo() const { return sFlowTo; } // Geography stage (the atlas, PlanetGeography.cpp). generateGeography() extracts named // geographic features from the frozen terrain by connectivity (continents/islands, oceans/seas, // lakes, mountain ranges/peaks, rivers) and names them with a separate RNG (tectonic determinism // intact). Saved (v17; active reshuffle salt saved v18). The per-cell index arrays give O(1) // "which features is this cell in". void generateGeography(uint32_t nameSalt = 0); // Rebuild the atlas against the current terrain and use a new naming salt, for manually // rechecking/relabeling geography after moving between world-building phases. void reshuffleGeography(); // Name a cell that has just become land (e.g. a volcanic island breaching the sea): join an // adjacent existing landmass, else create + name a new Island. Returns the land feature's name // ("" if the atlas isn't built). Appended to geoFeatures (saved). Called by the viewer on a // volcanic island-formation event so the new island enters the atlas. std::string nameNewLand(int cell); bool geographyBuilt() const { return !geoFeatures.empty(); } const std::vector& geography() const { return geoFeatures; } const std::vector& cellLand() const { return sCellLand; } // continent/island feature index (-1) const std::vector& cellWater() const { return sCellWater; } // ocean/sea/lake feature index (-1) const std::vector& cellRange() const { return sCellRange; } // mountain-range feature index (-1) const std::vector& cellRiver() const { return sCellRiver; } // river feature index (-1) // Ecoregion atlas (PlanetEcoregions.cpp): connected ecological provinces built from current // biome, land/water context, productivity and dominant broad biota. Saved (v19). void generateEcoregions(); bool ecoregionsBuilt() const { return !ecoRegions.empty(); } const std::vector& ecoregions() const { return ecoRegions; } const std::vector& cellEcoregion() const { return sCellEcoregion; } // ecoregion index (-1) // Civilization Step 2 (PlanetCiv.cpp). computeHabitability() builds the derived per-cell // habitability/food score (0..1; like climate, not saved). placeSettlements() seeds the fixed // settlement set once on the best-spaced fertile cells (separate RNG; tectonic determinism intact; // auto-builds geography/ecoregions for naming + productivity). stepCivilization() advances each // settlement's population on the live clock toward a food-driven carrying capacity (grows / shrinks // / is abandoned). Saved (v20); step-back restores populations via WeatherSnapshot. void computeHabitability(); void placeSettlements(); CivUpdate stepCivilization(double dtHours, double liveTime); // Colonization (once per sim year): a kingdom+ realm may found a new settlement on good unclaimed land // (or an island, from a coastal member) bound to it by allegiance. Appends to `settlements`; returns // the founding events. Deterministic (pure hashes) so step-back replays it. std::vector stepColonization(long year); bool settlementsPlaced() const { return !settlements.empty(); } const std::vector& cellSettlement() const { return sCellSettlement; } // settlement index per cell (-1) const std::vector& habitability() const { return sHabitability; } // 0..1 per cell (derived) // Territory & nations (PlanetNation.cpp). computeTerritory() groups settlements into realms // (capital + vassal towns) and claims cells within each settlement's size-scaled influence range // (wilderness frontiers between realms). Purely derived from the settlement set, so it is recomputed // (on placement / load / each sim year), not saved -- step-back replays it as populations restore. void computeTerritory(); bool nationsBuilt() const { return !nations.empty(); } const std::vector& nationList() const { return nations; } const std::vector& cellNation() const { return sCellNation; } // nation index per cell (-1 = wilderness/sea) const std::vector& settleNation() const { return sSettleNation; } // nation index per settlement (-1 = dead) // Cultures, beliefs & governments (PlanetCulture.cpp). computeCultures() is the DERIVED refresh: // it seeds the cultures once (one per inhabited continent) when none exist, then only recomputes // tallies, governments (folded into nation.name) and the per-cell culture view. Runs AFTER // computeTerritory() (reads nations/sCellNation/sCellSettleOwner). Culture identities + the // per-settlement culture are STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is // the once-per-sim-year mutation pass (border conversion / assimilation / schism, pure hashes -- // no RNG touched, so step-back replays it exactly). void computeCultures(); std::vector stepCulture(long year); bool culturesBuilt() const { return !cultures.empty(); } const std::vector& cultureList() const { return cultures; } const std::vector& cellCulture() const { return sCellCulture; } // culture index per cell (-1 = none) const std::vector& settleCulture() const { return sSettleCulture; } // culture index per settlement (-1 = none) // Conflict & war (PlanetConflict.cpp; civ Step 5). stepConflict() runs once per sim year: adjacent // realms grow hostile, declare wars, fight (casualties in frontier cities), conquer (a city's // allegiance flips to the victor) or sack (raze) cities, and conquered provinces revolt. Stateful + // path-dependent -> saved (v21) + snapshotted. computeTerritory() reads sSettleAllegiance so borders // move as cities change hands. Uses a separate war RNG (tectonic determinism intact). ConflictUpdate stepConflict(long year); const std::vector& warList() const { return wars; } const std::vector& settleAllegiance() const { return sSettleAllegiance; } // overlord capital settlement index (-1 = free) bool realmsAtWar(int nationA, int nationB) const; // are these two nation indices in an active war? // Diplomacy (civ Step 6). Relations are updated inside stepConflict(); these query them by nation index. const std::vector& diploList() const { return diplomacy; } DiploKind diploBetween(int nationA, int nationB) const; // relation kind between two nation indices bool realmsAllied(int nationA, int nationB) const; // are these two realms allied? // Trade & economy (PlanetTrade.cpp; civ Step 7). computeTrade() links nearby settlements (sea/river/ // overland), accrues prosperity at hubs (which feeds population growth) and fills a per-cell wealth // field. A pure derived function of settlements + geography + wars/diplomacy -- recomputed, not saved. void computeTrade(); bool tradeBuilt() const { return !sProsperity.empty(); } const std::vector& prosperity() const { return sProsperity; } // per settlement (0..~) const std::vector& tradeLinks() const { return sTradeLinks; } const std::vector& cellWealth() const { return sCellWealth; } // per cell (heat map) // Per-settlement live conditions (derived each stepCivilization; not saved). condition = the combined // environmental multiplier on carrying capacity (1 = normal, <1 = hardship, >1 = boom); drought = // current drought severity 0..1. Parallel to `settlements`. Used by the viewer for tint + events. const std::vector& settlementCondition() const { return sCivCond; } const std::vector& settlementDrought() const { return sCivDrought; } // Build a fine-resolution subgrid patch for one macro cell (phase 4/5 hook). std::shared_ptr makeSubGrid(int cellIndex, int res) const; // Save/load the full simulation state (binary). readState rebuilds geometry // from the saved cfg.subdivisions, so only dynamic per-cell fields are stored. // Reloading resumes the simulation exactly (deterministic continuation). void writeState(std::ostream& os) const; // hasBiome: whether the stream carries the per-cell biome byte (save v4+). For // older saves (v3) pass false -- biomes are reclassified after the cells load. // hasBiota: whether the stream carries the biota population block (save v7+). // hasMoons: whether the stream carries the moons block (save v9+); older saves // synthesize moons from the seed instead. hasWeather: the weather block (save v10+); // older saves leave weather to spin up on entering Live World. // hasVolcanoes: whether the stream carries a volcano block (save v14+). hasStatefulVolcanoes // means v15+ lifecycle volcanoes; v14's old pure-function block is consumed and discarded. // hasGeography: whether the stream carries the geography/atlas block (save v17+); hasGeoSalt: // whether the stream carries the active reshuffle salt (save v18+); hasEcoregions: whether the // stream carries the ecoregion atlas (save v19+). Older saves regenerate on demand. // hasSettlements: whether the stream carries the civilization settlements block (save v20+); older // saves load with none (re-seeded on demand via the civ key). hasCultures: whether the stream // carries the stateful culture block (save v23+); older saves re-seed one culture per continent // on the next computeCultures(). bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true, bool hasMoons = true, bool hasWeather = true, bool hasStorms = true, bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true, bool hasGeoSalt = true, bool hasEcoregions = true, bool hasSettlements = true, bool hasConflict = true, bool hasDiplo = true, bool hasCultures = true); // Helpers for rendering / info. double cellWidthMeters() const; // approx lateral cell spacing double minElevation() const; double maxElevation() const; const std::vector& triIndices() const { return sphere.triIndices; } private: IcoSphere sphere; uint32_t rngState = 1; int targetLand = -1; // land-cell count to conserve during drift uint32_t rnd(); double rndf(); // [0,1) void buildGeometry(); // build icosphere + per-cell unit/neighbors bool readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons, bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes, bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements, bool hasConflict, bool hasDiplo, bool hasCultures); void clearDerivedState(); // clear geometry-dependent scratch/derived fields void assignPlates(); void seedInitialRelief(); Vec3 driftVelocity(int plateId, const Vec3& pos) const; double oceanicBase(double age) const; // age-dependent seafloor depth // Phase-2 plate-dynamics helpers (see PlanetConfig above). void setPlateSpeed(Plate& p, double cmYr); // cm/yr -> driftSpeed + angSpeed void randomizePlateDrift(Plate& p); // random axis + random speed int acquirePlate(); // reuse a dead slot or append one void splitPlate(int pid); // fission: cut a plate roughly in two void maybeSplitPlates(const std::vector& cnt); void kickStalemates(const std::vector& cnt); void deleteEnclosedPlates(); // absorb plates ringed by one other void fuseMiniPlates(); // cluster of mini plates steals + merges void coalesceBabyPlates(); // merge connected baby cells into one id void promoteBabyPlates(const std::vector& cnt); void adjustSeaLevel(); // nudge seaLevel toward land target // Phase-3 hydrology helpers (see hydrology()). void routeFlow(); // depression-fill -> lakes, flow, discharge // Biota helpers (PlanetFloraGen/FaunaGen/FungiGen.cpp). compute*Density write the // derived scalars; fill* draw the per-cell population (nbr = already-filled, // same-biome neighbours, for regional consistency). void computeFloraDensity(); void computeFaunaDensity(); void computeFungaDensity(); double neighbourhoodPrey(int i) const; // mean fauna density over i + neighbours std::vector fillFlora(int i, const std::vector& nbr, uint32_t& rng); std::vector fillFauna(int i, const std::vector& nbr, uint32_t& rng); std::vector fillFunga(int i, const std::vector& nbr, uint32_t& rng); int driftIter = 0; // counts advect() calls (gates periodic checks) int erodeIter = 0; // counts erode() calls (gates sea-level control) std::vector sPrevCount; // per-plate cell count at the previous check std::vector sStaleStreak; // consecutive stuck windows per plate std::vector sFreePlateIds; // dead plate slots free for reuse // Reusable scratch buffers for step()/erode() so they allocate nothing per tick. std::vector sStress, sBelt, sBeltNext, sDelta, sSmoothed, sOldElev, sErode; std::vector sSub, sOver, sColl; // Phase-3 hydrology scratch (derived from elevation each routeFlow(); not saved). std::vector sFill, sLakeDepth, sDischarge; std::vector sFlowTo, sHydroOrder; // Phase-3 climate scratch (derived each computeClimate(); not saved). sMoist is the // 0..1-normalized precipitation the biome classifier reads. sTempSummer/sTempWinter are // the obliquity-driven seasonal extremes around the annual mean sTemp (see Seasons). std::vector sTemp, sPrecip, sMoist, sTempSummer, sTempWinter; std::vector sWind; std::vector sUpwind; // Live World scratch (derived each frame in Live World; not saved). sInsolation is the // instantaneous solar incidence; sLiveTemp is the temperature for the current day-of-year; // sTide is the equilibrium tidal height (m) from the moons + sun. std::vector sInsolation, sLiveTemp, sTide; std::vector sCurrent; // ocean surface current velocity (tangent; zero on land) // Weather (Live World; saved v10). sHasWeather latches once spun up/loaded. std::vector sHumidity, sCloud, sRain; bool sHasWeather = false; // Moving weather systems (saved with weather state). Separate RNG keeps tectonic // determinism intact (seeded from cfg.seed in initWeather). std::vector sStorms; uint32_t sWeatherRng = 1; uint32_t sStormNextId = 1; // monotonic id for follow-cam tracking // Volcanoes (Live World; saved v15). Separate RNG (seeded from cfg.seed in placeVolcanoes) // keeps tectonic determinism intact while lifecycle rolls happen during Live World. uint32_t sVolRng = 1; // Geography (the atlas; saved v17; active reshuffle salt saved v18). Per-cell feature index // arrays (-1 = none) + a separate RNG/salt so naming never perturbs the tectonic stream. std::vector sCellLand, sCellWater, sCellRange, sCellRiver; uint32_t sGeoRng = 1; uint32_t sGeoSalt = 0; // Ecoregions (saved v19). Per-cell ecoregion index (-1 = none) + separate RNG salt. std::vector sCellEcoregion; uint32_t sEcoRng = 1; // Civilization (saved v20). Per-cell settlement index (-1 = none; rebuilt on load, not saved), // a derived habitability field, and a separate RNG so placement never perturbs tectonics. std::vector sCellSettlement; std::vector sHabitability; std::vector sCivCond, sCivDrought; // per-settlement live conditions (derived) uint32_t sCivRng = 1; // Territory & nations (derived from settlements; not saved). sCellNation: nation index per cell // (-1 = wilderness/ocean); sSettleNation: nation index per settlement. std::vector sCellNation, sSettleNation; // Cultures (civ Step 8: stateful, saved v23). sSettleCulture: culture index per settlement -- // mutable identity state (seeded at the dawn, changed only by stepCulture()/colony founding); // sCellCulture: the derived per-cell view (refreshed by computeCultures(), not saved). // sCultureNextId: stable id counter for appended (schism) cultures. std::vector sCellCulture, sSettleCulture; uint32_t sCultureNextId = 1; void seedCultures(); // one-time seeding (dawn / pre-v23 load), PlanetCulture.cpp // Per-cell owning settlement index (filled by computeTerritory alongside sCellNation; -1 = none). // Reused by the trade wealth field so the heat map matches territory exactly. std::vector sCellSettleOwner; // Trade & economy (civ Step 7; derived, not saved). Per-settlement prosperity, the trade-link list, // and a per-cell wealth field (owning settlement's prosperity). Recomputed each sim year. std::vector sProsperity, sCellWealth; std::vector sTradeLinks; // Conflict & war (civ Step 5; stateful, saved v21). sSettleAllegiance[s] = the overlord's capital // settlement index if conquered, else -1 (independent). Separate war RNG + a stable war-id counter. std::vector sSettleAllegiance; uint32_t sWarRng = 1, sWarNextId = 1; // Biota: derived density scalars (0..1; recomputed each tick, not saved) and the // on-demand discrete population (saved). sHasBiota latches once generated/loaded. std::vector sFloraDensity, sFaunaDensity, sFungaDensity; std::vector sBiota; bool sHasBiota = false; }; // Human-editable config file (key = value text). All PlanetConfig input // parameters are written/read via one shared field table. Unknown keys ignored. // validateConfig returns an empty string if the config is reasonable. bool loadConfig(const std::string& path, PlanetConfig& cfg); bool saveConfig(const std::string& path, const PlanetConfig& cfg); std::string validateConfig(const PlanetConfig& cfg);