planetsim/src/sim/PlanetTypes.hpp
Jonas Reith 69f3aa2d22 Fix unbounded sea-level sinking on long runs
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
2026-08-31 19:41:25 +02:00

577 lines
43 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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
};