30 Commits

Author SHA1 Message Date
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
cd268167b0 Seed multiple cultures per continent instead of always exactly one
A continent used to always seed exactly one culture at the dawn
(seedCultures(), grouped by regionId), which -- combined with the existing
border-conversion/backfill dynamics -- tended to erode into one
super-dominant culture covering the entire world over a long simulated run.

seedCultures() now splits each continent into 1..cultDawnMaxPerContinent
initial peoples via deterministic farthest-point seeding (a settlement is
assigned to its nearest seed point, giving geographically coherent,
Voronoi-like cultural regions instead of a checkerboard). The target count
scales with the continent's settlement population via the new
cultDawnSettlementsPerCulture config knob, capped by cultDawnMaxPerContinent.
Per-culture ethos/faith/name hashes now fold in Culture.id so multiple
cultures sharing a continent (and therefore the same regionId/bank) get
distinct identities instead of colliding.

This surfaced a real, previously-latent ordering bug: computeTerritory()
(realm/vassalage grouping, which prefers matching by actual culture and only
falls back to same-continent when culture data doesn't exist yet) ran BEFORE
computeCultures() populated per-settlement culture, so on every fresh
recompute realms formed by continent for one full pass before the next
recompute split them correctly. Harmless before this change (one culture per
continent made the two equivalent), but would have left temporarily
multi-cultural realms now. Fixed at the root: split computeCultures() into
refreshSettlementCultures() (the settlement-level seed/backfill/tally slice,
no dependency on `nations`) and had computeTerritory() call it internally
before grouping -- every caller (production and the dozens of existing test
call sites) gets correct behaviour automatically, no call-site changes needed
beyond the two tests whose assertions encoded the old exact one-per-continent
invariant.

test_culture.cpp and test_cultevo.cpp updated to assert the new bounded
(1..cultDawnMaxPerContinent) invariant instead of exact equality, plus that a
single culture never itself spans two continents (still enforced).

Verified live: a single continent now shows six distinct peoples ("the
Thisur", "the Bomebro", "the Draordaes", etc.) instead of one dominant
culture, while realms remain correctly mono-cultural.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
2026-08-30 22:15:12 +02:00
8fec3d82ad Add edit mode: manually edit every cell/settlement property (save v24)
F3 opens a tabbed panel (Geology/Climate/Biota/Settlement) for the
selected tile. Every field writes straight into the field the sim
already reads next tick (Planet::setXxx(), PlanetEdit.cpp), so an
unlocked edit is a one-off nudge the simulation keeps evolving
afterward; a per-field lock button sets a new per-cell EditLock
bitmask that exempts it from automatic recompute (tectonics/erosion/
hydrology/drift/biomes skip a locked cell's write; climate/biota-
density/habitability, which have no other persistent storage, pin a
value in a small sparse map instead). Settlement population/
allegiance/culture locks are keyed by the settlement's home cell.

Adds headless test_edit.cpp (nudge vs. lock for every field, save
round-trip, corrupt-stream rejection, pre-v24 compatibility) and
fixes a real bug found while testing: raylib's default ESC-exits-app
behavior collided with edit mode's Escape-to-cancel, so SetExitKey
is now disabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
2026-08-21 22:35:40 +02:00
ab2c4024f8 Civ growth rebalance: urban crowding plateau, plagues, good-year cap
Cities used to climb a hugely stacked carrying capacity (K up to 20-40M
for the best trade hubs -- civMaxPopulation x habitability x siteQuality
x conditions x trade) at ~2%/yr for millennia: bounded in principle,
endless in practice. Three fixes, all population-only + derived vectors
(pure hashes, no RNG, no save-version bump, step-back exact):

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:52:27 +02:00
4cb0ef9328 Civ Step 8: cultural evolution (assimilation, conversion, schism; save v23)
Cultures stop being static one-per-continent blocs. Culture identities are
now stateful: the list is append-only (seeded once at the dawn, schism
children appended later, records frozen after creation) and the
per-settlement culture is mutable state.

- stepCulture(year) in the yearly tick (pure hashes, no RNG):
  assimilation (a conquered settlement adopts its ruler's culture, which
  drops the Step-5 revolt cultBonus -> assimilation pacifies provinces),
  border conversion (population x trade-prestige pressure; realm capitals
  exempt), schism (a far-flung coherent cluster -- typically overseas
  colonies -- breaks away as a new people with a local-bank NameGen name
  and ethos/faith re-derived from its own lands).
- computeCultures() became a pure derived refresh (seeds only when the
  list is empty); seedCultures() reproduces the old per-continent peoples
  byte-identically for the dawn and pre-v23 loads.
- Colonies inherit the founder's culture at founding; the Step-3
  mono-cultural vassalage rule is culture-matched (regionId fallback) so
  schism clusters found their own realms -> colonial independence wars.
- Per-cell culture view colours by the owning settlement's culture, so a
  conquered city keeps its colour until it assimilates.
- Save v23: culture identities + sSettleCulture + next-id counter, also in
  the step-back frames; snapshots rewind via truncate-and-replay. Pre-v23
  saves re-seed on the next refresh.
- Kind-7 world events; Cultures tab hides extinct peoples and shows a
  schism child's founding year. Knobs cult* in planet.cfg.
- New test_cultevo.cpp (30 checks); all 15 existing suites still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:02:11 +02:00
cdf4319b31 Civ: colonization (kingdoms found colonies + islands) + reseed overlay cleanup
Two things:

1. Fix: pressing R (reseed) after civilization existed left stale territory/
   culture/war/diplomacy/trade overlay lines drawing on the new world.
   regenWorld() now clears the civ overlay segment vectors + their toggles and
   resets a civ colour mode back to Biome.

2. Feature: the settlement set is no longer fixed after U. stepColonization()
   runs once per sim year: a kingdom+ realm (totalPop >= civColonyMinPop) may
   found a new settlement on the best unclaimed, habitable, well-spaced cell
   within reach -- overland, or across water from a coastal member (so islands /
   other continents get colonised). The colony is APPENDED and bound to the
   founder's realm by allegiance (Step 5) + a colonial supply trade link
   (computeTrade, Step 7) so it survives on marginal land while the founder
   lives; when the founder's capital falls, allegiance clears -> supply ends ->
   the colony subsists locally and may wither. Deterministic (pure hashes).

   The growing set needs no save-format change: the v20 settlement block is
   variable-length, and restoreWeather now TRUNCATES settlements to the
   snapshot's count (step-back drops colonies founded after; they re-found
   identically on replay). Founding logs a kind=3 event. civColon* config knobs.

   (Determinism fix: within one stepColonization call the per-settlement arrays
   sSettleNation/sSettleAllegiance/sCivCond are resized on each append, else the
   next realm's member loop read out of bounds.)

test_colony.cpp: colonies founded (set grows), colony in founder's realm + a
supply link, cross-water/island colonies, founder loss frees + de-supplies,
step-back truncates, determinism, save round-trip. All 16 suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:09:22 +02:00
e27b91a128 Civ Step 7: trade & economy
Settlements no longer prosper on local food alone. A derived economic layer:
trade routes link nearby settlements, prosperity accrues at hubs, and prosperity
boosts population growth. Like territory/culture it's a pure function of the
(saved) settlements + geography + wars/diplomacy -- recomputed each sim year,
NO new saved state and NO save-version bump.

- PlanetTrade.{hpp,cpp}: computeTrade() builds TradeLinks (sea when both coastal
  -> longer reach / river / overland) with volume from populations x proximity x
  a political factor (blockaded by war, boosted by an alliance); prosperity =
  base + sum of link volumes (hubs get rich); a per-cell wealth field via a new
  sCellSettleOwner filled by computeTerritory.
- Prosperity feeds carrying capacity in stepCivilization (K *= 1 +
  tradeProsperityWeight*prosperity), so connected coastal/river hubs grow bigger.
- Light feedback in stepConflict (reads last year's economy): trade partners get
  a diplomacy attitude nudge (reward alliance); a wealthy weak neighbour raises
  war hostility (tempt war).
- Render: a Wealth heat map (wealthColor) + trade-route overlay (sea cyan /
  land+river amber) under key Z, a gold wealth dot per settlement in the Civ tab,
  a cell-info prosperity/route line. trade* config knobs (no save block).
- test_trade.cpp: links form (sea over longer range), hubs richer than isolated,
  war blocks trade, prosperity raises carrying capacity, determinism, RNG
  isolation, save->load->recompute parity. All 15 suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:36:41 +02:00
8cd487a5dc Civ Step 6: diplomacy, alliances & coalitions (save v22)
Wars stop being isolated 1-v-1 grudges. Each pair of nearby realms carries an
attitude that drifts over time, crystallising into alliances / non-aggression
pacts / rivalries. Allies don't fight and join each other's wars (coalitions),
and wars end in real peace treaties. Extends the Step-5 conflict subsystem
in place (same stepConflict tick, same war RNG), saved v22 + snapshotted.

- PlanetTypes.hpp: DiploKind + DiploTie {a,b capital indices, attitude, truceUntil,
  kind}; diplo* config knobs; WeatherSnapshot carries diplomacy.
- PlanetConflict.cpp: a diplomacy pass in stepConflict (revolts -> prosecute ->
  diplomacy -> declare). Attitude drifts from culture/faith affinity + a war
  penalty + truce recovery + per-pair noise; reclassified by threshold with
  hysteresis (kind-6 events on change). War-declare skips allied/non-aggression/
  truced pairs; hostility now rises as attitude falls (rivals fight); a defender's
  allies join by declaring their own war on the aggressor; a war end sets a truce
  + grudge. diploBetween/realmsAllied helpers.
- Save v22 + step-back: allegiance/wars block joined by a diplomacy block (new
  hasDiplo readState param + per-frame history block); captureWeather/restoreWeather
  carry the ties.
- Render: green alliance / dark-red rivalry great-circle arcs over the Territory
  view (P), per-realm ally/rival counts + active-wars list in the Realms tab, a
  cell-info allies/rivals line, kind-6 events (= icon).
- test_diplomacy.cpp: alliances/rivalries form, allies never war, coalitions form,
  truces after peace, determinism, RNG isolation, save-v22 + snapshot round-trip.
  All 14 suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:07:10 +02:00
4a1f3377ba Civ Step 5: conflict, war & shifting borders (save v21)
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>
2026-07-01 08:33:38 +02:00
ee2ef3655b Civ Step 3: territory, nations & political borders
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>
2026-06-30 14:00:32 +02:00
868cc90667 Civ: cluster settlement placement (habitability-weighted, irregular)
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>
2026-06-30 13:25:25 +02:00
3655dc0661 Civ settlements: environment-driven dynamic growth (droughts, harvests, storms)
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>
2026-06-30 11:26:56 +02:00
2351fb79a5 Ecoregions atlas + Civilizations Step 2 (habitability & settlements, save v20)
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>
2026-06-30 09:01:43 +02:00
554878ef5e Add geography atlas reshuffle 2026-06-29 21:38:23 +02:00
8ae2f14bae Geography polish: ocean basins, unique name roots, name new islands
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>
2026-06-29 17:24:58 +02:00
a4a06996fa Civilizations Step 1: geography & place-naming (the atlas, save v17)
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>
2026-06-29 17:10:06 +02:00
0c8dbed3d7 Add stateful volcano lifecycle and event log 2026-06-29 14:35:51 +02:00
275511713c Add volcanoes & volcanic islands (Live World, save v14)
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>
2026-06-29 12:48:45 +02:00
b005969ee1 Add marine flora & fauna (life in the ocean)
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>
2026-06-29 12:04:53 +02:00
695e07ba99 Fix deterministic live weather rewind 2026-06-28 20:42:14 +02:00
8efb1e343b Live World stepper: step weather & storms back too (undo history)
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>
2026-06-28 18:44:28 +02:00
47fee0b25f Live World viewer controls: storm follow-cam, 2D map zoom, clock stepper
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>
2026-06-28 18:32:02 +02:00
ca844a5d3f Live World weather: moving systems (lows, hurricanes & typhoons)
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>
2026-06-28 14:40:58 +02:00
f84e06507a Live World weather: dynamic clouds & rain cycle (save v10)
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>
2026-06-28 14:01:34 +02:00
aa32e38dad Live World stage: clock, day/night, seasons, moons, tides & ocean currents
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>
2026-06-28 10:04:45 +02:00
8ed9ae4515 Seasons (obliquity): per-cell summer/winter temps + richer cold biomes
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>
2026-06-27 23:25:45 +02:00
708b23d774 Show more rivers (riverThreshold 50->25) + remove globe subgrid overlay
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>
2026-06-27 23:04:49 +02:00
4aacfdabdd Soft probabilistic peak cap: spread mountain heights, drop the 9000 m plateau
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>
2026-06-27 22:26:38 +02:00
53e371bb2f Add Biota stage: flora, fauna & funga (density + slot/point population)
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>
2026-06-25 13:56:03 +02:00
acc0e5eec9 Initial commit: fanworgen planet sim
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>
2026-06-23 15:08:25 +02:00