Reported after a real long-running world (elapsedMy ~1340) showed seaLevel drifted to -2900m, with "land" at -2000m next to "ocean" at -4000m. Root cause: adjustSeaLevel()'s geographic land target (elevation above seaLevel, fixed 30%) and advect()'s crust-type land conservation (targetLand/landBand, held near whatever Phase-1 forming settled at for that seed) are two independent notions of "land" that aren't guaranteed to agree -- this seed's continental crust settled at only ~26%. With genuine land structurally short of the target, the controller had no lower bound and kept sinking seaLevel to misclassify progressively older, deeper oceanic crust as land -- and since that crust keeps ageing and deepening even at a fixed seaLevel, it was chasing a moving target with no way to ever settle. Added seaLevelMin/seaLevelMax config fields (default +-3000m) that clamp adjustSeaLevel()'s candidate nudge, so a world that can't reach the target land fraction settles at a plausible offset instead of an unbounded one. New test_sealevel.cpp reproduces the pathology with a continuous elevation distribution (not a synthetic cliff) and confirms the controller still engages but never crosses either bound. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
577 lines
43 KiB
C++
577 lines
43 KiB
C++
#pragma once
|
||
#include "Vec3.hpp"
|
||
#include <vector>
|
||
#include <memory>
|
||
#include <cstdint>
|
||
|
||
// Data structures shared across the Planet engine (raylib-free). The Planet
|
||
// class itself lives in Planet.hpp; these are the per-cell / per-plate / config
|
||
// types it operates on. Geometry never moves -- properties flow over the fixed
|
||
// grid (see CLAUDE.md core principle).
|
||
|
||
// Fine-resolution subgrid (phases 4/5 hook). Generated on demand for one macro
|
||
// cell: a high-res patch of the sphere around that cell, with elevation blended
|
||
// from the cell + its neighbors so it transitions smoothly across boundaries.
|
||
struct SubCell {
|
||
Vec3 unit; // direction on the unit sphere
|
||
double elevation = 0.0; // meters (blended + fine detail noise)
|
||
int nearestMacro = -1;// macro cell (this cell or a neighbor) it lies under
|
||
};
|
||
struct SubGrid {
|
||
int macroCell = -1;
|
||
int res = 0; // grid is res x res, row-major
|
||
std::vector<SubCell> sub;
|
||
};
|
||
|
||
enum class PlateType { Oceanic, Continental };
|
||
|
||
// A moving weather system (Live World): a drifting low-pressure disturbance that travels with the
|
||
// steering wind and stamps clouds & rain onto the weather fields. Geometry is fixed, so this is a
|
||
// world-object agent (a point on the sphere, like a moon), not a cell. The intense tropical ones
|
||
// (strength past weatherHurricaneStrength) are hurricanes/typhoons. Saved with the weather state.
|
||
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)
|
||
double age = 0.0; // hours alive
|
||
double life = 120.0; // total lifetime (hours)
|
||
double spin = 1.0; // cyclonic sense: +1 CCW (N hemisphere) / -1 CW (S)
|
||
bool tropical = false; // warm-core tropical (can become a cyclone) vs extratropical low
|
||
};
|
||
|
||
// Phase-3 (climate & biomes) classification of a cell, derived from elevation,
|
||
// latitude (temperature) and hydrology/coast (moisture). Stored per cell (uint8,
|
||
// serialized) so Phase-4 civilization can read it. Keep Ocean == 0 so a default-
|
||
// constructed cell reads as ocean. Extend by appending new entries (don't reorder
|
||
// -- the numeric value is saved).
|
||
enum class Biome : uint8_t {
|
||
Ocean, Ice, Lake, Beach, Wetland, Grassland, Savanna,
|
||
Desert, Forest, Taiga, Tundra, Hills, Mountains
|
||
};
|
||
|
||
// Edit mode (manual world editing, save v24): a per-cell bitmask of which fields are
|
||
// LOCKED against automatic per-tick recomputation. An edit with a field's bit clear is a
|
||
// one-off "nudge" -- it changes current state but the ongoing simulation keeps evolving it
|
||
// (like a meteor impact); with the bit set, the normal recompute for that field skips this
|
||
// cell entirely until unlocked. Elevation/Plate/Crust/Biome lock the Cell field itself (no
|
||
// extra storage -- see PlanetTectonics/Erosion/Hydrology/Drift/Biomes.cpp); Climate/Biota/
|
||
// Habitability have no persistent backing of their own (fully recomputed from scratch each
|
||
// tick), so locking them also pins the frozen value in a small sparse map (see PlanetEdit.cpp).
|
||
// Population/Allegiance/Culture are keyed by a settlement's home cell and gate its yearly
|
||
// update in PlanetCiv/PlanetConflict/PlanetCulture.cpp.
|
||
enum EditLock : uint16_t {
|
||
LockElevation = 1u << 0,
|
||
LockPlate = 1u << 1,
|
||
LockCrust = 1u << 2, // oceanic + geoAge (crust identity)
|
||
LockBiome = 1u << 3,
|
||
LockClimate = 1u << 4, // temperature + moisture together
|
||
LockBiota = 1u << 5, // flora/fauna/funga density together
|
||
LockHabitability = 1u << 6,
|
||
LockPopulation = 1u << 7, // settlement: population growth/decline frozen
|
||
LockAllegiance = 1u << 8, // settlement: overlord assignment frozen
|
||
LockCulture = 1u << 9, // settlement: culture assignment frozen
|
||
};
|
||
|
||
// A natural satellite (Live World). Geometry is fixed, so the moon is a world object
|
||
// (not a cell): it orbits on the live clock, raises tides, and renders as a small sphere.
|
||
// Generated 1-3 per world from a separate RNG (so it never perturbs tectonic determinism)
|
||
// and saved (v9). orbitRadius is in planet radii (render scale); tideWeight is the tide-
|
||
// raising mass proxy; inclination tilts the orbit plane off the equator.
|
||
struct Moon {
|
||
double orbitRadius = 16.0; // planet radii (visual orbit distance)
|
||
double periodDays = 20.0; // orbital period (planetary days)
|
||
double phase = 0.0; // orbital phase offset (radians)
|
||
double inclination = 0.0; // orbit-plane tilt off the equator (radians)
|
||
double tideWeight = 1.0; // tide-raising strength (Moon mass proxy)
|
||
double dispRadius = 0.10; // display sphere radius (visual size)
|
||
};
|
||
|
||
// A volcano (Live World): a fixed point on the grid (one cell) placed by tectonic context when the
|
||
// world enters Live World -- high probability on young spreading ridges ("new plate"), medium on
|
||
// plate borders, low elsewhere (hotspots). It is a small stateful lifecycle agent: it grows, can
|
||
// go dormant, explodes after dormancy, then regrows weaker. Saved (v15).
|
||
struct Volcano {
|
||
uint32_t id = 0; // stable id (markers / cell-info)
|
||
int cell = -1; // the grid cell it sits on (fixed geometry)
|
||
uint8_t kind = 2; // 0 = ridge (new plate), 1 = plate border, 2 = hotspot/interior
|
||
uint8_t submarine = 0; // 1 if its baseElev is below sea level (can build an island)
|
||
uint8_t phase = 0; // 0 = growing, 1 = dormant
|
||
double activity = 0.5; // 0..1 eruption vigour (drives cadence + build rate)
|
||
double baseElev = 0.0; // m: cell elevation captured at placement (build adds on top)
|
||
double built = 0.0; // m: current height built above baseElev
|
||
double timer = 0.0; // h: dormancy countdown
|
||
double ashTimer = 0.0; // h: sustained ash emission after an explosion
|
||
double ashCarry = 0.0; // fractional ash-puff accumulator
|
||
};
|
||
|
||
// Result of one stepVolcanoes() call, telling the viewer how much of the view to rebuild:
|
||
// `recolor` if any vent's cell elevation changed (cone/island grew/shrank), `breach` if a
|
||
// submarine vent crossed sea level (a new island/sunk island -> needs biome reclassification).
|
||
struct VolcanoUpdate { bool recolor = false; bool breach = false; };
|
||
|
||
// Civilization Step 5: an active war between two realms (identified by their capital SETTLEMENT INDEX,
|
||
// a stable id across the yearly territory recompute). Stateful & saved (v21) + snapshotted for step-back.
|
||
struct War {
|
||
uint32_t id = 0;
|
||
int attacker = -1; // capital settlement index of the aggressor realm
|
||
int defender = -1; // capital settlement index of the defending realm
|
||
long startYear = 0;
|
||
double warscore = 0.0; // + favours the attacker, - the defender (accumulates from battles)
|
||
int battles = 0; // number of resolved battle-years
|
||
};
|
||
|
||
// Civilization Step 6: diplomacy. A standing relationship between two realms (by capital SETTLEMENT
|
||
// INDEX, a<b -- the same stable id wars use). attitude drifts from culture/faith affinity, proximity and
|
||
// the scars of past wars; it crystallises into an alliance / non-aggression pact / rivalry. Stateful,
|
||
// saved (v22) + snapshotted.
|
||
enum class DiploKind : uint8_t { Neutral, NonAggression, Alliance, Rival };
|
||
|
||
struct DiploTie {
|
||
int a = -1; // capital settlement index of one realm (a < b)
|
||
int b = -1; // capital settlement index of the other realm
|
||
double attitude = 0.0; // -1 hostile .. +1 friendly
|
||
long truceUntil = -1; // no war between them until this sim year (-1 = none)
|
||
DiploKind kind = DiploKind::Neutral;
|
||
};
|
||
|
||
// A full snapshot of the (integrated, non-analytic) weather state, for the viewer's step-back
|
||
// undo history -- weather can't be reversed in closed form, so we restore a saved frame instead.
|
||
struct WeatherSnapshot {
|
||
std::vector<double> humidity, cloud, rain;
|
||
std::vector<WeatherSystem> storms;
|
||
std::vector<Volcano> volcanoes;
|
||
uint32_t rng = 0, nextId = 0;
|
||
uint32_t volRng = 0;
|
||
// Civilization: settlement populations (the only mutable per-step civ state, since the set is
|
||
// fixed after placement). Restored on a step back so towns rewind/replay with the clock.
|
||
std::vector<double> settlementPop;
|
||
// Civilization Step 5 (conflict): mutable war state -- per-settlement allegiance (conquest), the
|
||
// active wars and the war RNG. Restored on a step back so conquests/revolts rewind with the clock.
|
||
std::vector<int> settlementAllegiance;
|
||
std::vector<War> wars;
|
||
uint32_t warRng = 0, warNextId = 0;
|
||
// Civilization Step 6 (diplomacy): standing realm relations (alliances / rivalries / truces).
|
||
std::vector<DiploTie> diplomacy;
|
||
// Civilization Step 8 (cultural evolution): per-settlement culture (mutable via conversion /
|
||
// assimilation) + the culture-list LENGTH. Culture records are append-only and immutable after
|
||
// creation, so a step back TRUNCATES the list (like colonies truncate settlements) and a
|
||
// deterministic replay re-creates any schism child identically -- no string-bearing records here
|
||
// (the viewer's persisted history frames are written as raw POD).
|
||
std::vector<int> settlementCulture;
|
||
uint32_t cultureCount = 0, cultureNextId = 0;
|
||
};
|
||
|
||
struct Plate {
|
||
int id = 0;
|
||
PlateType type = PlateType::Oceanic; // initial crust type seeded onto cells
|
||
Vec3 driftAxis; // rotation axis (unit) for tangential drift on sphere
|
||
double driftSpeed; // small value used by the Phase-1 uplift stress
|
||
double angSpeed = 0; // Phase-2 advection: rotation rate in radians / My
|
||
double speedCmYr = 0;// surface drift speed in cm/year (for display)
|
||
bool baby = false; // Phase-2: young spreading proto-plate, not yet promoted
|
||
};
|
||
|
||
// A fixed cell on the planet. Geometry never moves; properties flow.
|
||
struct Cell {
|
||
Vec3 unit; // unit-sphere direction (fixed)
|
||
double elevation = 0.0; // meters, relative to sea level (continuous, fine)
|
||
int plateId = -1;
|
||
double geoAge = 0.0; // My since this crust was (re)formed at a ridge
|
||
bool oceanic = true;// crust type travels WITH the cell (Phase-2 advection)
|
||
Biome biome = Biome::Ocean; // Phase-3 climate/biome classification (derived)
|
||
uint16_t editLock = 0; // Edit mode: EditLock bits (save v24), see above
|
||
|
||
// Phase-2 advection accumulator: signed convergence distance built up with
|
||
// the dominant other-plate neighbor (+ encroaching, - rifting), and which
|
||
// plate is encroaching.
|
||
double drift = 0.0;
|
||
int invader = -1;
|
||
|
||
std::vector<int> neighbors;
|
||
|
||
// Phase 4/5 hook: optional fine-resolution subgrid. Null until needed.
|
||
std::shared_ptr<SubGrid> subgrid;
|
||
};
|
||
|
||
struct PlanetConfig {
|
||
double radius = 6.371e6; // meters (Earth default)
|
||
int subdivisions = 5; // icosphere level
|
||
double seaLevel = 0.0; // meters
|
||
int plateCount = 12;
|
||
uint32_t seed = 1337;
|
||
double axialTilt = 23.44; // obliquity, degrees (Earth ~23.4). Visual tilt of
|
||
// the 3D globe + spin axis; groundwork for seasons.
|
||
|
||
// --- Tectonic tuning (Phase 1) ------------------------------------------
|
||
// Relief builds gradually toward an isostatic equilibrium instead of
|
||
// saturating: per-tick uplift competes with a relaxation pull toward the
|
||
// plate's base elevation, so peaks asymptote at base + uplift/relax.
|
||
double continentBase = 300.0; // m, resting elevation of continental crust
|
||
double oceanBase = -6000.0; // m, deep abyssal floor (oldest oceanic crust)
|
||
double upliftGain = 1.3e5; // m/tick per unit convergence stress
|
||
int beltWidth = 3; // cell-rings a mountain belt spreads inland
|
||
double relax = 0.02; // isostatic relaxation toward base, per tick
|
||
|
||
// --- Orogeny: collision uplift + isostatic persistence (Phase 2 inc. 4) --
|
||
// Continent-continent collisions build the tallest ranges; thick (high)
|
||
// continental crust resists isostatic relaxation, so ranges stand and are
|
||
// erosion-limited rather than snapping back to continentBase.
|
||
double collisionFactor = 1.8; // continent-continent uplift multiplier (Himalaya)
|
||
double arcFactor = 1.4; // continental subduction-arc uplift (Andes)
|
||
double isostaticPersist = 0.85; // how much high crust resists relax (0..<1)
|
||
double rootScale = 2500.0;// m above continentBase where persistence saturates
|
||
|
||
// --- Soft peak cap (drift-only): spread mountain heights, no hard plateau --
|
||
// Above peakSoftCapStart the chance that a tick's uplift "takes" falls linearly
|
||
// to 0 at peakSoftCapEnd, so peaks settle across a height band instead of all
|
||
// railing at one ceiling. A lost grow roll forfeits that tick's uplift and
|
||
// shaves a random 0..peakFailDrop metres off. The hard elevation clamp's upper
|
||
// bound tracks peakSoftCapEnd. Active only during drift (forming still settles).
|
||
double peakSoftCapStart = 7000.0; // m: below this, uplift always takes (P=1)
|
||
double peakSoftCapEnd = 12000.0; // m: at/above this, uplift never takes (P=0)
|
||
double peakFailDrop = 200.0; // m: max random drop when the grow roll loses
|
||
|
||
// --- Plate drift (Phase 2) ----------------------------------------------
|
||
double maxDriftSpeed = 20.0; // cm/year; fastest plate (Earth is 1-10)
|
||
double ridgeDepth = -2500.0; // m, elevation of brand-new crust at a ridge
|
||
|
||
// --- Seafloor aging -> depth (Phase 2 inc. 4) ---------------------------
|
||
// Oceanic crust subsides as it ages (half-space cooling): depth =
|
||
// ridgeDepth - seafloorSubsidence * sqrt(geoAge), clamped at oceanBase.
|
||
double seafloorSubsidence = 280.0; // m per sqrt(My) of crustal age
|
||
double seafloorSeedAge = 80.0; // My, initial oceanic age spread at generation
|
||
|
||
// --- Plate dynamics (Phase 2): fission, stalemate kick, spreading plates --
|
||
// Periodic checks run every `splitCheckEvery` drift iterations. A plate over
|
||
// `splitFraction` of all cells may rift in two with probability
|
||
// splitProbBase + splitProbSlope * (percentOverThreshold)
|
||
// (20%->5%, 21%->10%, ... 39%->100%). A plate whose cell count barely changed
|
||
// over a window gets a stalemate "kick" (new direction + a speed boost). Young
|
||
// rift crust grows on a "baby" proto-plate; once it reaches `babyPromoteFrac`
|
||
// of all cells it becomes a real plate and grows volcanic landmass.
|
||
// (Related spreading/volcanic fields are at bottom for binary save compat.)
|
||
int splitCheckEvery = 10; // drift iterations between periodic checks
|
||
double splitFraction = 0.20; // plate share of cells that may rift apart
|
||
double splitProbBase = 0.05; // split probability at the threshold
|
||
double splitProbSlope = 0.05; // added per 1 percentage-point over threshold
|
||
double stalemateEps = 0.005; // |dCells|/cells below this over a window = stuck
|
||
int stalemateWindows = 4; // consecutive stuck windows required before a kick
|
||
double stalemateBoost = 1.5; // speed multiplier when kicking a stuck plate
|
||
int miniPlateCells = 50; // a non-baby plate smaller than this is "mini"
|
||
int fuseMinPlates = 3; // distinct mini plates in a cluster to fuse + steal
|
||
|
||
// --- Erosion + sea level (Phase 2 increment 2) --------------------------
|
||
// erode() moves sediment downhill (highs wear down, basins/seas fill); a
|
||
// proportional sea-level controller holds a target geographic land fraction.
|
||
double erosionLandRate = 0.08; // subaerial erosion fraction / My
|
||
double erosionSeaRate = 0.02; // submarine erosion fraction / My (slower)
|
||
double landFractionTarget = 0.30; // geographic land goal (cells above seaLevel)
|
||
double seaLevelStep = 100.0; // m, fixed nudge per adjustment when off target
|
||
double seaLevelTol = 0.02; // deadband (land-fraction) where sea level rests
|
||
int seaLevelEvery = 100; // erode calls between sea-level adjustments
|
||
// Hard bounds on the controller: if a world's buoyant (continental) crust area
|
||
// permanently sits below landFractionTarget (crust generation is independent of
|
||
// this geographic target -- no guarantee they match), there is no seaLevel low
|
||
// enough to reach the target using real land, and ageing oceanic crust keeps
|
||
// deepening out of reach -- so an unbounded controller sinks seaLevel forever,
|
||
// eventually misclassifying deep aging seafloor as "geographic land". These
|
||
// clamp the candidate each adjustSeaLevel() step so it settles at a plausible
|
||
// coastal offset instead of running away (a slightly-off land fraction beats a
|
||
// physically nonsensical sea level).
|
||
double seaLevelMin = -3000.0; // m, lowest the controller may sink seaLevel
|
||
double seaLevelMax = 3000.0; // m, highest the controller may raise seaLevel
|
||
|
||
// --- Spreading & volcanic (Phase 2 plate dynamics, kept here for binary compat)
|
||
int babyMinCells = 4; // baby blobs smaller than this dissolve (noise)
|
||
double babyPromoteFrac = 0.007; // baby-patch size (x N cells) to become a plate
|
||
double volcanicLandFrac = 0.30; // fraction of a promoted patch turned into land
|
||
double volcanicElev = 400.0; // m, volcanic-island starting elevation
|
||
double landBand = 0.10; // soft land clamp: +/- around targetLand
|
||
|
||
// --- Phase 3: hydrology (rivers, lakes, fluvial erosion) ----------------
|
||
// Macro drainage network on the fixed grid: depression-fill -> lakes,
|
||
// steepest-descent routing -> rivers, stream-power incision + downstream
|
||
// sediment transport/deposition (mass-conserving). Drift keeps running but
|
||
// Phase 3 uses a finer timestep (cflDtMy * phase3DtScale).
|
||
double phase3AfterMy = 300.0; // My of drift before the Phase-3 prompt
|
||
double phase3DtScale = 0.2; // Phase-3 timestep = cflDtMy() * this (finer)
|
||
double rainfall = 1.0; // uniform precip per cell (drainage-area unit)
|
||
double riverThreshold = 25.0; // discharge above which a cell counts as a river
|
||
// (lower = richer network incl. tributaries shown)
|
||
double riverIncision = 0.02; // K in stream-power incision K*Q^m*S^n*dt
|
||
double riverDischargeExp = 0.5; // m: discharge exponent in stream power
|
||
double riverSlopeExp = 1.0; // n: slope exponent in stream power
|
||
double riverTransport = 0.10; // transport-capacity coefficient (cap=this*Q*S)
|
||
double depFrac = 0.25; // fraction of excess load deposited per cell
|
||
|
||
// --- Phase 3: biome classification thresholds (see PlanetBiomes.cpp) -----
|
||
// Temperature model (deg C): equator-warm curve cooling super-linearly toward the
|
||
// poles minus an elevation lapse. Moisture comes from latitude belts + hydrology.
|
||
double biomeEquatorTemp = 30.0; // C at the equator, sea level
|
||
double biomePoleDrop = 58.0; // C drop from equator to pole
|
||
double biomeLatExp = 1.3; // >1 keeps mid-latitudes temperate (cold near poles)
|
||
double biomeElevLapse = 0.0060; // C lost per metre above sea level
|
||
double biomeIceTemp = -9.5; // below -> Ice (polar caps + glaciated peaks); raise = bigger caps
|
||
double biomeTundraTemp = 2.0; // below (and above ice) -> Tundra/Taiga
|
||
double biomeTaigaTemp = 10.0; // cool + wet -> boreal forest
|
||
double biomeSavannaTemp = 22.0; // warm + moderate moisture -> savanna
|
||
double biomeMountainElev= 3000.0; // m above sea level -> Mountains
|
||
double biomeHillsElev = 1200.0; // m above sea level -> Hills
|
||
double biomeBeachBand = 60.0; // m above sea level + adjacent ocean -> Beach
|
||
double biomeLowlandElev = 500.0; // wetlands only below this elevation
|
||
double biomeWetlandMoist= 0.72; // moisture above this (low lowland) -> Wetland
|
||
double biomeDesertMoist = 0.28; // moisture below this -> Desert
|
||
double biomeGrassMoist = 0.50; // moisture below this -> Grassland/Savanna, else Forest
|
||
double biomeTaigaMoist = 0.40; // cool + above this -> Taiga (else Tundra)
|
||
double biomeLakeMinDepth= 20.0; // filled-basin depth above sea level counting as a Lake
|
||
double biomeSeasonWeight= 0.6; // how much winter temp (vs annual mean) sets the cold
|
||
// Tundra/Taiga cutoffs (0 = mean only/old behaviour, 1 = winter)
|
||
|
||
// --- Phase 3: climate (orographic precipitation) -- see PlanetClimate.cpp --
|
||
// Temperature reuses the biome* temperature fields above. Precipitation advects
|
||
// ocean moisture along prevailing (zonal) winds: it rains on windward upslopes and
|
||
// dries out leeward (rain shadow) and far inland (continentality).
|
||
double climateOceanMoisture = 1.0; // moisture air carries leaving the ocean (source)
|
||
double climateRainEfficiency = 0.5; // fraction of available moisture*belt that rains per cell
|
||
double climateOrographic = 3.0; // extra rain per unit normalized upslope (windward)
|
||
double climateOroRefHeight = 500.0; // m of upslope that counts as one orographic unit
|
||
double climateContinentality = 0.05; // moisture lost per land cell crossed (dries interiors)
|
||
int climateWindPasses = 50; // moisture-advection iterations (steady state)
|
||
int climateMoistureSmooth = 12; // precipitation diffusion passes (wet/dry transition zones)
|
||
double climateCurrentFactor = 4.0; // C: max coastal warming/cooling from ocean currents
|
||
// (warm poleward currents raise, cold equatorward lower)
|
||
|
||
// --- Seasons (obliquity) -- see PlanetClimate.cpp -----------------------
|
||
// axialTilt (above) drives a per-cell seasonal temperature range around the annual
|
||
// mean sTemp: summer/winter = mean +/- A, with A = seasonAmpMax * tiltFactor *
|
||
// latShape * continentality. Big swings at high-latitude continental interiors,
|
||
// small near coasts/equator. Static fields (warmest/coldest month), not animated.
|
||
double seasonAmpMax = 18.0; // max seasonal half-amplitude (C) at full tilt/lat/interior
|
||
double seasonLatExp = 1.2; // latitude shape exponent (>1 concentrates swing toward poles)
|
||
double seasonOceanFactor = 0.15; // continentality floor: ocean/coast seasonal swing fraction
|
||
int seasonContinentRings = 6; // ocean-distance rings to reach full continentality (1 = ~223 km)
|
||
|
||
// --- Biota: flora / fauna / funga (see PlanetBiota.cpp + *Gen.cpp) -------
|
||
// Density scalars (derived each tick) drive the colour views; the discrete
|
||
// slot/point population (generated on demand, saved) draws archetypes by size.
|
||
double bioVegTempMin = -5.0; // C below which plants don't grow
|
||
double bioVegTempOpt = 15.0; // C at/above which temperature isn't limiting
|
||
double bioVegMoistRef = 0.5; // normalized moisture where water isn't limiting
|
||
double bioFaunaProductivity = 0.9; // herbivore capacity per unit vegetation
|
||
double bioCarnPreyMin = 0.30; // min local prey (fauna density) to support carnivores
|
||
double bioCarnScale = 1.0; // carnivore weight ramp above the prey threshold
|
||
double bioFungaMoistRef = 0.4; // normalized moisture where fungi aren't water-limited
|
||
double bioFungaFloraWeight = 0.6; // how much fungi lean on flora (organic matter) 0..1
|
||
double bioFungaTempMin = -15.0; // C above which fungi are not cold-limited (cold-tolerant)
|
||
double bioRegionBonus = 0.5; // weight boost for archetypes present in same-biome neighbours
|
||
double bioMarineBase = 0.15; // open-ocean baseline marine flora density (deep, far from land)
|
||
double bioMarineShelfDepth = 2500.0; // m of depth over which shelf (light) productivity fades to base
|
||
int bioMarineCoastRings = 3; // ocean rings from land over which coastal richness fades to base
|
||
int bioFloraSlots = 12; // max distinct flora per cell (point budget caps abundance)
|
||
int bioFaunaSlots = 10; // max distinct fauna per cell
|
||
int bioFungaSlots = 8; // max distinct funga per cell
|
||
int bioFloraPoints = 20; // flora point budget at full density (scaled by density)
|
||
int bioFaunaPoints = 16; // fauna point budget at full density
|
||
int bioFungaPoints = 14; // funga point budget at full density
|
||
|
||
// --- Live World (slow real-time clock) -- see PlanetLive.cpp -------------
|
||
// The finished planet can run on a slow real-time clock (hours -> weeks/months) with a
|
||
// moving day/night terminator, a live seasonal temperature cycle and a moving snow line.
|
||
// dayLengthHours/yearLengthDays set the calendar; snowTemp/seaIceTemp the freezing lines.
|
||
double dayLengthHours = 24.0; // hours in one planetary day (rotation -> day/night)
|
||
double yearLengthDays = 365.25; // days in one planetary year (orbit -> seasons)
|
||
double snowTemp = 0.0; // C: land below the live temperature shows snow
|
||
double seaIceTemp = -2.0; // C: ocean below the live temperature shows sea ice
|
||
double tideAmplitude = 0.6; // m: equilibrium-tide scale per unit tide-raising weight
|
||
double tideSunFactor = 0.46; // sun's tide weight relative to a unit moon (Earth ~0.46)
|
||
|
||
// --- Weather (Live World dynamic clouds & rain) -- see PlanetWeather.cpp -----
|
||
// A per-cell humidity/cloud/rain cycle advanced on the live clock: evaporate over warm
|
||
// sunlit seas, advect along the prevailing wind, condense into cloud (more on windward
|
||
// upslopes), rain out, and dissipate. Rates are per simulated hour.
|
||
double weatherEvapRate = 0.4; // /h: ocean evaporation toward marine saturation
|
||
double weatherWindKmh = 45.0; // km/h: prevailing wind speed for advecting humidity/cloud
|
||
double weatherSatBase = 0.4; // air saturation humidity at 0 C (warmer air holds more)
|
||
double weatherSatTempCoef = 0.025; // saturation rise per +1 C
|
||
double weatherCondense = 0.6; // /h: fraction of supersaturation that becomes cloud
|
||
double weatherOrographic = 0.0009; // extra condensation per m of windward upslope
|
||
double weatherRainThresh = 0.5; // cloud cover above this precipitates
|
||
double weatherRainRate = 0.5; // /h: rain rate from excess cloud
|
||
double weatherCloudDissip = 0.12; // /h: cloud clearing (half returns to humidity)
|
||
|
||
// --- Weather systems (moving lows / hurricanes / typhoons) -- PlanetWeather.cpp ---
|
||
// Drifting low-pressure disturbances travel with the steering wind and stamp cloud/rain onto
|
||
// the grid, so the sky visibly evolves; the intense tropical ones become tropical cyclones.
|
||
int weatherSystemMax = 8; // max concurrent weather systems
|
||
double weatherSpawnRate = 0.06; // /h: genesis probability scale (when below the cap)
|
||
double weatherSystemSpeed = 28.0; // km/h: steering speed at which systems drift
|
||
double weatherTropicalSST = 26.0; // C: min sea-surface temp for tropical genesis
|
||
double weatherSystemRadius= 0.16; // rad: angular radius of a system's cloud/rain shield
|
||
double weatherSystemCloud = 1.2; // /h: cloud stamped at a system's core (scaled by strength)
|
||
double weatherSystemRain = 1.6; // /h: rain intensity at a system's core
|
||
double weatherHurricaneStr= 0.6; // strength above which a tropical system is a hurricane/typhoon
|
||
|
||
// --- Volcanoes (Live World) -- see PlanetVolcano.cpp ------------------------
|
||
// Placed once on entering Live World by tectonic context, then evolve statefully on the live
|
||
// clock (step-back snapshots preserve/reverse that state). Submarine volcanoes build up to
|
||
// breach sea level into new volcanic islands.
|
||
double volcanoProbRidge = 0.55; // per-cell placement prob on a young spreading-ridge cell
|
||
double volcanoProbBorder = 0.06; // per-cell placement prob on a normal plate-border cell
|
||
double volcanoProbInterior = 0.003; // per-cell placement prob elsewhere (intraplate hotspots)
|
||
int volcanoMaxCount = 60; // global cap on placed volcanoes
|
||
double volcanoBuildRate = 0.02; // m/h at activity=1 while growing
|
||
double volcanoFreeHeight = 1000.0;// m absolute elevation below which vents cannot go dormant
|
||
double volcanoInitialBuildMax = 2500.0; // m: max pre-built height on Live World entry
|
||
double volcanoMaxHeight = 3200.0;// m built height where dormancy becomes certain
|
||
double volcanoDormancyRate = 1.0; // /year hazard scale once above volcanoFreeHeight
|
||
double volcanoDormantMinYears = 120.0; // min dormancy before explosion
|
||
double volcanoDormantMaxYears = 1200.0; // max dormancy before explosion
|
||
double volcanoExplodeDropFrac = 0.20; // fraction of built height shaved by an explosion
|
||
double volcanoActivityDecay = 0.70; // activity multiplier after each explosion
|
||
double volcanoDeadActivity = 0.05; // activity floor below which growth stops
|
||
double volcanoBlastRadius = 0.09; // rad: wide ash blast radius around the vent
|
||
double volcanoBlastCloud = 1.5; // cloud added inside the explosion blast
|
||
double volcanoAshMinYears = 0.5; // min sustained ash emission after explosion
|
||
double volcanoAshMaxYears = 3.0; // max sustained ash emission after explosion
|
||
double volcanoAshPuffCellsPerWeek = 2.0;// average local cells puffed per week while ashTimer runs
|
||
double volcanoAshCloud = 0.9; // cloud cover injected at the vent per erupting hour (ash plume)
|
||
double volcanoAshCooling = 6.0; // C: peak local cooling under an active ash plume
|
||
|
||
// --- Geography (the atlas) -- see PlanetGeography.cpp ----------------------
|
||
// Thresholds for extracting + naming geographic features from the frozen terrain. Generated once
|
||
// on a settled world (key M), saved (v17+). Tune to control what counts as a continent vs island,
|
||
// an ocean vs sea, a named mountain range / major river, and to cap label clutter.
|
||
int geoContinentMinCells = 40; // land component >= this many cells = Continent (else Island)
|
||
int geoSeaMaxCells = 60; // ocean/basin <= this many cells = Sea (else Ocean)
|
||
double geoOceanSepRadians = 1.40; // min angular separation between ocean-basin centres (radians; ~4-6 oceans)
|
||
int geoOceanDeep = 4; // min rings from land for a cell to seed an ocean basin
|
||
double geoMountainElev = 2500.0;// m: min elevation for mountain-range membership
|
||
int geoRangeMinCells = 4; // min cells for a named mountain range
|
||
double geoRiverMinDischarge = 80.0; // min mouth discharge for a named river
|
||
int geoMaxRivers = 40; // cap on named rivers (largest by discharge)
|
||
int geoMaxPeaks = 40; // cap on named peaks (highest)
|
||
|
||
// --- Civilization: settlements & habitability -- see PlanetCiv.cpp ----------
|
||
// Placed once on a settled world ("dawn of civilization"); population then grows/declines on the
|
||
// Live World clock toward a food-driven carrying capacity. Habitability blends climate comfort,
|
||
// water access and food (flora/fauna + ecoregion productivity).
|
||
int civMaxSettlements = 80; // cap on settlement sites
|
||
double civMinSpacingRadians = 0.06; // soft suppression scale: a new settlement softens the weight of
|
||
// cells within ~this angle (organic clustering, not a hard grid)
|
||
double civClusterExp = 3.0; // habitability weighting exponent for placement (higher = settlements
|
||
// cluster harder on the best land; 1 = mild, 0 = uniform among habitable)
|
||
double civMinHabitability = 0.22; // don't place a settlement below this habitability
|
||
double civSeedPopulation = 250.0; // initial village population at placement
|
||
double civGrowthRate = 0.02; // logistic growth rate per year (toward carrying capacity)
|
||
double civMaxPopulation = 1.0e6; // carrying-capacity SCALE (multiplied by habitability, site
|
||
// quality and trade -- a top hub's K is several x this; the
|
||
// real metropolis ceiling is the civMetropolisPop crowding)
|
||
double civTownPop = 5000.0; // population at/above which a settlement is a Town
|
||
double civCityPop = 100000.0;// population at/above which a settlement is a City
|
||
double civAbandonPop = 50.0; // below this a settlement is abandoned (dormant; can revive)
|
||
double civHabWaterWeight = 0.45; // habitability weight of water access (rivers/lakes/coast)
|
||
double civHabFoodWeight = 0.40; // habitability weight of food (flora/fauna + ecoregion)
|
||
double civHabTempOpt = 18.0; // C: most comfortable annual-mean temperature
|
||
double civHabElevPenalty = 2500.0; // m above which high terrain steeply reduces habitability
|
||
// Dynamic environment (PlanetCiv.cpp): growth differs by local conditions and varies over time
|
||
// (harvests, droughts, cold years, floods, storms) so settlements aren't static. Deterministic
|
||
// functions of (cell, year, seed) -> reversible with the live stepper; no save change.
|
||
double civSiteVariety = 1.0; // 0 = flat capacities, 1 = full site-quality spread (big rivers/coasts host large cities)
|
||
double civGrowthMin = 0.25; // growth-rate fraction at habitability 0 (1 = at habitability 1)
|
||
double civHarvestVar = 0.25; // base year-to-year harvest swing amplitude (scaled by climate variability)
|
||
double civDroughtStrength = 0.70; // how hard a full drought cuts a region's carrying capacity
|
||
double civDroughtPeriod = 8.0; // years per drought-noise epoch (drought duration scale)
|
||
double civDroughtThresh = -0.15; // drought-onset threshold on the slow noise (lower = rarer)
|
||
double civDroughtArid = 0.50; // extra drought-proneness in arid regions (× aridity)
|
||
double civColdYearStrength = 0.50; // crop loss in a rare cold year, × the cell's cold exposure
|
||
double civFloodBonus = 0.25; // fertile-silt bonus on river cells most years (rare flood disaster)
|
||
double civFamineRate = 0.15; // /year accelerated population loss when food < population
|
||
double civStormDeathRate = 0.50; // /year population loss for a full-strength storm over a settlement
|
||
double civHurricaneDeathMult= 3.0; // extra storm death multiplier for a hurricane/typhoon
|
||
// Big-city demography: pre-industrial metropolises were population sinks (disease, crowding, food
|
||
// logistics), so growth meets a headwind that rises with city size -- cities PLATEAU (~1-2M for the
|
||
// best trade hubs) instead of exponentially chasing a huge carrying capacity for millennia. Plus
|
||
// rare deterministic PLAGUES: probability rises with population and trade connectivity (contagion
|
||
// is the cost of being a hub). All pure functions of (id, year, seed) -> reversible, no save change.
|
||
double civMetropolisPop = 1.5e6; // crowding scale: mortality = civCrowdingLoss x (P/this)^2 per year
|
||
double civCrowdingLoss = 0.02; // /yr crowding mortality at P = civMetropolisPop (0 = no plateau)
|
||
double civCondBoomCap = 1.25; // cap on the good-year condition upside in the K target (droughts uncapped)
|
||
double civPlagueRate = 0.01; // /yr per-settlement plague-outbreak chance (hash gate)
|
||
double civPlagueDeathMin = 0.20; // min total wave kill fraction at full exposure
|
||
double civPlagueDeathMax = 0.40; // max total wave kill fraction (also caps the per-year loss)
|
||
double civPlagueTradeWeight = 0.5; // exposure share driven by trade connectivity vs pure size
|
||
// Territory & nations (PlanetNation.cpp): influence range each settlement projects (size-scaled),
|
||
// realm grouping (vassals/kingdoms), and the empire threshold. Derived -> recomputed, not saved.
|
||
double civTerritoryBase = 0.035; // rad: base influence range of a seed-size village (~220 km)
|
||
double civTerritoryScale = 0.05; // rad added per log10 of (population / seed) -- big cities reach far
|
||
double civTerritoryMax = 0.35; // rad: cap on a single settlement's reach (~2200 km)
|
||
double civVassalRange = 1.5; // a capital annexes smaller settlements within this x its range
|
||
int civEmpireMinMembers = 5; // realm of >= this many settlements counts as an Empire
|
||
double civEmpirePop = 2.5e6; // ...or total population >= this counts as an Empire
|
||
// Civilization Step 5: conflict & war (stateful, saved v21). Neighbouring realms grow hostile and
|
||
// fight; casualties shrink frontier cities, winners conquer (flip) or sack (raze) them, empires
|
||
// fracture as provinces revolt. All rolls come from a separate war RNG (tectonic stream intact).
|
||
int warMaxConcurrent = 6; // cap on simultaneous active wars
|
||
double warDeclareRate = 0.12; // per-year war-declaration chance scale (x hostility)
|
||
double warAmbition = 1.0; // hostility weight of the size gap (strong preys on weak)
|
||
double warIdeology = 0.8; // hostility weight of culture + faith difference
|
||
double warBorder = 0.5; // hostility weight of contested-frontier length
|
||
double warWarlikeMult = 1.4; // military-strength multiplier for a Warlike-ethos realm
|
||
double warCasualtyRate = 0.06; // per war-year frontier-city population loss (loser more)
|
||
double warConquerScore = 0.6; // |warscore| past which the winner takes a frontier city
|
||
double warSackChance = 0.3; // chance a taken city is razed to ruins instead of flipped
|
||
double warExhaustion = 1.5; // |warscore| (or battle count) past which a war ends in peace
|
||
double warRevoltRate = 0.04; // per-year base revolt chance of a held foreign/distant city
|
||
double warMinRealmPop = 2000.0; // realms below this population don't start wars
|
||
// Civilization Step 6: diplomacy (stateful, saved v22). Realm attitudes drift into alliances /
|
||
// rivalries; allies don't fight + join each other's wars; wars end in truces.
|
||
double diploDriftRate = 0.06; // per-year attitude change scale
|
||
double diploAffinity = 1.0; // attitude pull from shared culture + faith (vs difference)
|
||
double diploWarPenalty = 0.5; // extra per-year attitude drop while two realms are at war
|
||
double diploTruceYears = 12.0; // post-war truce length (no new war between the two)
|
||
double diploWarGrudge = 0.4; // one-off attitude drop when a war between them ends (a scar)
|
||
double diploAllyThreshold = 0.5; // attitude at/above which two realms are allied
|
||
double diploNonAggThreshold = 0.2; // attitude at/above which they sign a non-aggression pact
|
||
double diploRivalThreshold = -0.5; // attitude at/below which they become rivals
|
||
// Civilization Step 7: trade & economy (derived, not saved). Trade routes link nearby settlements;
|
||
// prosperity accrues at hubs and boosts growth; trade feeds back into diplomacy + war motivation.
|
||
double tradeLandRange = 0.10; // rad: overland trade reach (~630 km)
|
||
double tradeSeaRange = 0.30; // rad: extra reach when both settlements are coastal (sea route)
|
||
double tradeRiverBonus = 0.06; // rad: reach bonus when either settlement sits on a river
|
||
double tradeMinVolume = 0.05; // links below this volume are dropped
|
||
double tradeProsperityWeight= 0.8; // how much a hub's prosperity multiplies its carrying capacity
|
||
double tradeWarBlock = 0.0; // trade-volume multiplier between realms at war (0 = blockade)
|
||
double tradeAllyBonus = 1.5; // trade-volume multiplier between allied realms
|
||
double tradeDiploBonus = 0.1; // attitude nudge/year between trade-partner realms (reward alliance)
|
||
double tradeTemptWar = 0.3; // war-hostility weight of a wealthy target (rich neighbours tempt war)
|
||
// Civilization colonization (kingdoms found new settlements over time, incl. islands). New settlements
|
||
// are appended, bound to the founder's realm by allegiance + a colonial supply trade link. Derived +
|
||
// deterministic; the settlement set grows (saved as-is, step-back truncates), no save-format change.
|
||
double civColonizeRate = 0.15; // per-eligible-realm per-year chance to found a colony
|
||
double civColonyMinPop = 2.0e5; // a realm must reach this total population to colonize (kingdoms+)
|
||
double civColonyMinHab = 0.30; // minimum habitability of a colony site
|
||
double civColonyReach = 0.12; // rad: overland colonization reach from a realm settlement
|
||
double civColonySeaReach = 0.35; // rad: reach over water from a COASTAL member (islands / abroad)
|
||
double civColonySpacing = 0.05; // rad: a colony must be at least this far from every settlement
|
||
int civMaxColonies = 120; // cap on colonies founded beyond the initial civMaxSettlements
|
||
double civColonySupply = 2.0; // prosperity from the overlord supply link (keeps colonies alive)
|
||
// Civilization Step 8: cultural evolution (stateful, saved v23). Cultures spread along borders
|
||
// (settlements convert under dominant foreign cultural pressure), conquered settlements assimilate
|
||
// into their ruler's culture, and far-flung cultures (overseas colonies) schism into new peoples.
|
||
double cultAssimRate = 0.03; // per-year chance a settlement under foreign allegiance adopts its ruler's culture
|
||
double cultConvertRate = 0.02; // per-year chance scale for border conversion under dominant foreign pressure
|
||
double cultConvertDominance = 2.5; // foreign cultural pressure must exceed this x the own-culture support
|
||
double cultSpreadRange = 0.25; // rad: how far a settlement projects cultural pressure (~1600 km)
|
||
double cultPrestigeWeight = 0.5; // how much trade prosperity boosts a settlement's cultural weight
|
||
int cultSchismMinMembers = 6; // a culture needs at least this many living settlements to schism
|
||
double cultSchismRange = 0.55; // rad from the culture's population centroid past which members are "distant"
|
||
int cultSchismMinCluster = 2; // distant settlements needed to break away together
|
||
double cultSchismRate = 0.08; // per-year chance a qualifying distant cluster becomes a new people
|
||
// Dawn seeding (seedCultures()): a populous continent starts as SEVERAL distinct peoples instead
|
||
// of one continent-spanning culture -- without this, decades of border conversion/backfill tend
|
||
// to erode a single per-continent culture into one super-dominant people across the whole world.
|
||
int cultDawnSettlementsPerCulture = 5; // roughly this many dawn settlements per initial culture on a continent
|
||
int cultDawnMaxPerContinent = 6; // hard cap on how many initial peoples one continent can seed
|
||
};
|