The live clock capped at 1 month/second (liveRate <= 720 sim-hours/s), far too
slow to watch civilizations, wars and empires evolve over centuries. Raise the
ceiling to ~20 sim-years/second (a century in ~5 s).
- liveRateMax() helper (20 x dayLengthHours x yearLengthDays); the [ / ] ramp and
the load-resume clamp both use it (was the hardcoded 720 in two places).
- HUD rate label gains a "yr/s" tier above "mo/s".
- The per-year sim tick (wars + territory) now catches up one year at a time over
any years a frame skips (bounded to 12/frame), so at high speed wars/borders are
still simulated for every year instead of teleporting; a backward step still just
refreshes territory without re-running wars.
Render/input-only: no engine, save-format or config change. Build clean, 13/13 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Realms stop coexisting peacefully and go to war. Unlike the derived Steps 3-4,
war is stateful & path-dependent, so it adds saved state (allegiance + wars +
a war RNG), extends the step-back snapshot, and bumps the save to v21.
- PlanetConflict.{hpp,cpp}: stepConflict(year) runs once per sim year. Neighbouring
realms (adjacency by settlement proximity) grow hostile from ambition (size gap)
+ ideology (culture/faith difference) + contested frontier + a yearly streak, and
declare wars (cap warMaxConcurrent). Each war-year runs a battle (strength =
totalPop x Warlike bonus x defender home advantage), inflicts casualties on
frontier cities, and the winner conquers a loser frontier city (allegiance flips
to the victor) or sacks it (ruins). Conquered foreign/distant cities revolt over
time; a realm that loses its capital collapses -> empires rise and fall. Separate
sWarRng keeps tectonics deterministic.
- computeTerritory() honours sSettleAllegiance (overriding the mono-cultural rule)
with a chain-resolving capital lookup, so borders move as cities change hands.
- Save v21: allegiance + wars + war RNG (new hasConflict readState param + a
per-frame block in the step-back history); WeatherSnapshot + capture/restoreWeather
extended, so ,/. rewind conquests + revolts.
- Render: red war-front lines + a red at-war marker & active-wars list in the Realms
tab over the Territory view (P), a cell-info AT WAR flag, a HUD war count, and
kind=5 events (declare / capture / sack / revolt / peace).
- war* config knobs; test_conflict.cpp covers wars erupting, conquest moving the
border, revolts, determinism, RNG isolation, save-v21 + snapshot round-trip.
All 13 suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give the world peoples and faiths on top of Step 3's realms. All derived
deterministically from the (saved) settlement set + geography (pure hashes,
no RNG, recomputed each sim year -> no save-version bump, step-back free).
- PlanetCulture.{hpp,cpp}: computeCultures() groups living settlements into one
culture per inhabited continent, each with a generated people name, a dominant
ethos picked from its cells' environment (Seafaring/Highland/Nomadic/Agrarian,
else a hashed Mercantile/Warlike), and a religion (a Faith focus biased by the
dominant biome + a generated faith name). Runs after computeTerritory().
- Governments: each realm (Nation) gets a GovType from its tier + a deterministic
pick, folded into its name ("Republic of X", "Duchy of X", "X Theocracy", "X
Confederation", "X Dominion", ...) -- culture-driven renaming.
- Culture-driven grouping: realm vassalage is now restricted to the same
continent, so every kingdom/empire is mono-cultural.
- Render: a Culture colour mode (cultureColor, cultural blocs) + pale
buildCultureBorders lines + a Cultures tab + a cell-info culture/faith line +
government-aware realm labels, all under key X. Cultures recompute with
territory in rebuildTerritory().
- test_culture.cpp: one culture per continent, valid ethos/faith, governments per
tier reflected in names, mono-cultural realms, determinism, RNG isolation,
save->load->recompute parity. All 12 suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entering Live World (W), and starting/stopping hydrology (H or the Start Phase 3
button), now pause the sim first instead of immediately resuming, so you can look
at the world before the new stage runs (press SPACE to start). Leaving Live World
already paused; the continuous forming->drift geological clock is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Group settlements into realms (city-states / kingdoms / empires) and give them
territory + political borders, all derived deterministically from the saved
settlement set (no save-version bump, step-back free).
- PlanetNation.{hpp,cpp}: computeTerritory() — size-scaled influence range per
settlement, realm grouping (a town joins the nearest larger capital within its
annexation reach, else founds its own nation), tier by member count / total pop,
and per-cell ownership maximising range - angular-distance (wilderness frontiers
where no settlement reaches). No RNG → tectonic stream untouched.
- Territory colour mode + nationColor, buildNationBorders (plate dual-contour
reused), realm labels at capitals, a Realms info tab, cell-info realm line, and
kind=4 WorldEvents (realm founded / rises to empire / collapsed). Key P toggles
the view + borders; territory recomputed once per sim year, on placement, load
and step-back.
- civTerritory*/civVassalRange/civEmpire* config knobs (self-describing → no save
break); test_nation.cpp covers ownership/wilderness, empires vs city-states,
realm grouping, tiers, determinism, RNG isolation, save/load parity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Placement used greedy farthest-first with a hard minimum spacing, so settlements
came out as a near-uniform lattice (unrealistic). Now placeSettlements() does
habitability-weighted random sampling (weight = habitability^civClusterExp) with
a soft Gaussian suppression (civMinSpacingRadians) around each pick, so towns
cluster on good land (rivers/coasts/fertile valleys) at irregular spacing and
leave empty stretches between. Nearest-neighbour distances now span ~0.04..0.39
rad (was ~uniform) and placement concentrates on the better cells.
- New knob civClusterExp (3.0; higher = tighter clustering on the best land);
civMinSpacingRadians repurposed as the soft suppression scale (0.10 -> 0.06).
- Separate sCivRng + deterministic, so determinism / RNG isolation hold.
- test_civ: replaced the hard-spacing assertion with clustering checks
(nearest-neighbour spacing varies; placed cells beat the habitable mean).
All 10 suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settlements were static -- every town grew with one global rate to the same
habitability cap (frozen in Live World), so they grew "the same amount
everywhere". Now stepCivilization(dtHours, liveTime) is environment-driven and
time-varying. All drivers are deterministic functions of (region, year, seed),
so step-back replays them with no new saved/snapshot state (population is
already snapshotted).
- Differentiated growth: rate scales with habitability (fertile boom, marginal
crawl); capacity K = maxPop * habitability * siteQuality * conditions.
- Site quality: max city size varies by an order of magnitude with location --
a great river (log-scaled discharge) or coast hosts a metropolis, a dry inland
cell a town (civSiteVariety). This spreads final sizes ~8-9x instead of all
saturating equally.
- Conditions (per ~20deg region, per year): multi-year droughts (worse in arid
regions), year-to-year harvests (bigger swings inland), rare cold years, river
floods, and the existing volcano ash. Regional, so different parts of a
continent have different fortunes.
- Storms kill directly: a system within its radius of a town deals deaths scaled
by strength (hurricanes x civHurricaneDeathMult) -- a parked hurricane can gut
a coastal city. Reads the already-snapshotted storms().
- Famine: accelerated loss when food < population; sustained drought / acute
disaster can collapse a settlement to ruins (revives when conditions return).
- Viewer: markers withered-tinted by hardship (3D + 2D), cell-info "drought/
conditions" line, and cause-attributed kind=3 events ("Hurricane X devastates
Y", "Famine shrinks Y to a Town", tier up/down/abandon).
New civ* knobs (self-describing config -> no save bump). test_civ extended:
sizes vary widely (not uniform), bad-year declines, a hurricane over a town
kills its population. All 10 suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two features (the ecoregions layer was authored locally and was still
uncommitted; civilization Step 2 builds on top and is intermingled in shared
files, so they land together):
Ecoregions (v19, PlanetEcoregions.*, key E):
- generateEcoregions() flood-fills cells sharing biome + land/water context +
productivity band into named ecological provinces (dominant flora/fauna/funga
+ per-kind productivity), a separate sEcoRng. ColorMode::Ecoregion + an Eco
tab + cell-info dominants. Saved v19 (v18 geography reshuffle salt already in).
Civilizations Step 2 (v20, PlanetCiv.*, keys U/I):
- computeHabitability(): derived per-cell food/livability (climate comfort +
water access (rivers/lakes/coast) + food (flora/fauna + ecoregion
productivity), gated by freezing winters / high terrain). ColorMode::
Habitability (key I).
- placeSettlements() (key U, "the dawn"): one-time greedy placement on the best
well-spaced fertile cells (separate sCivRng; named from the continent's
NameGen bank). The set is fixed, so the only mutable per-step state is each
settlement's population.
- stepCivilization(): logistic growth toward K = civMaxPopulation*habitability,
cut where an active volcano ashes the area, so settlements grow / decline /
are abandoned (floored at 1 so a site can revive). Tiers village->town->city.
Runs in liveAdvance; detectLiveEvents logs kind=3 events.
- Step-back snapshots only the population vector (WeatherSnapshot.settlementPop).
3D + 2D tier-sized markers + city/town labels, a Civ tab, cell-info line.
buildGeometry() clears settlements on reseed. Save v20; sCellSettlement
rebuilt on load. civ* config knobs.
New test_ecoregions.cpp + test_civ.cpp; all 10 headless suites pass; GUI build
clean. CLAUDE.md / design-notes / BUILD.md updated (roadmap Step 2 done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feedback fixes on the just-shipped atlas:
- Ocean basins: the connected world ocean was always one feature ("1 ocean",
unrealistic). Now generateGeography partitions it via a distance-from-land
watershed -- greedy farthest-first deep-water centres (geoOceanSepRadians apart,
>= geoOceanDeep rings from land) + multi-source BFS Voronoi over the ocean
graph. Each basin -> a named Ocean (or Sea if small). Defaults (sep 1.4 rad,
deep 4) give ~4-6 oceans on an Earth-like world, 1 on a waterworld. New geo*
knobs geoOceanSepRadians / geoOceanDeep.
- Shared names: features now dedupe on the PROPER-NOUN root (not the formatted
string), so a continent "Karn", a "Karn River" and "Karn Mountains" can't
coexist. NameGen also avoids identical adjacent syllables ("shio-shio").
- New islands: Planet::nameNewLand(cell) adds a volcanic island to the atlas on
the fly when it breaches the sea -- joins an adjacent landmass or mints a fresh
unique Island name; the island-formation WorldEvent now carries that name.
test_geography adds: proper-noun-root uniqueness, nameNewLand join vs mint.
All 8 headless suites pass; GUI build clean. Docs updated. (Save still v17 --
GeoFeature layout unchanged.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The foundation of the civilization arc: name the world so everything civic
(territory, borders, place-of-origin) can reference it. This is pure derived
geometry + a deterministic namer, so it lives in the raylib-free engine and is
fully testable headless. No agents/clock yet -- those come in later steps.
- NameGen.{hpp,cpp} (new): deterministic procedural name generator (xorshift
syllable banks; bankForRegion gives each continent a "language" so its rivers/
mountains share a sound). Reused by the whole civ arc later.
- PlanetGeography.{hpp,cpp} (new): Planet::generateGeography() extracts named
features by connectivity over the fixed grid -- continents/islands (connected
land), oceans/seas (connected water), lakes (inland filled basins), mountain
ranges + peaks (connected high terrain), rivers (largest discharge mouths
traced upstream via flowTo). Separate RNG (sGeoRng) keeps tectonic determinism
intact; per-cell index arrays (sCellLand/Water/Range/River) give O(1) lookup.
- Save v17: geography block (feature records with names + per-cell region arrays)
appended in writeState/readState; readState gains hasGeography; pre-v17 saves
load with none (regenerated on M). geo* config knobs + validation.
- Render: key M toggles place-name labels on globe (manual projection) + 2D map
(minor features only when zoomed); a 5th "Atlas" live-info tab lists features
by kind (click a row -> focusCell); cell-info shows a "region" line. Generated
lazily on a settled world (M) or on entering Live World (W).
- test_geography.cpp (new, in CMake foreach): extraction, per-cell membership,
river-traces-to-sink, names unique/deterministic, RNG isolation, v17 round-trip.
All 8 headless suites pass; GUI build clean. Docs updated (CLAUDE/design-notes/
BUILD), incl. the multi-step civilization roadmap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On entering Live World a one-time pass places volcanoes by tectonic context
(very high prob on young spreading-ridge/"new-plate" cells, medium on plate
borders, low elsewhere). Over the live clock they erupt; submarine vents build
up and breach sea level into new volcanic islands, land vents grow cones, and
each eruption injects a drifting ash cloud + local cooling into the weather.
Design: eruption state (built height + intensity) is a PURE FUNCTION of liveTime
(like insolation/tides/seasons), so the live stepper rewinds islands & eruptions
for free -- no per-cell snapshot, no volcano undo history. The only integrated
side-effect is the ash plume into sCloud (reverts via the weather snapshot).
- src/sim/PlanetVolcano.cpp (new): placeVolcanoes (separate RNG, reservoir-
sampled to volcanoMaxCount; tectonic determinism intact) + stepVolcanoes
(reassert elevation = baseElev + built(liveTime); breach/un-breach; ash).
- Volcano struct + volcano* config knobs (PlanetTypes.hpp); Planet members +
decls; readState gains hasVolcanoes; CONFIG_FIELDS + validateConfig.
- Save bumped to v14: flag-gated volcano block (set + sVolRng) in writeState/
readState; pre-v14 saves load with none and place on next Live World entry.
- Render: 3D cone + eruption glow/ash-plume (DrawCylinderEx) and 2D triangle
markers, key V toggle, HUD line, cell-info volcano line. Lazy placement on W
entry and on loading a live-world save with no volcanoes.
- test_volcano.cpp (new, registered in CMake): determinism, RNG isolation,
context classification + probability ordering, monotonic build + sea-level
breach + step-back recede (pure function of liveTime), ash->cloud, v14
round-trip. All six headless suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The biota density + population layers were hard-gated on elevation<=sea, so
the ocean was barren in the flora/fauna views and held no organisms.
- Density: ocean cells (not under polar Ice) get a marine primary productivity
in computeFloraDensity -- base + (1-base)*max(shelf, coast), where shelf is
shallowness (light) and coast is a BFS ring-distance from land (nutrients).
Rich shelves/coasts, lower open ocean, zero under ice; sMoist (a land field)
is not used at sea. computeFaunaDensity now skips only Ice, so marine fauna =
flora*productivity with the existing carnivore prey-gate clustering big
predators on rich shelves. Funga stays land-only.
- Population: append Ocean-masked archetypes (Kelp/Seagrass/Phytoplankton;
Forage fish/Reef fish/Shark/Baleen whale/Seal/Squid; moistMin=0, SST-zoned).
generateBiota fills ocean cells (skip Ice; no marine funga). fillFlora/
fillFauna unchanged -- their biome-mask filter zones marine vs terrestrial.
Append-only, so v7 saves are unaffected.
- Render: distinct marine ramps (marineFloraColor blue->teal/green bloom,
marineFaunaColor blue->cyan->warm) for water cells in the 8/9 views; land
ramps + funga view unchanged.
- Config: bioMarineBase/bioMarineShelfDepth/bioMarineCoastRings (self-describing
config -> no save bump).
- test_biota.cpp updated: zero life under ice, marine flora/fauna present at sea
+ populate ocean tiles, funga 0 on water; capacity/carnivore-gate/budgets/
determinism still hold. All five headless suites pass; GUI build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Weather is integrated and path-dependent, so reversing it can only restore recorded snapshots --
and a save held only the current moment, so after a load there was no past to step back to and
loaded storms froze on backward step. Save bumped to v12: it now appends the most recent
wxSaveMax(40) step-back frames (each = humidity/cloud/rain + storms + RNG) after the planet
state. loadGame restores them (and still drops any stale pre-load history), so after a load you
can step backward ~40 steps and the storms reverse along their saved track.
Pre-v12 saves load with no step-back history (you can still step forward then back). Adds ~10 MB
to a save (the user chose the short window). All five suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moved the step-back snapshot out of liveAdvance: a manual forward step now ALWAYS records the
pre-step state (rate-independent, so changing the rate or mixing play+step no longer makes the
back-step jump), while a continuous run records a throttled snapshot (~1/sec). Step-back with no
recorded past (e.g. immediately after a load) now shows a clear status instead of silently
freezing the storms. Forward-then-back reverses weather + storms as before.
Note: a load restores the current storms (v11) but not their past trajectory, so stepping back
BEFORE the loaded moment can only rewind the sky -- reversing loaded storms needs the rewind
history, which isn't persisted (see follow-up).
Build clean; logic/weather suites pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After loading, storms vanished on forward play/step but reappeared on step-back. Two causes:
the moving weather systems weren't saved (transient), so a load started with none; and loadGame
didn't clear the wxUndo step-back ring, so stepping back restored STALE snapshots from before
the load (which still held the old session's storms) -- hence 'back shows them, forward doesn't'.
Save bumped to v11: the weather block now also persists sStorms + sWeatherRng + sStormNextId,
so a load resumes the active storms and stepping forward continues them deterministically.
Pre-v11 saves load with no active storms (they respawn); pre-v10 still spin weather up live.
loadGame now clears wxUndo + followId so a load can't restore stale pre-load weather or follow a
gone storm. test_weather.cpp checks the storms round-trip; all five suites pass; GUI build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The step-back undo history was cleared on every continuous-run frame, so weather systems
(storms/hurricanes) created during a normal run had no recorded past. Stepping back then only
rewound the deterministic sky and left the storm frozen at its current spot, resuming motion
only on a forward step.
liveAdvance() now records a snapshot of the pre-advance weather state at ~one-step cadence on
ANY forward advance (continuous run or manual '.'), not just manual steps -- the interval
scales with liveRate, so it's ~one snapshot per real second at any clock rate, in a bounded
ring. liveStepBack() searches the ring for the most recent snapshot before the current time and
restores it (clock + humidity/cloud/rain + storms + RNG), so storms reverse regardless of when
they were born. The clear-on-run was removed; entering Live World still resets the ring.
All five suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backward stepping previously only rewound the deterministic sky (day/night, tides, seasons,
moons) and held the weather, because the cloud/rain/storm state is integrated forward and not
analytically reversible. Now a forward step snapshots the full weather state via new
Planet::captureWeather()/restoreWeather() (humidity/cloud/rain/storms + the storm RNG/next-id)
into a bounded wxUndo ring, and the backward step restores the previous snapshot -- so '.' then
',' reverses EVERYTHING, including clouds, rain and moving storms.
Both steps auto-pause (video frame-step feel). A continuous run (unpause) clears the undo
history, after which ',' falls back to the sky-only rewind. Restoring also reseeds the storm RNG
so re-stepping forward replays deterministically. test_weather.cpp adds a capture/restore
round-trip check; all five suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three viewer features over the Live World sim:
* Storm follow-cam (Y): the 3D camera locks onto a weather system and keeps it centred as it
moves, by pointing along rotateZ(storm.pos,+axialTilt) (model->world) via camYaw/camPitch.
Tracked by a new stable WeatherSystem.id (assigned at spawn from sStormNextId; transient, no
RNG/determinism impact). Cycles by descending strength, off after the last; orbit-drag disabled
while following; auto-releases if the storm dissipates; wheel still zooms.
* 2D map zoom/pan: a virtual projection rect (mapViewRect = mapRect scaled about its centre +
mapPanX/Y) routes every map projection call while the scissor/frame stay mapRect (drawMapTris
now derives y from the rect, not the fixed m.pos, so both axes zoom). Wheel over the map zooms
toward the cursor (1-8x); drag pans when zoomed, else rotates mapLon; 2D picking inverts the
same rect.
* Live-clock stepper: the stepSim live body is factored into liveAdvance(dtClock,dtWeather).
'.' steps forward and ',' back by liveRate hours; backward rewinds the deterministic sky
(day/night, tides, seasons, moon phases) but holds weather (not reversible). S in Live World
aliases the forward step (no longer runs a stray tectonic tick).
All five headless suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The base cloud/rain field relaxes to a static pattern under fixed forcing, so it looked
frozen. stepWeather now also runs a population of drifting WeatherSystem agents (world
objects, not cells; transient/not saved; separate sWeatherRng seeded from cfg.seed so
tectonic determinism is intact): spawn over warm tropical ocean (5-25 deg, SST gate) or a
mid-latitude ocean low (capped at weatherSystemMax); move along the steering wind (sWind at
the nearest cell) plus a poleward recurve at weatherSystemSpeed; intensify over warm sea,
decay and cull over land/cold; and stamp a Gaussian cloud/rain shield onto the grid so cloud
clusters travel and dissipate behind them. A tropical system past weatherHurricaneStr is a
hurricane/typhoon.
Render: an animated cyclonic spiral marker per system (red + eye for cyclones, blue lows;
spins with liveTime by hemisphere) in 3D + 2D, HUD system/cyclone counts, and a basin-named
storm list in the Sky & tides panel -- all under K. New weather* storm knobs. test_weather.cpp
adds: systems spawn, move between steps, thicken cloud, RNG isolation, determinism. All five
suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A per-cell humidity/cloud/rain cycle advanced on the live clock (PlanetWeather.cpp,
raylib-free): evaporate over warm sunlit seas -> advect humidity & cloud along the prevailing
wind (upwind differencing) -> condense into cloud (saturation vs temperature + windward
orographic lift) -> rain out thick cloud -> dissipate. Bounded exp-rate forms keep it stable at
any timestep, so it runs cleanly from hours/sec up to a month/sec. initWeather() spins the
fields up from the moisture climatology; fully deterministic (no RNG).
Render: a translucent cloud shell over the 3D globe (white -> dark slate where it rains,
alpha = cover) plus a matching drawWeather2D layer on the 2D map (shared drawMapTris
rasterizer), toggled with K (default on). stepSim runs stepWeather each live frame at the
sim-hours added to liveTime (held when paused). Cell-info shows cloud/humidity/raining.
Saved as v10 (humidity/cloud/rain, flag-gated; pre-v10 saves spin weather up live). New
weather* config knobs. Reseed/regen now also drops out of Live World. test_weather.cpp:
fields in range, clouds form + rain falls, oceans moister than land, determinism, v10
round-trip; the other four suites still pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A small closed-off ocean body cannot build a real tidal range -- the equilibrium tide
assumes a connected global ocean. computeTides now flood-fills connected ocean bodies and
caps any body under 10 cells to 0.01 m/cell + 0.03 m (a one-cell sea ~0.04 m); open oceans
(>=10 cells) keep the full equilibrium tide. The Sky & tides panel reads the capped level so
it stays consistent with the cell-info panel. test_ocean.cpp covers both cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the slow real-time "Live World" mode (key W on a settled world) that runs the
finished planet on an hours->weeks/months clock (liveTime/liveRate, [ / ] ramp the rate)
with geology frozen. Everything new is a derived per-cell field flowed over the fixed grid.
Day/night & seasons (PlanetLive.cpp): a raylib-free per-cell insolation field
(computeInsolation) drives a moving day/night terminator (N) from time-of-day rotation +
seasonal declination (axialTilt); a live seasonal temperature cycles the static summer/winter
fields over the year (computeLiveSeason); a moving snow/sea-ice line tracks it. Day/night +
snow are render overlays over any colour mode (3D + 2D). Save v8 stores the live clock.
Sky & tides (PlanetOcean.cpp): 1-3 random moons (separate RNG, saved v9) orbit on the
clock and, with the now small/distant sun, raise an equilibrium tide (computeTides -> sTide),
shown as a tide-coloured coastline (T, buildCoastline + tideColor). Moons render with sun-lit
phases, orbit rings and eclipses (solar shadow spot in the day/night overlay, lunar dimming).
The 2D map is left-aligned; the freed space holds a Live-World "Sky & tides" panel (per-moon
phase + a selected coastal tile's tidal phase).
Ocean currents + climate feedback (PlanetOcean.cpp): computeOceanCurrents builds a
per-ocean-cell tangent velocity from wind stress + Coriolis deflection + coast-following
(gyres); computeClimate feeds warm (poleward) / cold (equatorward) currents back into sTemp as
a bounded coastal anomaly (climateCurrentFactor) before seasons, so biomes shift. Rendered as
warm/cold current arrows (O).
Config: dayLengthHours/yearLengthDays/snowTemp/seaIceTemp/tideAmplitude/tideSunFactor/
climateCurrentFactor. Save header v7->v9 (version-gated; older saves load fine). Headless
test_live.cpp + test_ocean.cpp; test_logic/test_biota still pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
axialTilt was visual-only ("groundwork for seasons"). It now drives climate.
computeClimate() adds derived sTempSummer/sTempWinter around the annual mean:
summer/winter = sTemp +/- A, A = seasonAmpMax * sin(tilt)/sin(23.44) *
latShape * continentality
- tiltFactor: 0 tilt -> no seasons, Earth tilt -> 1.
- latShape (pow(|lat|/90, seasonLatExp)): poles swing most.
- continentality: a multi-source BFS ring-distance from ocean cells -- oceans
and coasts are muted by thermal inertia, interiors swing most.
Result: ~0 swing at the equatorial coast, large at high-latitude interiors.
classifyBiomes() blends WINTER temp into the Tundra/Taiga cold cutoffs via
biomeSeasonWeight (0 = annual-mean only = unchanged biomes; default 0.6), so
cold-winter continental interiors become boreal/tundra (Siberia effect). The
amplitude is geographically shaped, so cold biomes expand only where seasons
bite. Fields are derived/not-saved -> no save-format change.
Viewer: color key 6 now CYCLES Temperature -> summer -> winter -> seasonality
(new seasonColor ramp + labels); cell-info shows summer/winter. New season* +
biomeSeasonWeight config knobs (planet.cfg, validated). Docs updated.
Headless (test/season): summer >= mean >= winter; equator swing ~1.6 C vs
~20 C at high latitude; interior land >> ocean; tilt=0 -> no seasons; higher
tilt -> bigger swing; biomeSeasonWeight=0 leaves biomes unchanged; cold-biome
count rises with seasons; deterministic. test_logic + test_biota pass; full
app builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes from user feedback on the hydrology view:
1. Rivers looked too sparse. Investigated coupling rainfall into discharge
(climate precip -> river rainfall): it does NOT work as hoped because river
*location* is set by drainage topology (where water collects), not local
rainfall -- weighting rainfall mostly changes river *size*, and concentrating
a fixed water budget pushes mid-size rivers below the display threshold, so
FEWER rivers show. Reverted that approach. Instead lower the default
riverThreshold 50 -> 25 so tributaries render too: a richer, more visible
network (~2x river cells on test worlds) without descending into noise.
Tunable in planet.cfg.
2. Clicking a tile overlaid a low-res, always-elevation-coloured subgrid patch
on the 3D globe that clashed with the active colour mode and read as a
"strange pattern". Removed the globe overlay; the clicked tile's high-res
subgrid still shows in the right-side detail panel.
test_logic + test_biota pass; full app builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drift-time peaks used to rail into the hard elevation clamp and flatten into a
9000 m plateau (the clamp lived in step(), erode() AND hydrology(), so erosion
re-flattened them every tick). Replace the hard ceiling with a probabilistic
soft cap: above peakSoftCapStart (7000 m) the chance a tick's uplift "takes"
falls linearly to 0 at peakSoftCapEnd (12000 m); a lost grow roll forfeits the
uplift and shaves a random 0..peakFailDrop (200 m) off. Peaks now spread smoothly
across a height band (strong orogeny reaches ~10-11 km, most cluster lower) with
zero cells pinned at the ceiling.
- The roll is a pure hash of (cellIndex, erodeIter, seed): never touches
rngState, bit-identical across OpenMP thread counts, and since erodeIter is
saved (step/erode run 1:1 in drift) F5/F9 resumes bit-identical -- no save bump.
- Drift-only (gated on Planet::drifting) so Phase-1 forming still auto-settles.
- The hard clamp's upper bound now tracks peakSoftCapEnd in all three places
(step/erode/hydrology); lower -11000 m unchanged.
- New planet.cfg knobs peakSoftCapStart / peakSoftCapEnd / peakFailDrop with
validation (+ start < end cross-rule). Docs updated (CLAUDE.md, BUILD.md).
Verified headless: smooth 8.5->10.5 km taper, 0 pinned, determinism + exact
resume intact, test_logic + test_biota pass, full app builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
World-Creation stage after biomes. Two layers (raylib-free engine):
- Per-cell density scalars (flora/fauna/funga in [0,1]) derived from the
climate fields each tick (drive color modes 8/9/0). Flora = Liebig-min of
temp & moisture; fauna ~ flora with carnivores gated on local prey; funga =
moisture/organic-matter-led + cold-tolerant. Zero on water/ice.
- On-demand discrete population (key L, saved as v7): each land cell draws
broad archetypes from a comprehensive table into a per-kind slot cap +
density-scaled point budget (size -> cost), weighted by biome/climate
suitability and a regional bonus for same-biome neighbours. Separate RNG
seeded from cfg.seed so generating biota never perturbs tectonic determinism.
Organisms are labelled by taxonomy (Family + Size + role, e.g. "Felidae
(Big, Carnivore)") with the full Class > Order > Family tree stored, never an
informal common name. Cell-info panel word-wraps + aggregates duplicates so the
lists no longer get cut off.
New: src/sim/PlanetBiota.{hpp,cpp} + PlanetFlora/Fauna/FungiGen.cpp, color
modes/colors, bio* config knobs, save v7 (older saves load with empty
population), test_biota.cpp (densities, fauna<=capacity, carnivore gating,
slot/point budgets, determinism + RNG isolation, v7 round-trip). Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C++/raylib semi-realistic fantasy/sci-fi planet generator on a fixed icosphere
grid (Eulerian: properties flow over fixed cells). World-creation stages:
tectonics, continental drift & erosion, hydrology (rivers/lakes), climate
(temperature + orographic precipitation), and biome classification. Engine in
src/sim (raylib-free, headless-testable), viewer in src/render. See CLAUDE.md
and docs/ (design-notes.md, fauna-flora-plan.md = next step).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>