37 Commits

Author SHA1 Message Date
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
dae6981dc9 Add realm borders + culture annotation overlay, combinable with any colour mode
Political realm borders and realm-name labels were Territory-mode-only
(showNationBorders() == mode==Territory), so there was no way to see borders
and realm names layered over e.g. the Biome view without losing the
terrain-type coloring. Added an independent showRealms bool (key Q, a "Realm
borders" toggle in Civilization & Ecology) OR-ed with showNationBorders() at
just the border-line and realm-label draw sites (3D, 2D, and exportMapImage()
which shares the 2D path) -- mode/vcolors/recolor() are never touched, so
this overlay draws over any active colour mode without tinting cells. War
fronts and diplomacy arcs stay showNationBorders()-only on purpose, so the
combined view doesn't drag in Territory-only clutter.

Realm-name labels everywhere they're drawn (Territory view, this new toggle,
and the always-on atlas export) now also show the realm's dominant culture
as a smaller "(the Velmar)"-style line via Nation.cultureId ->
planet.cultureList(). exportAtlasImage() forces showRealms on for the
duration of its export (same pattern already used for showSettlements) and
adds the culture line as its own lower-priority label candidate in the
decluttered pass, anchored below each realm's own label so it drops out
gracefully on a dense world via the existing collision check.
AtlasLabel::text changed from a raw pointer to an owned std::string since the
culture strings are built on the fly.

Not reset in regenWorld() -- like showDiplomacy, its underlying nation/
segment lists are already cleared on reseed, so nothing stale can render.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
2026-08-30 21:20:35 +02:00
7cc44cda84 Fix Territory/Culture/Wealth view toggles reverting instantly + orphaned overlay lines
Two independent bugs, both found by reading the raygui source rather than
relying on live clicks:

1. The toolbar's view-mode dropdown re-applied its (stale) selected index back
   onto Viewer::mode every single frame whenever mode was Territory/Culture/
   Wealth/Ecoregion/Habitability -- none of which are in the dropdown's plain
   13-entry list, so this fired unconditionally and reverted the view within
   the same frame it was toggled on, regardless of pause state. Fixed by only
   applying a mode change when the dropdown's active index actually changed
   as a result of that frame's GuiDropdownBox call (verified against raygui's
   own source: it only writes back *active on a genuine item click).

2. showNationBorders/showCultureBorders/showTradeRoutes were separate bools
   set true only inside their own P/X/Z toggle method, with nothing resetting
   them when the user switched views by any other means (number keys, the
   dropdown, another civ toggle) -- so realm borders, war fronts, alliance
   arcs and trade routes, once ever turned on, kept drawing over every other
   view forever. Fixed by deriving all three from `mode` instead of storing
   them separately, removing the possibility of drift entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
2026-08-30 18:28:21 +02:00
56d74aba7b Add a clickable toolbar menu (raygui) alongside the existing keybindings
The app was 100% keyboard-driven (~40 single-key shortcuts dumped as a
dense text list in the HUD) -- this adds a discoverable, mouse-friendly
sidebar menu overlaying the top-right of the 3D globe, built with
raygui (raylib's official header-only widget library, fetched via
CMake FetchContent). Every existing keyboard shortcut keeps working
unchanged: 16 key-handler bodies were extracted from ViewerInput.cpp
into named Viewer methods so both the key and the matching toolbar
widget call the exact same code, and can never diverge.

Sections: a View Mode dropdown (replacing keys 1-0, expanding the old
6-key cycle into 4 explicit entries), Overlays (checkboxes), World/Time
(play/pause, step, fast-forward, a log-scaled speed slider, hydrology,
reseed, save/load), Civilization & Ecology, Live World, and Edit Mode.
Key F1 collapses/expands the sidebar (default expanded); a new
inToolbar input gate keeps interacting with it from also orbiting the
camera or picking a globe tile underneath, and it's disabled while the
Phase-3 modal prompt is up.

Found and fixed a real bug while building this: GuiCheckBox treats its
bounds as the checkbox glyph itself (raygui auto-positions the label
outside it), not a whole clickable row -- a naive full-row checkbox
call drew an oversized glyph with the label clipped off-panel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
2026-08-30 17:17:40 +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
1090d2d46e Harden save/load state validation 2026-07-02 19:11:06 +02:00
99f4a925a1 Fix defensive bounds checks found in review: plate indexing, war/diplomacy load, map zoom reset
- PlanetDrift.cpp: guard cells[i].plateId before indexing plates[] in advect(),
  matching the bounds-checking convention used elsewhere in plate lifecycle code.
- Panels.cpp: same guard in cellInfo() for the cell-info panel's plate lookup.
- PlanetIO.cpp: range-check War.attacker/.defender and DiploTie.a/.b against
  settlements.size() on load, matching the validation already done for
  sSettleAllegiance.
- Viewer.cpp: reset mapZoom/mapPanX/mapPanY in regenWorld() so a zoomed 2D map
  doesn't strand the view on reseed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 19:03:52 +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
cc6813e211 Live World: raise max clock speed to ~20 years/second
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>
2026-07-01 09:01:15 +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
91f1b90a93 Civ Step 4: culture, beliefs & governments
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>
2026-07-01 07:58:58 +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
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
2b0e1f8633 Harden save loading and derived state resets 2026-06-28 21:44:41 +02:00
695e07ba99 Fix deterministic live weather rewind 2026-06-28 20:42:14 +02:00
1358e81426 Persist step-back history in saves (v12): rewind storms past a loaded moment
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>
2026-06-28 19:30:54 +02:00
7631eb4964 Live stepper robustness: manual steps always snapshot; clearer no-history status
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>
2026-06-28 19:25:35 +02:00
b19d2a1703 Fix load: save weather systems (v11) + clear stale step-back history on load
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>
2026-06-28 19:11:39 +02:00
f748940004 Fix: step-back now reverses run-born storms (record undo history continuously)
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>
2026-06-28 19:01:07 +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
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
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