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>
This commit is contained in:
parent
554878ef5e
commit
2351fb79a5
28
BUILD.md
28
BUILD.md
@ -54,6 +54,9 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
|||||||
K toggle weather clouds/rain cover (Live World)
|
K toggle weather clouds/rain cover (Live World)
|
||||||
V toggle volcano markers (Live World; cones + eruption glow, build into islands)
|
V toggle volcano markers (Live World; cones + eruption glow, build into islands)
|
||||||
M toggle place-name labels (names continents/oceans/ranges/rivers/lakes; Atlas tab)
|
M toggle place-name labels (names continents/oceans/ranges/rivers/lakes; Atlas tab)
|
||||||
|
E ecoregions colour view (names ecological provinces on first use; Eco tab)
|
||||||
|
I habitability heat map (where civilization can thrive)
|
||||||
|
U settlements: the dawn of civilization on first press, then toggle markers (Civ tab)
|
||||||
Y follow-cam: cycle the 3D camera through active storms (Live World; off after last)
|
Y follow-cam: cycle the 3D camera through active storms (Live World; off after last)
|
||||||
. / , step the live clock forward / back by one rate-unit (auto-pauses; back also
|
. / , step the live clock forward / back by one rate-unit (auto-pauses; back also
|
||||||
rewinds weather + storms via an undo history)
|
rewinds weather + storms via an undo history)
|
||||||
@ -277,6 +280,23 @@ once on a settled world, saved v17. The foundation of the civilization arc.
|
|||||||
geoMaxRivers 40 cap on named rivers (largest by discharge)
|
geoMaxRivers 40 cap on named rivers (largest by discharge)
|
||||||
geoMaxPeaks 40 cap on named peaks (highest)
|
geoMaxPeaks 40 cap on named peaks (highest)
|
||||||
|
|
||||||
|
Civilization (PlanetConfig, key U): settlements placed once on the best fertile cells, then their
|
||||||
|
population grows/declines on the Live World clock toward a food-driven carrying capacity. Saved v20.
|
||||||
|
|
||||||
|
civMaxSettlements 80 cap on settlement sites
|
||||||
|
civMinSpacingRadians 0.10 min angular gap between sites (~640 km)
|
||||||
|
civMinHabitability 0.22 don't place a settlement below this habitability
|
||||||
|
civSeedPopulation 250 initial village population
|
||||||
|
civGrowthRate 0.02 logistic growth rate per year
|
||||||
|
civMaxPopulation 2e6 carrying capacity at habitability 1
|
||||||
|
civTownPop 5000 population at/above which a settlement is a Town
|
||||||
|
civCityPop 100000 population at/above which a settlement is a City
|
||||||
|
civAbandonPop 50 below this = abandoned/ruins (can revive)
|
||||||
|
civHabWaterWeight 0.45 habitability weight of water access (rivers/lakes/coast)
|
||||||
|
civHabFoodWeight 0.40 habitability weight of food (flora/fauna + ecoregion productivity)
|
||||||
|
civHabTempOpt 18 C most comfortable annual-mean temperature
|
||||||
|
civHabElevPenalty 2500 m high terrain steeply penalised above this
|
||||||
|
|
||||||
## Headless logic test (no display)
|
## Headless logic test (no display)
|
||||||
|
|
||||||
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
||||||
@ -286,12 +306,12 @@ once on a settled world, saved v17. The foundation of the civilization arc.
|
|||||||
src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetVolcano.cpp \
|
src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetVolcano.cpp \
|
||||||
src/sim/PlanetBiota.cpp \
|
src/sim/PlanetBiota.cpp \
|
||||||
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
||||||
src/sim/NameGen.cpp src/sim/PlanetGeography.cpp \
|
src/sim/NameGen.cpp src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp \
|
||||||
|
src/sim/PlanetCiv.cpp \
|
||||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
|
|
||||||
# Biota / Live World / Ocean / Weather / Volcano / Geography suites: same source list, swap
|
# Same source list for every suite: swap test_logic.cpp -> test_biota / test_live / test_ocean /
|
||||||
# test_logic.cpp -> test_biota.cpp, test_live.cpp, test_ocean.cpp, test_weather.cpp,
|
# test_weather / test_volcano / test_geography / test_ecoregions / test_civ.
|
||||||
# test_volcano.cpp or test_geography.cpp.
|
|
||||||
# The CMake build also includes test_events for the viewer event journal.
|
# The CMake build also includes test_events for the viewer event journal.
|
||||||
|
|
||||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||||
|
|||||||
55
CLAUDE.md
55
CLAUDE.md
@ -108,7 +108,7 @@ the fixed-grid Eulerian model + the climate fields are the groundwork for it.
|
|||||||
**Civilizations (in progress — the long arc after the world is finished):** the eventual goal is
|
**Civilizations (in progress — the long arc after the world is finished):** the eventual goal is
|
||||||
people who eat, name their world, found villages→cities, build kingdoms/empires, draw cultural +
|
people who eat, name their world, found villages→cities, build kingdoms/empires, draw cultural +
|
||||||
geographic borders, and go to war. Built in phases (cell = territory, settlements = point agents, all
|
geographic borders, and go to war. Built in phases (cell = territory, settlements = point agents, all
|
||||||
on the Live World clock). **Step 1 of the roadmap is done:**
|
on the Live World clock). **Steps 1–2 of the roadmap are done (plus a derived ecoregions atlas):**
|
||||||
- **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the
|
- **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the
|
||||||
foundation everything civic references. `Planet::generateGeography()` extracts named features from
|
foundation everything civic references. `Planet::generateGeography()` extracts named features from
|
||||||
the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land),
|
the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land),
|
||||||
@ -123,8 +123,25 @@ on the Live World clock). **Step 1 of the roadmap is done:**
|
|||||||
features is this cell in" (the hook for territory/borders later). Names dedupe on the proper-noun
|
features is this cell in" (the hook for territory/borders later). Names dedupe on the proper-noun
|
||||||
root (no two features share a base name); a **new volcanic island** is named on the fly when it
|
root (no two features share a base name); a **new volcanic island** is named on the fly when it
|
||||||
breaches (the island-formation event carries its name). Saved (**v17**). Knobs `geo*`.
|
breaches (the island-formation event carries its name). Saved (**v17**). Knobs `geo*`.
|
||||||
*Next steps (not yet built): settlements + food/habitability, territory + borders, culture +
|
- **Ecoregions (ecological provinces)** *(done — see `PlanetEcoregions.cpp`)* — `generateEcoregions()`
|
||||||
beliefs, conflict + diplomacy.*
|
flood-fills cells sharing a biome + land/water context + productivity band into named ecological
|
||||||
|
provinces (dominant flora/fauna/funga archetype + flora/fauna/funga productivity per region), a
|
||||||
|
separate `sEcoRng`. Key `E` (colour mode `Ecoregion` + an **Eco** tab); saved (**v19**; v18 added a
|
||||||
|
geography reshuffle salt). Productivity feeds settlement habitability.
|
||||||
|
- **Settlements & habitability (Step 2)** *(done — see `PlanetCiv.cpp`)* — a derived per-cell
|
||||||
|
**habitability/food** score (`computeHabitability`: climate comfort + water access (rivers/lakes/
|
||||||
|
coast) + food (flora/fauna density + ecoregion productivity), gated by freezing winters / high
|
||||||
|
terrain; colour mode `Habitability`, key `I`). On key **`U`** ("the dawn") `placeSettlements()` seeds
|
||||||
|
a fixed set **once** on the best, well-spaced (`civMinSpacingRadians`) fertile cells (separate
|
||||||
|
`sCivRng`; named from the continent's `NameGen` bank). `stepCivilization()` runs each live frame:
|
||||||
|
each settlement's population moves **logistically toward a food-driven carrying capacity** `K =
|
||||||
|
civMaxPopulation·habitability` (cut transiently where an active volcano ashes the area), so it
|
||||||
|
**grows / declines / is abandoned** (kept in the set, can revive). Tiers village→town→city by
|
||||||
|
population; markers (3D + 2D) sized by tier + a **Civ** tab + cell-info line + kind=3 `WorldEvent`s
|
||||||
|
("X grew into a city", "X was abandoned"). Since the set is fixed, the step-back snapshot only
|
||||||
|
restores the per-settlement **population** vector. Saved (**v20**). Knobs `civ*`.
|
||||||
|
*Next steps (not yet built): territory + borders, kingdoms/empires, culture + beliefs, conflict +
|
||||||
|
diplomacy.*
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
@ -532,6 +549,8 @@ src/
|
|||||||
PlanetFungiGen.cpp computeFungaDensity + fillFunga (moisture/organic-matter rule)
|
PlanetFungiGen.cpp computeFungaDensity + fillFunga (moisture/organic-matter rule)
|
||||||
NameGen.* deterministic procedural name generator (syllable banks; reused by civ arc)
|
NameGen.* deterministic procedural name generator (syllable banks; reused by civ arc)
|
||||||
PlanetGeography.* generateGeography() (named features: continents/oceans/ranges/rivers/lakes)
|
PlanetGeography.* generateGeography() (named features: continents/oceans/ranges/rivers/lakes)
|
||||||
|
PlanetEcoregions.* generateEcoregions() (named ecological provinces + dominant biota/productivity)
|
||||||
|
PlanetCiv.* computeHabitability/placeSettlements/stepCivilization (settlements; civ Step 2)
|
||||||
PlanetIO.cpp config file (text) + binary save/load
|
PlanetIO.cpp config file (text) + binary save/load
|
||||||
render/ (raylib viewer)
|
render/ (raylib viewer)
|
||||||
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
||||||
@ -594,12 +613,13 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
|||||||
src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetVolcano.cpp \
|
src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetVolcano.cpp \
|
||||||
src/sim/PlanetBiota.cpp \
|
src/sim/PlanetBiota.cpp \
|
||||||
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
||||||
src/sim/NameGen.cpp src/sim/PlanetGeography.cpp \
|
src/sim/NameGen.cpp src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp \
|
||||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
```
|
```
|
||||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
||||||
`test_weather.cpp`, `test_volcano.cpp` or `test_geography.cpp` to run the Biota / Live World / Ocean /
|
`test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp` or `test_civ.cpp`
|
||||||
Weather / Volcano / Geography suites — same source list. CMake also builds `test_events` for the
|
to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions / Civilization
|
||||||
|
suites — same source list. CMake also builds `test_events` for the
|
||||||
viewer event journal.)
|
viewer event journal.)
|
||||||
|
|
||||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||||
@ -633,6 +653,8 @@ active mode shown top-center of the globe) ·
|
|||||||
all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) ·
|
all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured coastline (Live World) ·
|
||||||
`O` ocean-current arrows (warm/cold) · `K` weather clouds/rain (Live World) ·
|
`O` ocean-current arrows (warm/cold) · `K` weather clouds/rain (Live World) ·
|
||||||
`V` volcano markers (Live World) · `M` place-name labels (the atlas; names the world on first use) ·
|
`V` volcano markers (Live World) · `M` place-name labels (the atlas; names the world on first use) ·
|
||||||
|
`E` ecoregions colour view (names ecology on first use) · `I` habitability heat map ·
|
||||||
|
`U` settlements (the dawn of civilization on first press; toggles markers after) ·
|
||||||
`SPACE` or on-screen button pause ·
|
`SPACE` or on-screen button pause ·
|
||||||
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) ·
|
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) ·
|
||||||
`S` single tick (in **Live World** steps the clock forward) · `.`/`,` step the live clock
|
`S` single tick (in **Live World** steps the clock forward) · `.`/`,` step the live clock
|
||||||
@ -672,7 +694,7 @@ PlanetConfig param, auto-created on first run, reload with `F2`) and
|
|||||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
||||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
||||||
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
||||||
save header is versioned (currently **17**; v2 adds the `[`/`]` drift rate, v3 a
|
save header is versioned (currently **20**; v2 adds the `[`/`]` drift rate, v3 a
|
||||||
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
||||||
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
||||||
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
||||||
@ -681,8 +703,10 @@ humidity/cloud/rain, flag-gated, v11 also persists the **weather systems** + RNG
|
|||||||
active storms, v12 appends the most recent **step-back frames** — `wxSaveMax`(40) weather snapshots
|
active storms, v12 appends the most recent **step-back frames** — `wxSaveMax`(40) weather snapshots
|
||||||
— so a load can rewind storms past the saved moment, v13 appends the Live World clock rate, v14
|
— so a load can rewind storms past the saved moment, v13 appends the Live World clock rate, v14
|
||||||
appends the old pure-function **volcanoes** block, v15 replaces it with stateful volcano lifecycle
|
appends the old pure-function **volcanoes** block, v15 replaces it with stateful volcano lifecycle
|
||||||
agents plus volcano state in step-back frames, v16 appends the saved **event journal**, and v17
|
agents plus volcano state in step-back frames, v16 appends the saved **event journal**, v17
|
||||||
appends the **geography/atlas** block — named features + per-cell region indices;
|
appends the **geography/atlas** block — named features + per-cell region indices, v18 a geography
|
||||||
|
reshuffle salt, v19 the **ecoregions** block, and v20 the **civilization settlements** block (the
|
||||||
|
fixed settlement set + per-frame populations in the step-back history);
|
||||||
newer-than-supported is
|
newer-than-supported is
|
||||||
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
||||||
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
||||||
@ -691,7 +715,8 @@ load with no step-back history (you can still step forward then back); pre-v13 s
|
|||||||
the default live clock rate; pre-v14 saves load with no volcanoes (placed on the next Live World
|
the default live clock rate; pre-v14 saves load with no volcanoes (placed on the next Live World
|
||||||
entry); v14 volcanoes are discarded and reseeded as v15 lifecycle agents, with old history skipped;
|
entry); v14 volcanoes are discarded and reseeded as v15 lifecycle agents, with old history skipped;
|
||||||
pre-v16 saves load with an empty event journal; pre-v17 saves load with no geography (regenerated on
|
pre-v16 saves load with an empty event journal; pre-v17 saves load with no geography (regenerated on
|
||||||
demand via `M`).
|
demand via `M`); pre-v19 saves load with no ecoregions (regenerated via `E`); pre-v20 saves load with
|
||||||
|
no settlements (re-seeded via `U`).
|
||||||
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
||||||
saved one.
|
saved one.
|
||||||
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
||||||
@ -826,6 +851,16 @@ triangles (plates are fixed in phase 1).
|
|||||||
a new volcanic island is named on the fly (`Planet::nameNewLand`, joins an adjacent landmass or mints
|
a new volcanic island is named on the fly (`Planet::nameNewLand`, joins an adjacent landmass or mints
|
||||||
a fresh Island). Name flavour (syllable banks, a "language" per continent) + label fonts/colours are
|
a fresh Island). Name flavour (syllable banks, a "language" per continent) + label fonts/colours are
|
||||||
constants in NameGen.cpp / ViewerRender.cpp, not config.
|
constants in NameGen.cpp / ViewerRender.cpp, not config.
|
||||||
|
- **Civilization / settlements (`civ*` in PlanetConfig / `planet.cfg`):** placement — `civMaxSettlements`
|
||||||
|
(80, cap), `civMinSpacingRadians` (0.10 rad ≈ 640 km, min gap between sites), `civMinHabitability`
|
||||||
|
(0.22, don't place below this). Habitability blend — `civHabWaterWeight` (0.45), `civHabFoodWeight`
|
||||||
|
(0.40, the rest is temperature comfort), `civHabTempOpt` (18 °C, most comfortable mean), `civHabElevPenalty`
|
||||||
|
(2500 m, high terrain steeply penalised above this). Population — `civSeedPopulation` (250, initial
|
||||||
|
village), `civGrowthRate` (0.02/yr logistic rate), `civMaxPopulation` (2e6, the carrying capacity at
|
||||||
|
habitability 1), tier thresholds `civTownPop` (5000) / `civCityPop` (100000), `civAbandonPop` (50,
|
||||||
|
below = abandoned/ruins but can revive). An active volcano's ash within ~1.5× its blast radius cuts a
|
||||||
|
settlement's carrying capacity (the "ashed-out region" decline). Marker sizes/colours are render
|
||||||
|
constants (ViewerRender.cpp).
|
||||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||||
knob for how fast/high relief builds.
|
knob for how fast/high relief builds.
|
||||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||||
|
|||||||
@ -34,6 +34,8 @@ set(SIM_SOURCES
|
|||||||
src/sim/PlanetFungiGen.cpp
|
src/sim/PlanetFungiGen.cpp
|
||||||
src/sim/NameGen.cpp
|
src/sim/NameGen.cpp
|
||||||
src/sim/PlanetGeography.cpp
|
src/sim/PlanetGeography.cpp
|
||||||
|
src/sim/PlanetEcoregions.cpp
|
||||||
|
src/sim/PlanetCiv.cpp
|
||||||
src/sim/PlanetIO.cpp
|
src/sim/PlanetIO.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -72,7 +74,7 @@ if(UNIX AND NOT APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
foreach(test_name logic biota ocean live weather volcano geography)
|
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ)
|
||||||
add_executable(test_${test_name} test_${test_name}.cpp)
|
add_executable(test_${test_name} test_${test_name}.cpp)
|
||||||
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
||||||
add_test(NAME ${test_name} COMMAND test_${test_name})
|
add_test(NAME ${test_name} COMMAND test_${test_name})
|
||||||
|
|||||||
@ -47,7 +47,11 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
|||||||
islands; saved v15).
|
islands; saved v15).
|
||||||
- `NameGen.{hpp,cpp}` — deterministic procedural name generator (syllable banks; reused by the civ arc).
|
- `NameGen.{hpp,cpp}` — deterministic procedural name generator (syllable banks; reused by the civ arc).
|
||||||
- `PlanetGeography.{hpp,cpp}` — `generateGeography()` (named features: continents/oceans/ranges/
|
- `PlanetGeography.{hpp,cpp}` — `generateGeography()` (named features: continents/oceans/ranges/
|
||||||
rivers/lakes; the atlas, saved v17).
|
rivers/lakes; the atlas, saved v17+).
|
||||||
|
- `PlanetEcoregions.{hpp,cpp}` — `generateEcoregions()` (named ecological provinces from biome,
|
||||||
|
land/water context, productivity and broad biota; saved v19).
|
||||||
|
- `PlanetCiv.{hpp,cpp}` — `computeHabitability`/`placeSettlements`/`stepCivilization` (civ Step 2:
|
||||||
|
habitability + settlements that grow/decline on the live clock; saved v20).
|
||||||
- `PlanetIO.cpp` — text config + binary save/load.
|
- `PlanetIO.cpp` — text config + binary save/load.
|
||||||
|
|
||||||
The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration),
|
The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration),
|
||||||
@ -321,16 +325,58 @@ magic`) + a per-feature hash, so it is deterministic and **never perturbs the te
|
|||||||
size) plus four per-cell index arrays (`sCellLand`/`sCellWater`/`sCellRange`/`sCellRiver`) giving O(1)
|
size) plus four per-cell index arrays (`sCellLand`/`sCellWater`/`sCellRange`/`sCellRiver`) giving O(1)
|
||||||
"which features is this cell in" — the hook the later territory/border step will build on. Geography is
|
"which features is this cell in" — the hook the later territory/border step will build on. Geography is
|
||||||
static (terrain is frozen), so it is generated **once** on a settled world (key `M`, in or out of Live
|
static (terrain is frozen), so it is generated **once** on a settled world (key `M`, in or out of Live
|
||||||
World) and **saved (v17)** — names persist so a future culture step can rename places. The viewer draws
|
World) and **saved (v17+)** — names persist so a future culture step can rename places. The viewer draws
|
||||||
names as labels on the globe (the plate-label manual projection) + 2D map (minor features only when
|
names as labels on the globe (the plate-label manual projection) + 2D map (minor features only when
|
||||||
zoomed, to declutter), lists them in an **Atlas** tab (5th live-info tab; click a row → `focusCell`),
|
zoomed, to declutter), lists them in an **Atlas** tab (5th live-info tab; click a row → `focusCell`),
|
||||||
and adds a "region" line to cell-info. Save v17 appends the feature records (with `std::string` names,
|
and adds a "region" line to cell-info. Save v17 appends the feature records (with `std::string` names,
|
||||||
written field-by-field) + the POD index arrays; pre-v17 saves load with none and regenerate on demand.
|
written field-by-field) + the POD index arrays; pre-v17 saves load with none and regenerate on demand.
|
||||||
|
Save v18 appends the active geography reshuffle salt (`Shift+M`) so repeated renames continue after
|
||||||
|
load.
|
||||||
Names dedupe on the **proper-noun root** (not the formatted string), so a continent, its river and its
|
Names dedupe on the **proper-noun root** (not the formatted string), so a continent, its river and its
|
||||||
mountains can't share a base name. New land created during Live World (a volcanic island breaching the
|
mountains can't share a base name. New land created during Live World (a volcanic island breaching the
|
||||||
sea) is added to the atlas on the fly by `Planet::nameNewLand(cell)` — it joins an adjacent existing
|
sea) is added to the atlas on the fly by `Planet::nameNewLand(cell)` — it joins an adjacent existing
|
||||||
landmass or mints a fresh unique Island name, which the island-formation `WorldEvent` then carries.
|
landmass or mints a fresh unique Island name, which the island-formation `WorldEvent` then carries.
|
||||||
|
|
||||||
|
## Ecoregions — named ecological provinces
|
||||||
|
|
||||||
|
`PlanetEcoregions.cpp` (engine, raylib-free, deterministic) adds the next atlas-like layer after
|
||||||
|
geography. `Planet::generateEcoregions()` ensures climate/biomes, biota density, hydrology and
|
||||||
|
geography exist, then flood-fills connected cells by **biome + land/ocean/wet context + similar
|
||||||
|
productivity band**. Tiny fragments merge into an adjacent compatible region when possible. Each
|
||||||
|
`Ecoregion` stores a name, biome, anchor cell, containing geography feature id, size, average
|
||||||
|
flora/fauna/funga productivity and dominant broad flora/fauna/funga archetype. If the discrete biota
|
||||||
|
population exists (`L`), dominants come from the actual placed organisms; otherwise they are inferred
|
||||||
|
from density + archetype suitability. Ecoregions summarize existing broad ecology — they do not create
|
||||||
|
new species or a food-web simulation.
|
||||||
|
|
||||||
|
Names use `NameGen` with the containing geography bank, so ecological names inherit regional sound
|
||||||
|
without touching the tectonic RNG. Viewer key `E` lazily generates/toggles the ecoregion colour view,
|
||||||
|
cell-info shows the local ecoregion and dominants, and Live World has an **Eco** tab (6th tab; click
|
||||||
|
a row → `focusCell`). Save **v19** appends the ecoregion records and `sCellEcoregion`; pre-v19 saves
|
||||||
|
load with none and regenerate on demand.
|
||||||
|
|
||||||
|
## Civilization Step 2 — habitability & settlements
|
||||||
|
|
||||||
|
`PlanetCiv.cpp` (engine, raylib-free, deterministic, separate `sCivRng`). `computeHabitability()` is a
|
||||||
|
derived per-cell food/livability score (0..1): a weighted blend of temperature comfort, water access
|
||||||
|
(river `discharge`, adjacent lake, coast) and food (`floraDensity`+`faunaDensity`+the cell's ecoregion
|
||||||
|
productivity), gated by freezing winters and high elevation. `placeSettlements()` (key `U`, "the dawn")
|
||||||
|
seeds a **fixed** set once — greedily the highest-habitability cells with a minimum angular spacing
|
||||||
|
(the ocean-basin farthest-first idiom) — naming each from its continent's `NameGen` bank.
|
||||||
|
|
||||||
|
Because placement is one-time, the settlement *set* never changes, so the only mutable per-step state
|
||||||
|
is each settlement's **population** — which is all the step-back snapshot stores (a `vector<double>` in
|
||||||
|
`WeatherSnapshot`, restored in `restoreWeather`; no per-frame string churn). `stepCivilization(dtHours)`
|
||||||
|
runs in `liveAdvance` after `stepVolcanoes`: each population moves logistically toward a carrying
|
||||||
|
capacity `K = civMaxPopulation·habitability(cell)`, cut transiently where an active volcano's ash plume
|
||||||
|
(`ashTimer>0`, within ~1.5× blast radius) overlaps — so towns **grow, decline, and are abandoned**
|
||||||
|
(floored at 1 so a site can revive when K recovers). Tier (village/town/city) is derived from
|
||||||
|
population. The viewer compares before/after populations in `detectLiveEvents` to log kind=3
|
||||||
|
`WorldEvent`s (tier crossings, abandonment), draws markers (3D spheres + 2D dots, sized by tier; city/
|
||||||
|
town names as 3D labels), adds a **Civ** tab (7th) and a cell-info line, and a `Habitability` colour
|
||||||
|
mode (key `I`). `buildGeometry()` clears the set on reseed (like geography/volcanoes). Save **v20**
|
||||||
|
appends the settlement records (population included); `sCellSettlement` is rebuilt on load. Knobs `civ*`.
|
||||||
|
|
||||||
## Headless testing
|
## Headless testing
|
||||||
|
|
||||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||||
|
|||||||
@ -91,6 +91,8 @@ const char* colorModeName(ColorMode m) {
|
|||||||
case ColorMode::FloraDensity: return "Flora density";
|
case ColorMode::FloraDensity: return "Flora density";
|
||||||
case ColorMode::FaunaDensity: return "Fauna density";
|
case ColorMode::FaunaDensity: return "Fauna density";
|
||||||
case ColorMode::FungaDensity: return "Funga density";
|
case ColorMode::FungaDensity: return "Funga density";
|
||||||
|
case ColorMode::Ecoregion: return "Ecoregions";
|
||||||
|
case ColorMode::Habitability: return "Habitability";
|
||||||
case ColorMode::TempSummer: return "Temperature (summer)";
|
case ColorMode::TempSummer: return "Temperature (summer)";
|
||||||
case ColorMode::TempWinter: return "Temperature (winter)";
|
case ColorMode::TempWinter: return "Temperature (winter)";
|
||||||
case ColorMode::Seasonality: return "Seasonality (summer-winter)";
|
case ColorMode::Seasonality: return "Seasonality (summer-winter)";
|
||||||
@ -140,6 +142,36 @@ Color marineFaunaColor(double d01) { // deep blue -> cyan -> warm (rich shelve
|
|||||||
return Color{ L(0), L(1), L(2), 255 };
|
return Color{ L(0), L(1), L(2), 255 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color habitabilityColor(double h01) { // barren grey -> green -> fertile gold
|
||||||
|
double t = std::clamp(h01, 0.0, 1.0);
|
||||||
|
static const unsigned char key[3][3] = {
|
||||||
|
{ 70, 74, 82 }, // 0.0 barren grey
|
||||||
|
{ 70, 150, 80 }, // 0.5 green
|
||||||
|
{ 230, 205, 90 }, // 1.0 fertile gold
|
||||||
|
};
|
||||||
|
double s = t * 2.0; int k = std::min(1, (int)s); double f = s - k;
|
||||||
|
auto L = [&](int c) { return (unsigned char)(key[k][c] + (key[k + 1][c] - key[k][c]) * f); };
|
||||||
|
return Color{ L(0), L(1), L(2), 255 };
|
||||||
|
}
|
||||||
|
|
||||||
|
Color ecoregionColor(int id, Biome b, double productivity) {
|
||||||
|
if (id < 0) return Color{ 48, 52, 58, 255 };
|
||||||
|
Color base = ColorFromHSV(std::fmod((id + 11) * 0.61803398875f, 1.0f) * 360.0f, 0.55f, 0.82f);
|
||||||
|
Color bio = biomeColor(b);
|
||||||
|
double p = std::clamp(productivity, 0.0, 1.0);
|
||||||
|
auto mix = [&](unsigned char a, unsigned char c, double t) {
|
||||||
|
return (unsigned char)(a * (1.0 - t) + c * t);
|
||||||
|
};
|
||||||
|
double biomeWeight = 0.35;
|
||||||
|
Color out{ mix(base.r, bio.r, biomeWeight), mix(base.g, bio.g, biomeWeight),
|
||||||
|
mix(base.b, bio.b, biomeWeight), 255 };
|
||||||
|
double brighten = 0.72 + 0.28 * p;
|
||||||
|
out.r = (unsigned char)std::clamp(out.r * brighten, 0.0, 255.0);
|
||||||
|
out.g = (unsigned char)std::clamp(out.g * brighten, 0.0, 255.0);
|
||||||
|
out.b = (unsigned char)std::clamp(out.b * brighten, 0.0, 255.0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// Temperature ramp over ~[-40, 40] C: deep blue -> cyan -> green -> yellow -> red.
|
// Temperature ramp over ~[-40, 40] C: deep blue -> cyan -> green -> yellow -> red.
|
||||||
Color tempColor(double celsius) {
|
Color tempColor(double celsius) {
|
||||||
double t = std::clamp((celsius + 40.0) / 80.0, 0.0, 1.0); // 0 cold .. 1 hot
|
double t = std::clamp((celsius + 40.0) / 80.0, 0.0, 1.0); // 0 cold .. 1 hot
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
||||||
FloraDensity, FaunaDensity, FungaDensity,
|
FloraDensity, FaunaDensity, FungaDensity,
|
||||||
|
Ecoregion, Habitability,
|
||||||
TempSummer, TempWinter, Seasonality }; // 6 cycles these temp sub-views
|
TempSummer, TempWinter, Seasonality }; // 6 cycles these temp sub-views
|
||||||
|
|
||||||
Color elevationColor(double e, double seaLevel);
|
Color elevationColor(double e, double seaLevel);
|
||||||
@ -38,3 +39,6 @@ Color fungaColor(double d01);
|
|||||||
// blue->cyan->warm. Used for water cells in the flora/fauna views.
|
// blue->cyan->warm. Used for water cells in the flora/fauna views.
|
||||||
Color marineFloraColor(double d01);
|
Color marineFloraColor(double d01);
|
||||||
Color marineFaunaColor(double d01);
|
Color marineFaunaColor(double d01);
|
||||||
|
Color ecoregionColor(int id, Biome b, double productivity);
|
||||||
|
// Habitability heat map (0..1): barren grey -> fertile green/gold (where civilization can thrive).
|
||||||
|
Color habitabilityColor(double h01);
|
||||||
|
|||||||
@ -61,6 +61,42 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
|||||||
const char* lk = (c.elevation > p.cfg.seaLevel) ? nameOf(p.cellWater()) : nullptr;
|
const char* lk = (c.elevation > p.cfg.seaLevel) ? nameOf(p.cellWater()) : nullptr;
|
||||||
if (lk) L.push_back(std::string(" ") + lk);
|
if (lk) L.push_back(std::string(" ") + lk);
|
||||||
}
|
}
|
||||||
|
if (p.ecoregionsBuilt()) {
|
||||||
|
const auto& E = p.ecoregions();
|
||||||
|
const auto& ce = p.cellEcoregion();
|
||||||
|
int ei = (i < (int)ce.size()) ? ce[i] : -1;
|
||||||
|
if (ei >= 0 && ei < (int)E.size()) {
|
||||||
|
const Ecoregion& e = E[ei];
|
||||||
|
L.push_back(std::string("ecoregion: ") + e.name);
|
||||||
|
auto dom = [&](const char* tag, int arch) {
|
||||||
|
if (arch < 0 || arch >= (int)biotaArchetypes().size()) return;
|
||||||
|
Organism o{ (uint16_t)arch, (uint8_t)e.biome };
|
||||||
|
const BiotaArchetype& a = biotaArchetypes()[arch];
|
||||||
|
L.push_back(std::string(" ") + tag + ": " + organismName(o) +
|
||||||
|
" (" + roleName(a.role) + ")");
|
||||||
|
};
|
||||||
|
dom("flora", e.dominantFlora);
|
||||||
|
dom("fauna", e.dominantFauna);
|
||||||
|
dom("funga", e.dominantFunga);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Civilization: the cell's settlement (if any) + its habitability/food score.
|
||||||
|
if (sized(p.habitability()))
|
||||||
|
L.push_back(std::string(TextFormat("habitability %.0f%%", p.habitability()[i] * 100.0)));
|
||||||
|
if (p.settlementsPlaced()) {
|
||||||
|
const auto& cs = p.cellSettlement();
|
||||||
|
int si = (i < (int)cs.size()) ? cs[i] : -1;
|
||||||
|
if (si >= 0 && si < (int)p.settlements.size()) {
|
||||||
|
const Settlement& s = p.settlements[si];
|
||||||
|
SettleTier t = settleTierOf(s.population, p.cfg.civTownPop, p.cfg.civCityPop);
|
||||||
|
bool alive = s.population >= p.cfg.civAbandonPop;
|
||||||
|
const char* pop = s.population >= 1.0e6 ? TextFormat("%.2fM", s.population / 1.0e6)
|
||||||
|
: s.population >= 1.0e3 ? TextFormat("%.0fk", s.population / 1.0e3)
|
||||||
|
: TextFormat("%.0f", s.population);
|
||||||
|
L.push_back(std::string(alive ? settleTierName(t) : "Ruins of") + " " + s.name
|
||||||
|
+ " (pop " + pop + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
// Climate (derived; present once computeClimate() has run).
|
// Climate (derived; present once computeClimate() has run).
|
||||||
if (sized(p.temperature()) && sized(p.moisture()))
|
if (sized(p.temperature()) && sized(p.moisture()))
|
||||||
L.push_back(std::string(TextFormat("temp %.1f C precip %.0f%%",
|
L.push_back(std::string(TextFormat("temp %.1f C precip %.0f%%",
|
||||||
|
|||||||
@ -119,6 +119,9 @@ void Viewer::recolor() {
|
|||||||
const std::vector<double>& flora = planet.floraDensity();
|
const std::vector<double>& flora = planet.floraDensity();
|
||||||
const std::vector<double>& fauna = planet.faunaDensity();
|
const std::vector<double>& fauna = planet.faunaDensity();
|
||||||
const std::vector<double>& funga = planet.fungaDensity();
|
const std::vector<double>& funga = planet.fungaDensity();
|
||||||
|
const std::vector<int>& ecoCell = planet.cellEcoregion();
|
||||||
|
const auto& eco = planet.ecoregions();
|
||||||
|
const std::vector<double>& hab = planet.habitability();
|
||||||
vcolors.resize(planet.cells.size());
|
vcolors.resize(planet.cells.size());
|
||||||
for (size_t i = 0; i < planet.cells.size(); ++i) {
|
for (size_t i = 0; i < planet.cells.size(); ++i) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
@ -143,6 +146,20 @@ void Viewer::recolor() {
|
|||||||
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255}
|
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255}
|
||||||
: (planet.cells[i].elevation <= planet.cfg.seaLevel ? marineFaunaColor(fauna[i]) : faunaColor(fauna[i])); break;
|
: (planet.cells[i].elevation <= planet.cfg.seaLevel ? marineFaunaColor(fauna[i]) : faunaColor(fauna[i])); break;
|
||||||
case ColorMode::FungaDensity: vcolors[i] = funga.empty() ? Color{90,90,90,255} : fungaColor(funga[i]); break;
|
case ColorMode::FungaDensity: vcolors[i] = funga.empty() ? Color{90,90,90,255} : fungaColor(funga[i]); break;
|
||||||
|
case ColorMode::Ecoregion: {
|
||||||
|
int ei = (i < ecoCell.size()) ? ecoCell[i] : -1;
|
||||||
|
double prod = (ei >= 0 && ei < (int)eco.size())
|
||||||
|
? std::max({ eco[ei].floraProductivity, eco[ei].faunaProductivity, eco[ei].fungaProductivity })
|
||||||
|
: 0.0;
|
||||||
|
vcolors[i] = (ei >= 0 && ei < (int)eco.size())
|
||||||
|
? ecoregionColor(ei, eco[ei].biome, prod) : Color{55, 58, 64, 255};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ColorMode::Habitability:
|
||||||
|
vcolors[i] = (i < hab.size() && planet.cells[i].elevation > planet.cfg.seaLevel
|
||||||
|
&& planet.cells[i].biome != Biome::Ice)
|
||||||
|
? habitabilityColor(hab[i]) : Color{30, 42, 64, 255}; // ocean/ice: dim blue
|
||||||
|
break;
|
||||||
default: vcolors[i] = elevationColor(planet.cells[i].elevation, planet.cfg.seaLevel);
|
default: vcolors[i] = elevationColor(planet.cells[i].elevation, planet.cfg.seaLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,7 +298,8 @@ void Viewer::appendEvent(uint8_t kind, uint8_t severity, double timeHours, int c
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
||||||
const std::vector<Volcano>& beforeVolcanoes) {
|
const std::vector<Volcano>& beforeVolcanoes,
|
||||||
|
const std::vector<Settlement>& beforeSettlements) {
|
||||||
auto beforeStorm = [&](uint32_t id) -> const WeatherSystem* {
|
auto beforeStorm = [&](uint32_t id) -> const WeatherSystem* {
|
||||||
for (const WeatherSystem& ws : beforeStorms) if (ws.id == id) return &ws;
|
for (const WeatherSystem& ws : beforeStorms) if (ws.id == id) return &ws;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@ -332,6 +350,29 @@ void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
|||||||
std::string(TextFormat("%s exploded, +%.0f m remains", kind, v.built)));
|
std::string(TextFormat("%s exploded, +%.0f m remains", kind, v.built)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Civilization (kind=3): a settlement crossing a tier boundary or being abandoned / revived. The
|
||||||
|
// set is fixed, so compare by index against the before-snapshot.
|
||||||
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
||||||
|
auto popLine = [&](const Settlement& st) -> std::string {
|
||||||
|
double p = st.population;
|
||||||
|
if (p >= 1.0e6) return std::string(TextFormat("pop %.1fM", p / 1.0e6));
|
||||||
|
if (p >= 1.0e3) return std::string(TextFormat("pop %.0fk", p / 1.0e3));
|
||||||
|
return std::string(TextFormat("pop %.0f", p));
|
||||||
|
};
|
||||||
|
for (size_t k = 0; k < planet.settlements.size() && k < beforeSettlements.size(); ++k) {
|
||||||
|
const Settlement& s = planet.settlements[k];
|
||||||
|
const Settlement& o = beforeSettlements[k];
|
||||||
|
SettleTier tb = settleTierOf(o.population, townP, cityP), ta = settleTierOf(s.population, townP, cityP);
|
||||||
|
bool aliveB = o.population >= abP, aliveA = s.population >= abP;
|
||||||
|
if (aliveB && !aliveA)
|
||||||
|
appendEvent(3, 2, liveTime, s.cell, s.id, s.name + " was abandoned", popLine(s));
|
||||||
|
else if (!aliveB && aliveA)
|
||||||
|
appendEvent(3, 1, liveTime, s.cell, s.id, s.name + " was resettled", popLine(s));
|
||||||
|
else if (aliveA && (int)ta > (int)tb)
|
||||||
|
appendEvent(3, 1, liveTime, s.cell, s.id,
|
||||||
|
s.name + " grew into a " + settleTierName(ta), popLine(s));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Viewer::focusCell(int idx, const std::string& status) {
|
void Viewer::focusCell(int idx, const std::string& status) {
|
||||||
@ -396,6 +437,7 @@ void Viewer::saveGame(const char* path) {
|
|||||||
os.write((char*)&f.w.rng, 4); os.write((char*)&f.w.nextId, 4);
|
os.write((char*)&f.w.rng, 4); os.write((char*)&f.w.nextId, 4);
|
||||||
wV(f.w.volcanoes);
|
wV(f.w.volcanoes);
|
||||||
os.write((char*)&f.w.volRng, 4);
|
os.write((char*)&f.w.volRng, 4);
|
||||||
|
wD(f.w.settlementPop); // v20: per-frame settlement populations
|
||||||
}
|
}
|
||||||
// v16: persistent world event journal, separate from step-back history.
|
// v16: persistent world event journal, separate from step-back history.
|
||||||
uint32_t en = (uint32_t)std::min<size_t>(events.size(), (size_t)EVENT_LOG_MAX);
|
uint32_t en = (uint32_t)std::min<size_t>(events.size(), (size_t)EVENT_LOG_MAX);
|
||||||
@ -429,7 +471,7 @@ void Viewer::loadGame(const char* path) {
|
|||||||
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
||||||
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
||||||
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
||||||
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15, ver >= 17, ver >= 18)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes, v17 geography, v18 geography salt
|
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15, ver >= 17, ver >= 18, ver >= 19, ver >= 20)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes, v17 geography, v18 geography salt, v19 ecoregions, v20 settlements
|
||||||
cfg = planet.cfg; // adopt the loaded config
|
cfg = planet.cfg; // adopt the loaded config
|
||||||
elapsedMy = em; settled = (st != 0);
|
elapsedMy = em; settled = (st != 0);
|
||||||
planet.drifting = settled; // resume drift boosts iff mid-drift
|
planet.drifting = settled; // resume drift boosts iff mid-drift
|
||||||
@ -462,6 +504,13 @@ void Viewer::loadGame(const char* path) {
|
|||||||
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(Volcano)));
|
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(Volcano)));
|
||||||
if (!is) historyOk = false;
|
if (!is) historyOk = false;
|
||||||
};
|
};
|
||||||
|
auto rP = [&](std::vector<double>& v){ // settlement populations (not cell-sized)
|
||||||
|
uint64_t m = 0; is.read((char*)&m, 8);
|
||||||
|
if (!is || m > 1000000) { historyOk = false; v.clear(); return; }
|
||||||
|
v.resize((size_t)m);
|
||||||
|
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(double)));
|
||||||
|
if (!is) historyOk = false;
|
||||||
|
};
|
||||||
uint32_t hn = 0; is.read((char*)&hn, 4);
|
uint32_t hn = 0; is.read((char*)&hn, 4);
|
||||||
if (!is || hn > (uint32_t)wxUndoMax) historyOk = false;
|
if (!is || hn > (uint32_t)wxUndoMax) historyOk = false;
|
||||||
for (uint32_t k = 0; k < hn && is; ++k) {
|
for (uint32_t k = 0; k < hn && is; ++k) {
|
||||||
@ -474,6 +523,7 @@ void Viewer::loadGame(const char* path) {
|
|||||||
is.read((char*)&f.w.rng, 4); is.read((char*)&f.w.nextId, 4);
|
is.read((char*)&f.w.rng, 4); is.read((char*)&f.w.nextId, 4);
|
||||||
rV(f.w.volcanoes);
|
rV(f.w.volcanoes);
|
||||||
is.read((char*)&f.w.volRng, 4);
|
is.read((char*)&f.w.volRng, 4);
|
||||||
|
if (ver >= 20) rP(f.w.settlementPop); // v20: per-frame settlement populations
|
||||||
auto sized = [&](const std::vector<double>& v) { return v.empty() || v.size() == planet.cells.size(); };
|
auto sized = [&](const std::vector<double>& v) { return v.empty() || v.size() == planet.cells.size(); };
|
||||||
if (!is || !sized(f.w.humidity) || !sized(f.w.cloud) || !sized(f.w.rain)
|
if (!is || !sized(f.w.humidity) || !sized(f.w.cloud) || !sized(f.w.rain)
|
||||||
|| f.w.humidity.size() != f.w.cloud.size() || f.w.humidity.size() != f.w.rain.size())
|
|| f.w.humidity.size() != f.w.cloud.size() || f.w.humidity.size() != f.w.rain.size())
|
||||||
@ -601,17 +651,20 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
|||||||
}
|
}
|
||||||
std::vector<WeatherSystem> beforeStorms;
|
std::vector<WeatherSystem> beforeStorms;
|
||||||
std::vector<Volcano> beforeVolcanoes;
|
std::vector<Volcano> beforeVolcanoes;
|
||||||
|
std::vector<Settlement> beforeSettlements;
|
||||||
if (dtWeather > 0.0) {
|
if (dtWeather > 0.0) {
|
||||||
beforeStorms = planet.storms();
|
beforeStorms = planet.storms();
|
||||||
beforeVolcanoes = planet.volcanoes;
|
beforeVolcanoes = planet.volcanoes;
|
||||||
|
beforeSettlements = planet.settlements;
|
||||||
}
|
}
|
||||||
planet.stepWeather(dtWeather);
|
planet.stepWeather(dtWeather);
|
||||||
// Volcanoes are stateful lifecycle agents; step-back restores their snapshot, then dt=0 here
|
// Volcanoes are stateful lifecycle agents; step-back restores their snapshot, then dt=0 here
|
||||||
// reasserts restored terrain/biome state without advancing the lifecycle.
|
// reasserts restored terrain/biome state without advancing the lifecycle.
|
||||||
VolcanoUpdate vu = planet.stepVolcanoes(dtWeather);
|
VolcanoUpdate vu = planet.stepVolcanoes(dtWeather);
|
||||||
if (dtWeather > 0.0) detectLiveEvents(beforeStorms, beforeVolcanoes);
|
CivUpdate cu = planet.stepCivilization(dtWeather); // population grows/declines on the clock
|
||||||
|
if (dtWeather > 0.0) detectLiveEvents(beforeStorms, beforeVolcanoes, beforeSettlements);
|
||||||
if (vu.breach) refreshView();
|
if (vu.breach) refreshView();
|
||||||
else if (vu.recolor) recolor();
|
else if (vu.recolor || cu.recolor) recolor();
|
||||||
rebuildLiveOverlay();
|
rebuildLiveOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||||
struct Viewer {
|
struct Viewer {
|
||||||
// ---- Files / save format ------------------------------------------------
|
// ---- Files / save format ------------------------------------------------
|
||||||
static constexpr uint32_t SAVE_VERSION = 18; // v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
static constexpr uint32_t SAVE_VERSION = 20; // v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
||||||
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
||||||
static constexpr int EVENT_LOG_MAX = 200;
|
static constexpr int EVENT_LOG_MAX = 200;
|
||||||
const char* CONFIG_PATH = "planet.cfg";
|
const char* CONFIG_PATH = "planet.cfg";
|
||||||
@ -100,7 +100,8 @@ struct Viewer {
|
|||||||
bool showClouds = true; // Live World cloud/rain cover overlay (key K)
|
bool showClouds = true; // Live World cloud/rain cover overlay (key K)
|
||||||
bool showVolcanoes = true; // Live World volcano markers (cones + eruption glow, key V)
|
bool showVolcanoes = true; // Live World volcano markers (cones + eruption glow, key V)
|
||||||
bool showNames = false; // geographic place-name labels (the atlas, key M)
|
bool showNames = false; // geographic place-name labels (the atlas, key M)
|
||||||
std::vector<int> atlasRowCells; // cell to focus per visible Atlas-tab row (parallel to the list)
|
bool showSettlements = true; // civilization settlement markers (key U seeds + toggles)
|
||||||
|
std::vector<int> atlasRowCells; // cell to focus per visible Atlas/Eco/Civ-tab row (parallel to the list)
|
||||||
|
|
||||||
// World event journal: currently Live World events, shaped to be reused by later phases.
|
// World event journal: currently Live World events, shaped to be reused by later phases.
|
||||||
struct WorldEvent {
|
struct WorldEvent {
|
||||||
@ -114,7 +115,7 @@ struct Viewer {
|
|||||||
};
|
};
|
||||||
std::vector<WorldEvent> events;
|
std::vector<WorldEvent> events;
|
||||||
uint32_t nextEventId = 1;
|
uint32_t nextEventId = 1;
|
||||||
int liveInfoTab = 0; // 0 Sky, 1 Tides, 2 Weather, 3 Events, 4 Atlas
|
int liveInfoTab = 0; // 0 Sky, 1 Tides, 2 Weather, 3 Events, 4 Atlas, 5 Eco
|
||||||
std::vector<Rectangle> liveInfoTabRects;
|
std::vector<Rectangle> liveInfoTabRects;
|
||||||
std::vector<Rectangle> eventRowRects;
|
std::vector<Rectangle> eventRowRects;
|
||||||
std::vector<int> eventRowIndices; // indices into events for visible event rows
|
std::vector<int> eventRowIndices; // indices into events for visible event rows
|
||||||
@ -172,7 +173,8 @@ struct Viewer {
|
|||||||
void appendEvent(uint8_t kind, uint8_t severity, double timeHours, int cell, uint32_t sourceId,
|
void appendEvent(uint8_t kind, uint8_t severity, double timeHours, int cell, uint32_t sourceId,
|
||||||
const std::string& title, const std::string& detail);
|
const std::string& title, const std::string& detail);
|
||||||
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
||||||
const std::vector<Volcano>& beforeVolcanoes);
|
const std::vector<Volcano>& beforeVolcanoes,
|
||||||
|
const std::vector<Settlement>& beforeSettlements);
|
||||||
void focusCell(int idx, const std::string& status = "");
|
void focusCell(int idx, const std::string& status = "");
|
||||||
|
|
||||||
// ---- Input (ViewerInput.cpp) --------------------------------------------
|
// ---- Input (ViewerInput.cpp) --------------------------------------------
|
||||||
|
|||||||
@ -44,7 +44,7 @@ void Viewer::handleInput() {
|
|||||||
focusCell(events[ei].cell, events[ei].title);
|
focusCell(events[ei].cell, events[ei].title);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (liveInfoTab == 4) { // Atlas: click a feature row to fly there
|
} else if (liveInfoTab >= 4) { // Atlas/Eco/Civ: click a row to fly there
|
||||||
for (size_t i = 0; i < eventRowRects.size() && i < atlasRowCells.size(); ++i) {
|
for (size_t i = 0; i < eventRowRects.size() && i < atlasRowCells.size(); ++i) {
|
||||||
if (!CheckCollisionPointRec(mp, eventRowRects[i])) continue;
|
if (!CheckCollisionPointRec(mp, eventRowRects[i])) continue;
|
||||||
if (atlasRowCells[i] >= 0) focusCell(atlasRowCells[i], "");
|
if (atlasRowCells[i] >= 0) focusCell(atlasRowCells[i], "");
|
||||||
@ -185,10 +185,36 @@ void Viewer::handleInput() {
|
|||||||
}
|
}
|
||||||
if (IsKeyPressed(KEY_L) && settled) { // generate / regenerate biota population
|
if (IsKeyPressed(KEY_L) && settled) { // generate / regenerate biota population
|
||||||
planet.generateBiota();
|
planet.generateBiota();
|
||||||
|
if (planet.ecoregionsBuilt()) planet.generateEcoregions();
|
||||||
if (mode != ColorMode::FaunaDensity && mode != ColorMode::FungaDensity)
|
if (mode != ColorMode::FaunaDensity && mode != ColorMode::FungaDensity)
|
||||||
{ mode = ColorMode::FloraDensity; recolor(); }
|
{ mode = ColorMode::FloraDensity; recolor(); }
|
||||||
setStatus("Biota generated (flora/fauna/funga)");
|
setStatus("Biota generated (flora/fauna/funga)");
|
||||||
}
|
}
|
||||||
|
if (IsKeyPressed(KEY_E) && settled) { // generate / toggle ecoregion atlas colour view
|
||||||
|
if (!planet.ecoregionsBuilt()) planet.generateEcoregions();
|
||||||
|
mode = (mode == ColorMode::Ecoregion) ? ColorMode::Biome : ColorMode::Ecoregion;
|
||||||
|
recolor();
|
||||||
|
setStatus(mode == ColorMode::Ecoregion ? "Ecoregions on" : "Ecoregions off");
|
||||||
|
}
|
||||||
|
if (IsKeyPressed(KEY_I) && settled) { // toggle the habitability heat-map view
|
||||||
|
planet.computeHabitability();
|
||||||
|
mode = (mode == ColorMode::Habitability) ? ColorMode::Biome : ColorMode::Habitability;
|
||||||
|
recolor();
|
||||||
|
setStatus(mode == ColorMode::Habitability ? "Habitability on" : "Habitability off");
|
||||||
|
}
|
||||||
|
if (IsKeyPressed(KEY_U) && settled) { // civilization: seed on first press ("the dawn"), then toggle markers
|
||||||
|
if (!planet.settlementsPlaced()) {
|
||||||
|
planet.placeSettlements();
|
||||||
|
showSettlements = true;
|
||||||
|
appendEvent(3, 1, liveTime, planet.settlements.empty() ? 0 : planet.settlements[0].cell, 0,
|
||||||
|
"Civilization begins",
|
||||||
|
std::string(TextFormat("%d villages founded", (int)planet.settlements.size())));
|
||||||
|
setStatus(TextFormat("Civilization begins (%d settlements)", (int)planet.settlements.size()));
|
||||||
|
} else {
|
||||||
|
showSettlements = !showSettlements;
|
||||||
|
setStatus(showSettlements ? "Settlements on" : "Settlements off");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock)
|
if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock)
|
||||||
liveWorld = !liveWorld;
|
liveWorld = !liveWorld;
|
||||||
if (liveWorld) {
|
if (liveWorld) {
|
||||||
|
|||||||
@ -208,6 +208,29 @@ void Viewer::renderGlobe3D() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Settlement markers (civilization): a dot per settlement, sized + coloured by tier; dim for ruins.
|
||||||
|
if (showSettlements && !planet.settlements.empty()) {
|
||||||
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
||||||
|
for (const Settlement& s : planet.settlements) {
|
||||||
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
||||||
|
const Cell& c = planet.cells[s.cell];
|
||||||
|
float r = visBase + (float)c.elevation * elevExagg + 0.006f;
|
||||||
|
Vector3 p{ (float)(c.unit.x * r), (float)(c.unit.y * r), (float)(c.unit.z * r) };
|
||||||
|
bool alive = s.population >= abP;
|
||||||
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
||||||
|
float rad = t == SettleTier::City ? 0.026f : t == SettleTier::Town ? 0.018f : 0.012f;
|
||||||
|
Color col = !alive ? Color{110, 110, 116, 255}
|
||||||
|
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
||||||
|
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
||||||
|
: Color{210, 130, 85, 255};
|
||||||
|
DrawSphere(p, rad, col);
|
||||||
|
if (alive && t != SettleTier::Village) { // a ring marks notable settlements
|
||||||
|
float rr = visBase + (float)c.elevation * elevExagg + 0.01f;
|
||||||
|
Vector3 e = { (float)(c.unit.x * rr), (float)(c.unit.y * rr), (float)(c.unit.z * rr) };
|
||||||
|
DrawSphereWires(e, rad + 0.008f, 6, 6, Color{255, 245, 210, 150});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (showGrat) drawGraticule3D(graticule, gratR);
|
if (showGrat) drawGraticule3D(graticule, gratR);
|
||||||
// Markers: selected (orange), hovered cell (yellow), hovered subcell (white).
|
// Markers: selected (orange), hovered cell (yellow), hovered subcell (white).
|
||||||
if (selectedCell >= 0) {
|
if (selectedCell >= 0) {
|
||||||
@ -330,6 +353,24 @@ void Viewer::renderMap2D() {
|
|||||||
Color{255, 170, 70, (unsigned char)std::clamp(90.0 + 150.0 * er, 0.0, 255.0)});
|
Color{255, 170, 70, (unsigned char)std::clamp(90.0 + 150.0 * er, 0.0, 255.0)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showSettlements && !planet.settlements.empty()) {
|
||||||
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
||||||
|
float zf = (float)std::min(2.0, mapZoom);
|
||||||
|
for (const Settlement& s : planet.settlements) {
|
||||||
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
||||||
|
double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat);
|
||||||
|
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
||||||
|
bool alive = s.population >= abP;
|
||||||
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
||||||
|
float rad = (t == SettleTier::City ? 4.5f : t == SettleTier::Town ? 3.2f : 2.2f) * zf;
|
||||||
|
Color col = !alive ? Color{120, 120, 126, 255}
|
||||||
|
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
||||||
|
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
||||||
|
: Color{210, 130, 85, 255};
|
||||||
|
DrawCircleV(sp, rad, col);
|
||||||
|
if (alive && t != SettleTier::Village) DrawCircleLines((int)sp.x, (int)sp.y, rad + 2.0f, Color{255, 245, 210, 180});
|
||||||
|
}
|
||||||
|
}
|
||||||
if (phase3 && showRivers) {
|
if (phase3 && showRivers) {
|
||||||
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, vr, mapLon);
|
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, vr, mapLon);
|
||||||
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, vr, mapLon);
|
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, vr, mapLon);
|
||||||
@ -376,17 +417,18 @@ void Viewer::renderLiveInfo() {
|
|||||||
DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255});
|
DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255});
|
||||||
int x = (int)r.x + 14, y = (int)r.y + 10;
|
int x = (int)r.x + 14, y = (int)r.y + 10;
|
||||||
DrawText("Live info", x, y, 20, RAYWHITE);
|
DrawText("Live info", x, y, 20, RAYWHITE);
|
||||||
const char* tabs[5] = { "Sky", "Tides", "Weather", "Events", "Atlas" };
|
const char* tabs[7] = { "Sky", "Tides", "Weather", "Events", "Atlas", "Eco", "Civ" };
|
||||||
float tx = r.x + 10.0f, ty = r.y + 38.0f;
|
float tx = r.x + 10.0f, ty = r.y + 38.0f;
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 7; ++i) {
|
||||||
float tw = (r.width - 20.0f) / 5.0f;
|
float tw = (r.width - 20.0f) / 7.0f;
|
||||||
Rectangle tr{ tx + i * tw, ty, tw - 4.0f, 24.0f };
|
Rectangle tr{ tx + i * tw, ty, tw - 4.0f, 24.0f };
|
||||||
liveInfoTabRects.push_back(tr);
|
liveInfoTabRects.push_back(tr);
|
||||||
bool on = liveInfoTab == i;
|
bool on = liveInfoTab == i;
|
||||||
DrawRectangleRec(tr, on ? Color{42, 48, 68, 255} : Color{18, 22, 34, 255});
|
DrawRectangleRec(tr, on ? Color{42, 48, 68, 255} : Color{18, 22, 34, 255});
|
||||||
DrawRectangleLinesEx(tr, 1, on ? Color{125, 145, 190, 255} : Color{65, 70, 90, 255});
|
DrawRectangleLinesEx(tr, 1, on ? Color{125, 145, 190, 255} : Color{65, 70, 90, 255});
|
||||||
int w = MeasureText(tabs[i], 14);
|
int tfs = 12; // smaller font: 7 tabs are narrow
|
||||||
DrawText(tabs[i], (int)(tr.x + (tr.width - w) * 0.5f), (int)tr.y + 5, 14,
|
int w = MeasureText(tabs[i], tfs);
|
||||||
|
DrawText(tabs[i], (int)(tr.x + (tr.width - w) * 0.5f), (int)tr.y + 6, tfs,
|
||||||
on ? RAYWHITE : Color{155, 165, 185, 255});
|
on ? RAYWHITE : Color{155, 165, 185, 255});
|
||||||
}
|
}
|
||||||
y = (int)r.y + 72;
|
y = (int)r.y + 72;
|
||||||
@ -507,7 +549,7 @@ void Viewer::renderLiveInfo() {
|
|||||||
y += 43;
|
y += 43;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Atlas: named geographic features, grouped by kind; click a row to fly there
|
} else if (liveInfoTab == 4) { // Atlas: named geographic features, grouped by kind; click a row to fly there
|
||||||
const auto& F = planet.geography();
|
const auto& F = planet.geography();
|
||||||
DrawText("Atlas", x, y, 18, Color{200, 205, 220, 255});
|
DrawText("Atlas", x, y, 18, Color{200, 205, 220, 255});
|
||||||
DrawText(TextFormat("%d named", (int)F.size()), (int)(r.x + r.width) - 78, y + 2, 13, Color{145, 155, 175, 255});
|
DrawText(TextFormat("%d named", (int)F.size()), (int)(r.x + r.width) - 78, y + 2, 13, Color{145, 155, 175, 255});
|
||||||
@ -544,6 +586,68 @@ void Viewer::renderLiveInfo() {
|
|||||||
y += 4;
|
y += 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (liveInfoTab == 5) { // Eco: named ecoregions, richest first; click a row to fly there
|
||||||
|
const auto& E = planet.ecoregions();
|
||||||
|
DrawText("Ecoregions", x, y, 18, Color{200, 205, 220, 255});
|
||||||
|
DrawText(TextFormat("%d named", (int)E.size()), (int)(r.x + r.width) - 78, y + 2, 13, Color{145, 155, 175, 255});
|
||||||
|
y += 26;
|
||||||
|
if (E.empty()) {
|
||||||
|
DrawText(planet.ecoregionsBuilt() ? "(none)" : "press E to name ecology", x, y, 14, Color{150, 155, 170, 255});
|
||||||
|
} else {
|
||||||
|
std::vector<int> idx(E.size());
|
||||||
|
for (size_t i = 0; i < E.size(); ++i) idx[i] = (int)i;
|
||||||
|
std::sort(idx.begin(), idx.end(), [&](int a, int b) {
|
||||||
|
double pa = std::max({ E[a].floraProductivity, E[a].faunaProductivity, E[a].fungaProductivity });
|
||||||
|
double pb = std::max({ E[b].floraProductivity, E[b].faunaProductivity, E[b].fungaProductivity });
|
||||||
|
if (std::fabs(pa - pb) > 1e-9) return pa > pb;
|
||||||
|
return E[a].size > E[b].size;
|
||||||
|
});
|
||||||
|
for (int ei : idx) {
|
||||||
|
if (y > (int)(r.y + r.height) - 34) break;
|
||||||
|
const Ecoregion& e = E[ei];
|
||||||
|
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 32.0f };
|
||||||
|
eventRowRects.push_back(row); atlasRowCells.push_back(e.anchorCell);
|
||||||
|
double prod = std::max({ e.floraProductivity, e.faunaProductivity, e.fungaProductivity });
|
||||||
|
DrawRectangleRec(row, Color{18, 22, 34, 205});
|
||||||
|
DrawRectangleLinesEx(row, 1, Color{70, 75, 92, 255});
|
||||||
|
DrawText(e.name.c_str(), (int)row.x + 8, (int)row.y + 2, 14, ecoregionColor(ei, e.biome, prod));
|
||||||
|
DrawText(TextFormat("%s %.0f%% life %d cells", biomeName(e.biome), prod * 100.0, e.size),
|
||||||
|
(int)row.x + 8, (int)row.y + 18, 11, Color{150, 158, 178, 255});
|
||||||
|
y += 35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Civ: settlements by population (largest first); click a row to fly there
|
||||||
|
const auto& S = planet.settlements;
|
||||||
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
||||||
|
DrawText("Settlements", x, y, 18, Color{200, 205, 220, 255});
|
||||||
|
DrawText(TextFormat("%d", (int)S.size()), (int)(r.x + r.width) - 40, y + 2, 13, Color{145, 155, 175, 255});
|
||||||
|
y += 26;
|
||||||
|
if (S.empty()) {
|
||||||
|
DrawText(planet.settlementsPlaced() ? "(none)" : "press U for the dawn of civilization", x, y, 13, Color{150, 155, 170, 255});
|
||||||
|
} else {
|
||||||
|
std::vector<int> idx(S.size());
|
||||||
|
for (size_t i = 0; i < S.size(); ++i) idx[i] = (int)i;
|
||||||
|
std::sort(idx.begin(), idx.end(), [&](int a, int b) { return S[a].population > S[b].population; });
|
||||||
|
for (int si : idx) {
|
||||||
|
if (y > (int)(r.y + r.height) - 22) break;
|
||||||
|
const Settlement& s = S[si];
|
||||||
|
bool alive = s.population >= abP;
|
||||||
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
||||||
|
Color fg = !alive ? Color{120, 120, 128, 255}
|
||||||
|
: t == SettleTier::City ? Color{245, 215, 110, 255}
|
||||||
|
: t == SettleTier::Town ? Color{210, 200, 150, 255}
|
||||||
|
: Color{185, 195, 175, 255};
|
||||||
|
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 19.0f };
|
||||||
|
eventRowRects.push_back(row); atlasRowCells.push_back(s.cell);
|
||||||
|
const char* pop = s.population >= 1.0e6 ? TextFormat("%.1fM", s.population / 1.0e6)
|
||||||
|
: s.population >= 1.0e3 ? TextFormat("%.0fk", s.population / 1.0e3)
|
||||||
|
: TextFormat("%.0f", s.population);
|
||||||
|
DrawText(s.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, fg);
|
||||||
|
const char* tag = !alive ? "ruins" : settleTierName(t);
|
||||||
|
DrawText(TextFormat("%s %s", tag, pop), (int)(r.x + r.width) - 92, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
||||||
|
y += 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -619,11 +723,12 @@ void Viewer::renderHUD() {
|
|||||||
}
|
}
|
||||||
y += 8;
|
y += 8;
|
||||||
line("hover: cell info | click tile: open detail panel | C close");
|
line("hover: cell info | click tile: open detail panel | C close");
|
||||||
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga (*6 cycles mean/summer/winter/season)");
|
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga E eco (*6 cycles mean/summer/winter/season)");
|
||||||
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]",
|
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]",
|
||||||
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
|
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
|
||||||
line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | Shift+M rename/recheck atlas",
|
line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | E eco | I habitability | U settlements [%s]",
|
||||||
showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off"));
|
showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off",
|
||||||
|
!planet.settlementsPlaced() ? "seed" : showSettlements ? "on" : "off"));
|
||||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
|
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
|
||||||
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
|
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
|
||||||
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
||||||
@ -727,6 +832,37 @@ void Viewer::renderFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3D settlement labels: name towns + cities (villages only when zoomed in), same manual projection.
|
||||||
|
if (showSettlements && !planet.settlements.empty()) {
|
||||||
|
Vec3 camPos{cam.position.x, cam.position.y, cam.position.z};
|
||||||
|
Vec3 camTgt{cam.target.x, cam.target.y, cam.target.z};
|
||||||
|
Vec3 forward = (camTgt - camPos).normalized();
|
||||||
|
Vec3 right = forward.cross(Vec3{cam.up.x, cam.up.y, cam.up.z}).normalized();
|
||||||
|
Vec3 up = right.cross(forward);
|
||||||
|
double fovRad = cam.fovy * M_PI / 180.0, aspect = (double)view3DW / view3DH;
|
||||||
|
double projH = std::tan(fovRad * 0.5), projW = projH * aspect;
|
||||||
|
bool zoomed = camDist < 5.0;
|
||||||
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
||||||
|
for (const Settlement& s : planet.settlements) {
|
||||||
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size() || s.population < abP) continue;
|
||||||
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
||||||
|
if (t == SettleTier::Village && !zoomed) continue; // declutter
|
||||||
|
int font = t == SettleTier::City ? 15 : t == SettleTier::Town ? 13 : 12;
|
||||||
|
const Cell& c = planet.cells[s.cell];
|
||||||
|
double sr = visBase + (double)c.elevation * elevExagg + 0.02;
|
||||||
|
Vec3 lp = rotateZ(c.unit, planet.cfg.axialTilt) * sr;
|
||||||
|
if (lp.dot(camPos) <= 0.0) continue;
|
||||||
|
Vec3 rel = lp - camPos; double z = rel.dot(forward);
|
||||||
|
if (z <= 0.0) continue;
|
||||||
|
float sx = (float)((rel.dot(right) / (projW * z) * 0.5 + 0.5) * view3DW);
|
||||||
|
float sy = (float)((0.5 - rel.dot(up) / (projH * z) * 0.5) * view3DH);
|
||||||
|
int w = MeasureText(s.name.c_str(), font);
|
||||||
|
DrawText(s.name.c_str(), (int)sx - w / 2 + 1, (int)sy + 6 + 1, font, Color{0, 0, 0, 190});
|
||||||
|
DrawText(s.name.c_str(), (int)sx - w / 2, (int)sy + 6, font,
|
||||||
|
t == SettleTier::City ? Color{250, 230, 150, 255} : Color{225, 210, 175, 255});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderMap2D();
|
renderMap2D();
|
||||||
renderLiveInfo();
|
renderLiveInfo();
|
||||||
renderPanels();
|
renderPanels();
|
||||||
|
|||||||
@ -57,7 +57,11 @@ void Planet::buildGeometry() {
|
|||||||
geoFeatures.clear(); sGeoRng = cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u; sGeoSalt = 0;
|
geoFeatures.clear(); sGeoRng = cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u; sGeoSalt = 0;
|
||||||
sCellLand.assign(cells.size(), -1); sCellWater.assign(cells.size(), -1);
|
sCellLand.assign(cells.size(), -1); sCellWater.assign(cells.size(), -1);
|
||||||
sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1);
|
sCellRange.assign(cells.size(), -1); sCellRiver.assign(cells.size(), -1);
|
||||||
|
ecoRegions.clear(); sEcoRng = cfg.seed ? (cfg.seed ^ 0xEC011FEu) : 0xEC011FEu;
|
||||||
|
sCellEcoregion.assign(cells.size(), -1);
|
||||||
volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
||||||
|
settlements.clear(); sCivRng = cfg.seed ? (cfg.seed ^ 0x017B1A2Eu) : 0x017B1A2Eu;
|
||||||
|
sCellSettlement.assign(cells.size(), -1); sHabitability.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Planet::clearDerivedState() {
|
void Planet::clearDerivedState() {
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
#include "PlanetTypes.hpp" // Cell, Plate, SubGrid/SubCell, PlanetConfig
|
#include "PlanetTypes.hpp" // Cell, Plate, SubGrid/SubCell, PlanetConfig
|
||||||
#include "PlanetBiota.hpp" // BiotaKind, Organism, CellBiota
|
#include "PlanetBiota.hpp" // BiotaKind, Organism, CellBiota
|
||||||
#include "PlanetGeography.hpp" // FeatureKind, GeoFeature
|
#include "PlanetGeography.hpp" // FeatureKind, GeoFeature
|
||||||
|
#include "PlanetCiv.hpp" // Settlement, SettleTier, CivUpdate
|
||||||
|
#include "PlanetEcoregions.hpp" // Ecoregion
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@ -18,6 +20,8 @@ public:
|
|||||||
std::vector<Moon> moons; // Live World: 1-3 natural satellites (generated + saved)
|
std::vector<Moon> moons; // Live World: 1-3 natural satellites (generated + saved)
|
||||||
std::vector<Volcano> volcanoes; // Live World: stateful lifecycle volcanoes (saved v15)
|
std::vector<Volcano> volcanoes; // Live World: stateful lifecycle volcanoes (saved v15)
|
||||||
std::vector<GeoFeature> geoFeatures; // named geographic features / the atlas (saved v17+)
|
std::vector<GeoFeature> geoFeatures; // named geographic features / the atlas (saved v17+)
|
||||||
|
std::vector<Ecoregion> ecoRegions; // named ecological provinces (saved v19+)
|
||||||
|
std::vector<Settlement> settlements; // civilization: settlements placed once, grow/decline (saved v20+)
|
||||||
|
|
||||||
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
||||||
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
||||||
@ -164,6 +168,26 @@ public:
|
|||||||
const std::vector<int>& cellRange() const { return sCellRange; } // mountain-range feature index (-1)
|
const std::vector<int>& cellRange() const { return sCellRange; } // mountain-range feature index (-1)
|
||||||
const std::vector<int>& cellRiver() const { return sCellRiver; } // river feature index (-1)
|
const std::vector<int>& cellRiver() const { return sCellRiver; } // river feature index (-1)
|
||||||
|
|
||||||
|
// Ecoregion atlas (PlanetEcoregions.cpp): connected ecological provinces built from current
|
||||||
|
// biome, land/water context, productivity and dominant broad biota. Saved (v19).
|
||||||
|
void generateEcoregions();
|
||||||
|
bool ecoregionsBuilt() const { return !ecoRegions.empty(); }
|
||||||
|
const std::vector<Ecoregion>& ecoregions() const { return ecoRegions; }
|
||||||
|
const std::vector<int>& cellEcoregion() const { return sCellEcoregion; } // ecoregion index (-1)
|
||||||
|
|
||||||
|
// Civilization Step 2 (PlanetCiv.cpp). computeHabitability() builds the derived per-cell
|
||||||
|
// habitability/food score (0..1; like climate, not saved). placeSettlements() seeds the fixed
|
||||||
|
// settlement set once on the best-spaced fertile cells (separate RNG; tectonic determinism intact;
|
||||||
|
// auto-builds geography/ecoregions for naming + productivity). stepCivilization() advances each
|
||||||
|
// settlement's population on the live clock toward a food-driven carrying capacity (grows / shrinks
|
||||||
|
// / is abandoned). Saved (v20); step-back restores populations via WeatherSnapshot.
|
||||||
|
void computeHabitability();
|
||||||
|
void placeSettlements();
|
||||||
|
CivUpdate stepCivilization(double dtHours);
|
||||||
|
bool settlementsPlaced() const { return !settlements.empty(); }
|
||||||
|
const std::vector<int>& cellSettlement() const { return sCellSettlement; } // settlement index per cell (-1)
|
||||||
|
const std::vector<double>& habitability() const { return sHabitability; } // 0..1 per cell (derived)
|
||||||
|
|
||||||
// Build a fine-resolution subgrid patch for one macro cell (phase 4/5 hook).
|
// Build a fine-resolution subgrid patch for one macro cell (phase 4/5 hook).
|
||||||
std::shared_ptr<SubGrid> makeSubGrid(int cellIndex, int res) const;
|
std::shared_ptr<SubGrid> makeSubGrid(int cellIndex, int res) const;
|
||||||
|
|
||||||
@ -180,12 +204,14 @@ public:
|
|||||||
// hasVolcanoes: whether the stream carries a volcano block (save v14+). hasStatefulVolcanoes
|
// hasVolcanoes: whether the stream carries a volcano block (save v14+). hasStatefulVolcanoes
|
||||||
// means v15+ lifecycle volcanoes; v14's old pure-function block is consumed and discarded.
|
// means v15+ lifecycle volcanoes; v14's old pure-function block is consumed and discarded.
|
||||||
// hasGeography: whether the stream carries the geography/atlas block (save v17+); hasGeoSalt:
|
// hasGeography: whether the stream carries the geography/atlas block (save v17+); hasGeoSalt:
|
||||||
// whether the stream carries the active reshuffle salt (save v18+). Older saves load with none
|
// whether the stream carries the active reshuffle salt (save v18+); hasEcoregions: whether the
|
||||||
// (regenerated on demand), or with a deterministic first reshuffle if only the salt is absent.
|
// stream carries the ecoregion atlas (save v19+). Older saves regenerate on demand.
|
||||||
|
// hasSettlements: whether the stream carries the civilization settlements block (save v20+); older
|
||||||
|
// saves load with none (re-seeded on demand via the civ key).
|
||||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
||||||
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
|
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
|
||||||
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true,
|
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true,
|
||||||
bool hasGeoSalt = true);
|
bool hasGeoSalt = true, bool hasEcoregions = true, bool hasSettlements = true);
|
||||||
|
|
||||||
// Helpers for rendering / info.
|
// Helpers for rendering / info.
|
||||||
double cellWidthMeters() const; // approx lateral cell spacing
|
double cellWidthMeters() const; // approx lateral cell spacing
|
||||||
@ -277,6 +303,16 @@ private:
|
|||||||
uint32_t sGeoRng = 1;
|
uint32_t sGeoRng = 1;
|
||||||
uint32_t sGeoSalt = 0;
|
uint32_t sGeoSalt = 0;
|
||||||
|
|
||||||
|
// Ecoregions (saved v19). Per-cell ecoregion index (-1 = none) + separate RNG salt.
|
||||||
|
std::vector<int> sCellEcoregion;
|
||||||
|
uint32_t sEcoRng = 1;
|
||||||
|
|
||||||
|
// Civilization (saved v20). Per-cell settlement index (-1 = none; rebuilt on load, not saved),
|
||||||
|
// a derived habitability field, and a separate RNG so placement never perturbs tectonics.
|
||||||
|
std::vector<int> sCellSettlement;
|
||||||
|
std::vector<double> sHabitability;
|
||||||
|
uint32_t sCivRng = 1;
|
||||||
|
|
||||||
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||||
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
||||||
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
||||||
|
|||||||
134
src/sim/PlanetCiv.cpp
Normal file
134
src/sim/PlanetCiv.cpp
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#include "Planet.hpp"
|
||||||
|
#include "NameGen.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
// --- Civilization Step 2: settlements & habitability -------------------------
|
||||||
|
// A per-cell habitability/food score, a one-time placement of settlement point-agents on the best
|
||||||
|
// (well-spaced) cells, and their population growth/decline on the Live World clock toward a
|
||||||
|
// food-driven carrying capacity. The settlement SET is fixed after placement (one-time), so the only
|
||||||
|
// mutable per-step state is `population` -- which the step-back snapshot restores. Placement uses a
|
||||||
|
// SEPARATE RNG (sCivRng) so it never perturbs the tectonic stream (mirrors placeVolcanoes).
|
||||||
|
|
||||||
|
SettleTier settleTierOf(double pop, double townPop, double cityPop) {
|
||||||
|
if (pop >= cityPop) return SettleTier::City;
|
||||||
|
if (pop >= townPop) return SettleTier::Town;
|
||||||
|
return SettleTier::Village;
|
||||||
|
}
|
||||||
|
const char* settleTierName(SettleTier t) {
|
||||||
|
switch (t) { case SettleTier::City: return "City"; case SettleTier::Town: return "Town";
|
||||||
|
case SettleTier::Village: return "Village"; }
|
||||||
|
return "Village";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derived per-cell habitability (0..1): a weighted blend of temperature comfort, water access and
|
||||||
|
// food (flora/fauna + ecoregion productivity), gated by freezing winters + high elevation. Not saved.
|
||||||
|
void Planet::computeHabitability() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if ((int)sTemp.size() != n || (int)sMoist.size() != n) computeClimate();
|
||||||
|
if ((int)sFloraDensity.size() != n) computeBiotaDensity();
|
||||||
|
sHabitability.assign(n, 0.0);
|
||||||
|
const double sea = cfg.seaLevel;
|
||||||
|
const double wW = std::clamp(cfg.civHabWaterWeight, 0.0, 1.0);
|
||||||
|
const double fW = std::clamp(cfg.civHabFoodWeight, 0.0, 1.0);
|
||||||
|
const double tW = std::max(0.0, 1.0 - wW - fW);
|
||||||
|
const double tOpt = cfg.civHabTempOpt;
|
||||||
|
const bool haveDisch = (int)sDischarge.size() == n, haveLake = (int)sLakeDepth.size() == n;
|
||||||
|
const bool haveWinter = (int)sTempWinter.size() == n, haveEco = (int)sCellEcoregion.size() == n;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) continue;
|
||||||
|
double mean = sTemp[i], winter = haveWinter ? sTempWinter[i] : mean;
|
||||||
|
double tComfort = std::clamp(1.0 - std::fabs(mean - tOpt) / 22.0, 0.0, 1.0);
|
||||||
|
double coldGate = std::clamp((winter + 25.0) / 20.0, 0.0, 1.0); // frozen winters (< -25C) -> 0
|
||||||
|
// Water access: moisture baseline, strong bonus for a river / lake / coast.
|
||||||
|
double water = std::clamp(0.25 + 0.6 * sMoist[i], 0.0, 1.0);
|
||||||
|
if (haveDisch && sDischarge[i] > cfg.riverThreshold) water = std::max(water, 0.92);
|
||||||
|
if (haveLake && sLakeDepth[i] > cfg.biomeLakeMinDepth) water = std::max(water, 0.85);
|
||||||
|
for (int j : cells[i].neighbors) if (cells[j].elevation <= sea) { water = std::max(water, 0.85); break; }
|
||||||
|
// Food: plant + animal productivity + the cell's ecoregion productivity.
|
||||||
|
double eco = 0.0;
|
||||||
|
if (haveEco && sCellEcoregion[i] >= 0 && sCellEcoregion[i] < (int)ecoRegions.size()) {
|
||||||
|
const Ecoregion& e = ecoRegions[sCellEcoregion[i]];
|
||||||
|
eco = std::max(e.floraProductivity, e.faunaProductivity);
|
||||||
|
}
|
||||||
|
double food = std::clamp(0.5 * sFloraDensity[i] + 0.3 * sFaunaDensity[i] + 0.2 * eco, 0.0, 1.0);
|
||||||
|
double elevF = 1.0;
|
||||||
|
if (cells[i].elevation > cfg.civHabElevPenalty)
|
||||||
|
elevF = std::clamp(1.0 - (cells[i].elevation - cfg.civHabElevPenalty) / 3000.0, 0.0, 1.0);
|
||||||
|
double hab = (wW * water + fW * food + tW * tComfort) * coldGate * elevF;
|
||||||
|
sHabitability[i] = std::clamp(hab, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-time placement: greedily seed the highest-habitability cells with a minimum angular spacing
|
||||||
|
// (the ocean-basin seeding idiom). Auto-builds geography/ecoregions first (names + productivity).
|
||||||
|
void Planet::placeSettlements() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if (!geographyBuilt()) generateGeography();
|
||||||
|
if (!ecoregionsBuilt()) generateEcoregions();
|
||||||
|
computeHabitability();
|
||||||
|
settlements.clear();
|
||||||
|
sCellSettlement.assign(n, -1);
|
||||||
|
sCivRng = cfg.seed ? (cfg.seed ^ 0x017B1A2Eu) : 0x017B1A2Eu;
|
||||||
|
auto next = [&]() { sCivRng ^= sCivRng << 13; sCivRng ^= sCivRng >> 17; sCivRng ^= sCivRng << 5; return sCivRng; };
|
||||||
|
|
||||||
|
std::vector<int> cand;
|
||||||
|
for (int i = 0; i < n; ++i) if (sHabitability[i] >= cfg.civMinHabitability) cand.push_back(i);
|
||||||
|
std::sort(cand.begin(), cand.end(), [&](int a, int b) { return sHabitability[a] > sHabitability[b]; });
|
||||||
|
|
||||||
|
const double sepCos = std::cos(std::max(0.01, cfg.civMinSpacingRadians));
|
||||||
|
const int cap = std::max(0, cfg.civMaxSettlements);
|
||||||
|
std::set<std::string> usedNames;
|
||||||
|
std::vector<int> chosen;
|
||||||
|
for (int i : cand) {
|
||||||
|
if ((int)settlements.size() >= cap) break;
|
||||||
|
bool ok = true;
|
||||||
|
for (int c : chosen) if (cells[i].unit.dot(cells[c].unit) > sepCos) { ok = false; break; }
|
||||||
|
if (!ok) continue;
|
||||||
|
chosen.push_back(i);
|
||||||
|
int regId = ((int)sCellLand.size() == n) ? sCellLand[i] : -1;
|
||||||
|
int bank = (regId >= 0) ? namegen::bankForRegion(cfg.seed, regId)
|
||||||
|
: namegen::bankForRegion(cfg.seed, 2000 + i);
|
||||||
|
uint32_t nameSeed = next() ^ (uint32_t)(i * 2654435761u);
|
||||||
|
std::string nm = namegen::makeName(nameSeed, bank);
|
||||||
|
for (int g = 0; usedNames.count(nm) && g < 128; ++g) nm = namegen::makeName(nameSeed += 0x9E3779B9u, bank);
|
||||||
|
usedNames.insert(nm);
|
||||||
|
Settlement st;
|
||||||
|
st.cell = i; st.bank = bank; st.regionId = regId;
|
||||||
|
st.population = cfg.civSeedPopulation; st.name = nm;
|
||||||
|
settlements.push_back(std::move(st));
|
||||||
|
}
|
||||||
|
for (int k = 0; k < (int)settlements.size(); ++k) {
|
||||||
|
settlements[k].id = (uint32_t)(k + 1);
|
||||||
|
sCellSettlement[settlements[k].cell] = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One live-frame civilization step: each settlement's population moves logistically toward its
|
||||||
|
// food-driven carrying capacity K = civMaxPopulation * habitability (cut transiently where an active
|
||||||
|
// volcano ashes the area). Grows below K, declines above it; floored at 1 so an abandoned site can
|
||||||
|
// revive if K recovers. Pure of any string churn; the set never changes here.
|
||||||
|
CivUpdate Planet::stepCivilization(double dtHours) {
|
||||||
|
CivUpdate up;
|
||||||
|
if (settlements.empty() || dtHours <= 0.0) return up;
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if ((int)sHabitability.size() != n) computeHabitability();
|
||||||
|
const double yearHours = std::max(1.0, cfg.dayLengthHours * cfg.yearLengthDays);
|
||||||
|
const double dtYears = dtHours / yearHours;
|
||||||
|
for (Settlement& st : settlements) {
|
||||||
|
if (st.cell < 0 || st.cell >= n) continue;
|
||||||
|
double K = cfg.civMaxPopulation * sHabitability[st.cell];
|
||||||
|
for (const Volcano& v : volcanoes) { // active ash plume nearby cuts carrying capacity
|
||||||
|
if (v.ashTimer <= 0.0 || v.cell < 0 || v.cell >= n) continue;
|
||||||
|
double ang = std::acos(std::clamp(cells[st.cell].unit.dot(cells[v.cell].unit), -1.0, 1.0));
|
||||||
|
if (ang < cfg.volcanoBlastRadius * 1.5) { K *= 0.3; break; }
|
||||||
|
}
|
||||||
|
double P = st.population;
|
||||||
|
P += cfg.civGrowthRate * P * (1.0 - P / std::max(1.0, K)) * dtYears; // logistic (declines when K<P)
|
||||||
|
P = std::max(1.0, P); // keep a seed so an abandoned site can revive
|
||||||
|
if (std::fabs(P - st.population) > std::max(1.0, st.population * 0.0005)) up.recolor = true;
|
||||||
|
st.population = P;
|
||||||
|
}
|
||||||
|
return up;
|
||||||
|
}
|
||||||
28
src/sim/PlanetCiv.hpp
Normal file
28
src/sim/PlanetCiv.hpp
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Civilization stage, Step 2: settlements + habitability. A Settlement is a point agent fixed to one
|
||||||
|
// cell (the cell is the territory unit, ~223 km). Placed once on a settled world ("dawn of
|
||||||
|
// civilization"), then its population grows toward a food-driven carrying capacity on the Live World
|
||||||
|
// clock and shrinks / is abandoned when food drops. Raylib-free + deterministic (separate RNG). The
|
||||||
|
// set is fixed after placement, so the only mutable per-step state is `population`. Saved (v20).
|
||||||
|
|
||||||
|
enum class SettleTier : uint8_t { Village, Town, City };
|
||||||
|
|
||||||
|
struct Settlement {
|
||||||
|
uint32_t id = 0;
|
||||||
|
int cell = -1; // the grid cell it sits on (fixed geometry)
|
||||||
|
int bank = 0; // NameGen "language" bank (its continent's), kept for later culture use
|
||||||
|
int regionId = -1; // containing continent/island geography-feature index (-1 = none)
|
||||||
|
double population = 0.0; // the only mutable field (grows/declines each live step)
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
|
||||||
|
// What stepCivilization() changed this call, telling the viewer how much to rebuild + whether to log.
|
||||||
|
struct CivUpdate { bool recolor = false; bool changed = false; };
|
||||||
|
|
||||||
|
// Tier from population + the config thresholds (village < townPop <= town < cityPop <= city).
|
||||||
|
SettleTier settleTierOf(double population, double townPop, double cityPop);
|
||||||
|
const char* settleTierName(SettleTier t); // "Village" / "Town" / "City"
|
||||||
208
src/sim/PlanetEcoregions.cpp
Normal file
208
src/sim/PlanetEcoregions.cpp
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
#include "Planet.hpp"
|
||||||
|
#include "NameGen.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// --- Ecoregions --------------------------------------------------------------
|
||||||
|
// Named ecological provinces: connected areas with compatible biome, land/water
|
||||||
|
// context and productivity. This is an atlas layer, not a live ecological sim.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
enum EcoContext { EcoOcean = 0, EcoLand = 1, EcoWet = 2 };
|
||||||
|
|
||||||
|
uint32_t ecoHash(uint32_t seed, int biome, int context, int anchor) {
|
||||||
|
uint32_t h = (seed ^ 0xEC011FEu) + (uint32_t)biome * 0x85EBCA6Bu
|
||||||
|
+ (uint32_t)context * 0xC2B2AE35u;
|
||||||
|
h ^= (uint32_t)(anchor * 2654435761u + 0x165667B1u);
|
||||||
|
h ^= h >> 15; h *= 0x2545F491u; h ^= h >> 13;
|
||||||
|
return h ? h : 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ecoSuffix(Biome b, int context, double temp, double moist,
|
||||||
|
int domFlora, int domFauna) {
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
auto archName = [&](int idx) -> std::string {
|
||||||
|
return (idx >= 0 && idx < (int)AR.size()) ? std::string(AR[idx].name) : std::string();
|
||||||
|
};
|
||||||
|
if (context == EcoOcean) {
|
||||||
|
std::string fl = archName(domFlora), fa = archName(domFauna);
|
||||||
|
if (fl == "Kelp") return "Kelp Shelf";
|
||||||
|
if (fa == "Reef fish" || (temp > 18.0 && moist > 0.35)) return "Reef";
|
||||||
|
if (fa == "Baleen whale") return "Whale Grounds";
|
||||||
|
return "Marine Province";
|
||||||
|
}
|
||||||
|
if (context == EcoWet || b == Biome::Lake || b == Biome::Wetland) return "Wetlands";
|
||||||
|
switch (b) {
|
||||||
|
case Biome::Beach: return "Coast";
|
||||||
|
case Biome::Grassland: return "Grasslands";
|
||||||
|
case Biome::Savanna: return "Savanna";
|
||||||
|
case Biome::Desert: return "Desert";
|
||||||
|
case Biome::Forest: return (temp > 18.0 && moist > 0.55) ? "Rainforest" : "Forest";
|
||||||
|
case Biome::Taiga: return "Taiga";
|
||||||
|
case Biome::Tundra: return "Tundra";
|
||||||
|
case Biome::Hills: return "Highlands";
|
||||||
|
case Biome::Mountains: return "Alpine Zone";
|
||||||
|
default: return "Ecoregion";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::generateEcoregions() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if (n == 0) return;
|
||||||
|
if ((int)sTemp.size() != n || (int)sMoist.size() != n) computeClimate();
|
||||||
|
classifyBiomes();
|
||||||
|
if ((int)sFloraDensity.size() != n || (int)sFaunaDensity.size() != n ||
|
||||||
|
(int)sFungaDensity.size() != n) computeBiotaDensity();
|
||||||
|
if ((int)sLakeDepth.size() != n) computeHydrology();
|
||||||
|
if (!geographyBuilt()) generateGeography();
|
||||||
|
|
||||||
|
ecoRegions.clear();
|
||||||
|
sCellEcoregion.assign(n, -1);
|
||||||
|
sEcoRng = cfg.seed ? (cfg.seed ^ 0xEC011FEu) : 0xEC011FEu;
|
||||||
|
|
||||||
|
auto contextOf = [&](int i) {
|
||||||
|
if (cells[i].elevation <= cfg.seaLevel) return EcoOcean;
|
||||||
|
if (cells[i].biome == Biome::Wetland || cells[i].biome == Biome::Lake ||
|
||||||
|
(!sLakeDepth.empty() && sLakeDepth[i] > cfg.biomeLakeMinDepth)) return EcoWet;
|
||||||
|
return EcoLand;
|
||||||
|
};
|
||||||
|
auto productivity = [&](int i) {
|
||||||
|
return std::max({ sFloraDensity[i], sFaunaDensity[i], sFungaDensity[i] });
|
||||||
|
};
|
||||||
|
auto bandOf = [&](int i) {
|
||||||
|
return std::clamp((int)std::floor(productivity(i) * 4.0), 0, 3);
|
||||||
|
};
|
||||||
|
auto sameKey = [&](int a, int b) {
|
||||||
|
if (cells[a].biome == Biome::Ice || cells[b].biome == Biome::Ice) return false;
|
||||||
|
return cells[a].biome == cells[b].biome && contextOf(a) == contextOf(b)
|
||||||
|
&& std::abs(bandOf(a) - bandOf(b)) <= 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::vector<int>> comps;
|
||||||
|
std::vector<char> seen(n, 0);
|
||||||
|
std::vector<int> stack;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (seen[i] || cells[i].biome == Biome::Ice) continue;
|
||||||
|
std::vector<int> comp;
|
||||||
|
stack.clear(); stack.push_back(i); seen[i] = 1;
|
||||||
|
while (!stack.empty()) {
|
||||||
|
int u = stack.back(); stack.pop_back(); comp.push_back(u);
|
||||||
|
for (int v : cells[u].neighbors)
|
||||||
|
if (!seen[v] && sameKey(u, v)) { seen[v] = 1; stack.push_back(v); }
|
||||||
|
}
|
||||||
|
comps.push_back(std::move(comp));
|
||||||
|
}
|
||||||
|
std::sort(comps.begin(), comps.end(), [](const auto& a, const auto& b) { return a.size() > b.size(); });
|
||||||
|
|
||||||
|
auto centroidCell = [&](const std::vector<int>& comp) {
|
||||||
|
Vec3 c{0, 0, 0};
|
||||||
|
for (int i : comp) c = c + cells[i].unit;
|
||||||
|
if (c.length() < 1e-9) return comp.front();
|
||||||
|
c = c.normalized();
|
||||||
|
int best = comp.front(); double bd = -2.0;
|
||||||
|
for (int i : comp) { double d = cells[i].unit.dot(c); if (d > bd) { bd = d; best = i; } }
|
||||||
|
return best;
|
||||||
|
};
|
||||||
|
auto dominant = [&](const std::vector<int>& comp, BiotaKind kind) {
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
std::vector<double> score(AR.size(), 0.0);
|
||||||
|
if (sHasBiota && sBiota.size() == cells.size()) {
|
||||||
|
for (int i : comp) {
|
||||||
|
const std::vector<Organism>* list = nullptr;
|
||||||
|
if (kind == BiotaKind::Flora) list = &sBiota[i].flora;
|
||||||
|
else if (kind == BiotaKind::Fauna) list = &sBiota[i].fauna;
|
||||||
|
else list = &sBiota[i].funga;
|
||||||
|
for (const Organism& o : *list)
|
||||||
|
if (o.archetype < AR.size()) score[o.archetype] += pointCost(AR[o.archetype].size);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i : comp) {
|
||||||
|
double dens = (kind == BiotaKind::Flora) ? sFloraDensity[i]
|
||||||
|
: (kind == BiotaKind::Fauna) ? sFaunaDensity[i] : sFungaDensity[i];
|
||||||
|
if (dens <= 0.0) continue;
|
||||||
|
for (size_t a = 0; a < AR.size(); ++a) {
|
||||||
|
if (AR[a].kind != kind) continue;
|
||||||
|
score[a] += dens * biotaSuitability(AR[a], cells[i].biome, sTemp[i], sMoist[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int best = -1; double bs = 0.0;
|
||||||
|
for (size_t a = 0; a < score.size(); ++a)
|
||||||
|
if (score[a] > bs) { bs = score[a]; best = (int)a; }
|
||||||
|
return best;
|
||||||
|
};
|
||||||
|
auto regionOf = [&](int anchor, int context) {
|
||||||
|
const std::vector<int>& arr = (context == EcoOcean) ? sCellWater : sCellLand;
|
||||||
|
if (anchor >= 0 && anchor < (int)arr.size()) return arr[anchor];
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
auto updateProductivity = [&](Ecoregion& e, const std::vector<int>& comp, int oldSize) {
|
||||||
|
double fl = 0.0, fa = 0.0, fu = 0.0;
|
||||||
|
for (int i : comp) { fl += sFloraDensity[i]; fa += sFaunaDensity[i]; fu += sFungaDensity[i]; }
|
||||||
|
int add = (int)comp.size();
|
||||||
|
int total = oldSize + add;
|
||||||
|
if (total <= 0) return;
|
||||||
|
e.floraProductivity = (e.floraProductivity * oldSize + fl) / total;
|
||||||
|
e.faunaProductivity = (e.faunaProductivity * oldSize + fa) / total;
|
||||||
|
e.fungaProductivity = (e.fungaProductivity * oldSize + fu) / total;
|
||||||
|
e.size = total;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::set<std::string> usedNames;
|
||||||
|
auto makeName = [&](Biome b, int context, int anchor, int regId, int domFlora, int domFauna,
|
||||||
|
double temp, double moist) {
|
||||||
|
int bank = namegen::bankForRegion(cfg.seed ^ 0xEC011FEu, regId >= 0 ? regId : 3000 + anchor);
|
||||||
|
uint32_t seed = ecoHash(cfg.seed, (int)b, context, anchor);
|
||||||
|
std::string suffix = ecoSuffix(b, context, temp, moist, domFlora, domFauna);
|
||||||
|
std::string root = namegen::makeName(seed, bank);
|
||||||
|
std::string out = root + " " + suffix;
|
||||||
|
for (int guard = 0; usedNames.count(out) && guard < 128; ++guard) {
|
||||||
|
seed += 0x9E3779B9u;
|
||||||
|
root = namegen::makeName(seed, bank);
|
||||||
|
out = root + " " + suffix;
|
||||||
|
}
|
||||||
|
usedNames.insert(out);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const int tinyMax = 3;
|
||||||
|
for (const auto& comp : comps) {
|
||||||
|
int anchor = centroidCell(comp);
|
||||||
|
int context = contextOf(anchor);
|
||||||
|
if ((int)comp.size() <= tinyMax) {
|
||||||
|
int merge = -1;
|
||||||
|
for (int c : comp) {
|
||||||
|
for (int nb : cells[c].neighbors) {
|
||||||
|
int ei = (nb >= 0 && nb < (int)sCellEcoregion.size()) ? sCellEcoregion[nb] : -1;
|
||||||
|
if (ei >= 0 && ecoRegions[ei].biome == cells[anchor].biome) { merge = ei; break; }
|
||||||
|
}
|
||||||
|
if (merge >= 0) break;
|
||||||
|
}
|
||||||
|
if (merge >= 0) {
|
||||||
|
int oldSize = ecoRegions[merge].size;
|
||||||
|
updateProductivity(ecoRegions[merge], comp, oldSize);
|
||||||
|
for (int c : comp) sCellEcoregion[c] = merge;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ecoregion e;
|
||||||
|
e.id = (uint32_t)ecoRegions.size() + 1;
|
||||||
|
e.biome = cells[anchor].biome;
|
||||||
|
e.anchorCell = anchor;
|
||||||
|
e.regionId = regionOf(anchor, context);
|
||||||
|
e.dominantFlora = dominant(comp, BiotaKind::Flora);
|
||||||
|
e.dominantFauna = dominant(comp, BiotaKind::Fauna);
|
||||||
|
e.dominantFunga = (context == EcoOcean) ? -1 : dominant(comp, BiotaKind::Funga);
|
||||||
|
updateProductivity(e, comp, 0);
|
||||||
|
e.name = makeName(e.biome, context, anchor, e.regionId, e.dominantFlora, e.dominantFauna,
|
||||||
|
sTemp[anchor], sMoist[anchor]);
|
||||||
|
int ei = (int)ecoRegions.size();
|
||||||
|
ecoRegions.push_back(std::move(e));
|
||||||
|
for (int c : comp) sCellEcoregion[c] = ei;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/sim/PlanetEcoregions.hpp
Normal file
22
src/sim/PlanetEcoregions.hpp
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "PlanetTypes.hpp" // Biome
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Ecoregions are named ecological provinces: connected cells with compatible biome,
|
||||||
|
// water/land context and productivity. They summarize existing broad biota; they do
|
||||||
|
// not create new species.
|
||||||
|
struct Ecoregion {
|
||||||
|
uint32_t id = 0;
|
||||||
|
std::string name;
|
||||||
|
Biome biome = Biome::Ocean;
|
||||||
|
int anchorCell = -1;
|
||||||
|
int regionId = -1; // containing geography feature index, if known
|
||||||
|
int size = 0; // member-cell count
|
||||||
|
int dominantFlora = -1; // biotaArchetypes() index, or -1
|
||||||
|
int dominantFauna = -1;
|
||||||
|
int dominantFunga = -1;
|
||||||
|
double floraProductivity = 0.0;
|
||||||
|
double faunaProductivity = 0.0;
|
||||||
|
double fungaProductivity = 0.0;
|
||||||
|
};
|
||||||
@ -49,12 +49,15 @@
|
|||||||
D(volcanoBlastRadius) D(volcanoBlastCloud) D(volcanoAshMinYears) D(volcanoAshMaxYears) \
|
D(volcanoBlastRadius) D(volcanoBlastCloud) D(volcanoAshMinYears) D(volcanoAshMaxYears) \
|
||||||
D(volcanoAshPuffCellsPerWeek) D(volcanoAshCloud) D(volcanoAshCooling) \
|
D(volcanoAshPuffCellsPerWeek) D(volcanoAshCloud) D(volcanoAshCooling) \
|
||||||
D(geoMountainElev) D(geoRiverMinDischarge) D(geoOceanSepRadians) \
|
D(geoMountainElev) D(geoRiverMinDischarge) D(geoOceanSepRadians) \
|
||||||
|
D(civMinSpacingRadians) D(civMinHabitability) D(civSeedPopulation) D(civGrowthRate) \
|
||||||
|
D(civMaxPopulation) D(civTownPop) D(civCityPop) D(civAbandonPop) \
|
||||||
|
D(civHabWaterWeight) D(civHabFoodWeight) D(civHabTempOpt) D(civHabElevPenalty) \
|
||||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
||||||
I(volcanoMaxCount) \
|
I(volcanoMaxCount) \
|
||||||
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
||||||
I(geoOceanDeep) \
|
I(geoOceanDeep) I(civMaxSettlements) \
|
||||||
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||||
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
|
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
|
||||||
U(seed)
|
U(seed)
|
||||||
@ -248,6 +251,18 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.geoMountainElev, 0.0, 12000.0, "geoMountainElev"));
|
E(rng(cfg.geoMountainElev, 0.0, 12000.0, "geoMountainElev"));
|
||||||
E(rng(cfg.geoRiverMinDischarge, 0.0, 1.0e9, "geoRiverMinDischarge"));
|
E(rng(cfg.geoRiverMinDischarge, 0.0, 1.0e9, "geoRiverMinDischarge"));
|
||||||
E(rng(cfg.geoOceanSepRadians, 0.05, 3.14159, "geoOceanSepRadians"));
|
E(rng(cfg.geoOceanSepRadians, 0.05, 3.14159, "geoOceanSepRadians"));
|
||||||
|
E(rng(cfg.civMinSpacingRadians, 0.001, 3.14159, "civMinSpacingRadians"));
|
||||||
|
E(rng(cfg.civMinHabitability, 0.0, 1.0, "civMinHabitability"));
|
||||||
|
E(rng(cfg.civSeedPopulation, 1.0, 1.0e9, "civSeedPopulation"));
|
||||||
|
E(rng(cfg.civGrowthRate, 0.0, 100.0, "civGrowthRate"));
|
||||||
|
E(rng(cfg.civMaxPopulation, 1.0, 1.0e12, "civMaxPopulation"));
|
||||||
|
E(rng(cfg.civTownPop, 1.0, 1.0e12, "civTownPop"));
|
||||||
|
E(rng(cfg.civCityPop, 1.0, 1.0e12, "civCityPop"));
|
||||||
|
E(rng(cfg.civAbandonPop, 0.0, 1.0e9, "civAbandonPop"));
|
||||||
|
E(rng(cfg.civHabWaterWeight, 0.0, 1.0, "civHabWaterWeight"));
|
||||||
|
E(rng(cfg.civHabFoodWeight, 0.0, 1.0, "civHabFoodWeight"));
|
||||||
|
E(rng(cfg.civHabTempOpt, -20.0, 50.0, "civHabTempOpt"));
|
||||||
|
E(rng(cfg.civHabElevPenalty, 0.0, 12000.0, "civHabElevPenalty"));
|
||||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||||
@ -268,6 +283,7 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(irng(cfg.geoMaxRivers, 0, 100000, "geoMaxRivers"));
|
E(irng(cfg.geoMaxRivers, 0, 100000, "geoMaxRivers"));
|
||||||
E(irng(cfg.geoMaxPeaks, 0, 100000, "geoMaxPeaks"));
|
E(irng(cfg.geoMaxPeaks, 0, 100000, "geoMaxPeaks"));
|
||||||
E(irng(cfg.geoOceanDeep, 1, 1000, "geoOceanDeep"));
|
E(irng(cfg.geoOceanDeep, 1, 1000, "geoOceanDeep"));
|
||||||
|
E(irng(cfg.civMaxSettlements, 0, 1000000, "civMaxSettlements"));
|
||||||
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
||||||
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
||||||
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
||||||
@ -393,17 +409,40 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
writeVec(os, sCellLand); writeVec(os, sCellWater);
|
writeVec(os, sCellLand); writeVec(os, sCellWater);
|
||||||
writeVec(os, sCellRange); writeVec(os, sCellRiver);
|
writeVec(os, sCellRange); writeVec(os, sCellRiver);
|
||||||
writePod(os, sGeoSalt);
|
writePod(os, sGeoSalt);
|
||||||
|
// v19: named ecoregions + per-cell ecoregion index.
|
||||||
|
uint64_t ne = ecoRegions.size(); writePod(os, ne);
|
||||||
|
for (const Ecoregion& e : ecoRegions) {
|
||||||
|
writePod(os, e.id);
|
||||||
|
uint8_t b = (uint8_t)e.biome; writePod(os, b);
|
||||||
|
writePod(os, e.anchorCell); writePod(os, e.regionId); writePod(os, e.size);
|
||||||
|
writePod(os, e.dominantFlora); writePod(os, e.dominantFauna); writePod(os, e.dominantFunga);
|
||||||
|
writePod(os, e.floraProductivity); writePod(os, e.faunaProductivity); writePod(os, e.fungaProductivity);
|
||||||
|
uint64_t L = e.name.size(); writePod(os, L);
|
||||||
|
if (L) os.write(e.name.data(), (std::streamsize)L);
|
||||||
|
}
|
||||||
|
writeVec(os, sCellEcoregion);
|
||||||
|
// v20: civilization settlements (placed once, then grow/decline). sCellSettlement is rebuilt on
|
||||||
|
// load, so only the settlement records (id/cell/bank/regionId/population/name) are written.
|
||||||
|
uint64_t ns = settlements.size(); writePod(os, ns);
|
||||||
|
for (const Settlement& st : settlements) {
|
||||||
|
writePod(os, st.id); writePod(os, st.cell); writePod(os, st.bank); writePod(os, st.regionId);
|
||||||
|
writePod(os, st.population);
|
||||||
|
uint64_t L = st.name.size(); writePod(os, L);
|
||||||
|
if (L) os.write(st.name.data(), (std::streamsize)L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
||||||
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
||||||
bool hasGeography, bool hasGeoSalt) {
|
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements) {
|
||||||
// Save blocks are append-only by version. If a caller asks for an older prefix,
|
// Save blocks are append-only by version. If a caller asks for an older prefix,
|
||||||
// later blocks cannot exist in that stream even if the default arguments say otherwise.
|
// later blocks cannot exist in that stream even if the default arguments say otherwise.
|
||||||
if (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; hasVolcanoes = false; }
|
if (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; hasVolcanoes = false; }
|
||||||
if (!hasWeather) { hasStorms = false; hasVolcanoes = false; } // volcano block follows the weather block
|
if (!hasWeather) { hasStorms = false; hasVolcanoes = false; } // volcano block follows the weather block
|
||||||
if (!hasVolcanoes || !hasStatefulVolcanoes) hasGeography = false; // geography block follows the volcano block
|
if (!hasVolcanoes || !hasStatefulVolcanoes) hasGeography = false; // geography block follows the volcano block
|
||||||
if (!hasGeography) hasGeoSalt = false; // salt follows the geography block
|
if (!hasGeography) hasGeoSalt = false; // salt follows the geography block
|
||||||
|
if (!hasGeoSalt) hasEcoregions = false; // ecoregions follow the v18 salt
|
||||||
|
if (!hasEcoregions) hasSettlements = false; // settlements follow the ecoregion block
|
||||||
|
|
||||||
// Read the length-prefixed key=value config block (see writeState). A default
|
// Read the length-prefixed key=value config block (see writeState). A default
|
||||||
// PlanetConfig is parsed over, so fields absent from an older save keep their
|
// PlanetConfig is parsed over, so fields absent from an older save keep their
|
||||||
@ -570,6 +609,54 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
|||||||
sGeoRng = sGeoSalt ? sGeoSalt : (cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u);
|
sGeoRng = sGeoSalt ? sGeoSalt : (cfg.seed ? (cfg.seed ^ 0x6E0C12A7u) : 0x6E0C12A7u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// v19: named ecoregions. Older saves load with none (regenerate on demand).
|
||||||
|
ecoRegions.clear(); sEcoRng = cfg.seed ? (cfg.seed ^ 0xEC011FEu) : 0xEC011FEu;
|
||||||
|
sCellEcoregion.assign(cells.size(), -1);
|
||||||
|
if (hasEcoregions) {
|
||||||
|
uint64_t ne = 0; readPod(is, ne);
|
||||||
|
if (!is || ne > 200000) return false;
|
||||||
|
ecoRegions.resize((size_t)ne);
|
||||||
|
for (Ecoregion& e : ecoRegions) {
|
||||||
|
readPod(is, e.id);
|
||||||
|
uint8_t b = 0; readPod(is, b);
|
||||||
|
if (!validBiomeByte(b)) return false;
|
||||||
|
e.biome = (Biome)b;
|
||||||
|
readPod(is, e.anchorCell); readPod(is, e.regionId); readPod(is, e.size);
|
||||||
|
readPod(is, e.dominantFlora); readPod(is, e.dominantFauna); readPod(is, e.dominantFunga);
|
||||||
|
readPod(is, e.floraProductivity); readPod(is, e.faunaProductivity); readPod(is, e.fungaProductivity);
|
||||||
|
uint64_t L = 0; readPod(is, L);
|
||||||
|
if (!is || L > 256) return false;
|
||||||
|
e.name.resize((size_t)L);
|
||||||
|
if (L) is.read(&e.name[0], (std::streamsize)L);
|
||||||
|
if (!is || e.anchorCell < 0 || e.anchorCell >= (int)cells.size() || e.size < 0) return false;
|
||||||
|
auto validArch = [](int a) { return a == -1 || (a >= 0 && a < (int)biotaArchetypes().size()); };
|
||||||
|
if (!validArch(e.dominantFlora) || !validArch(e.dominantFauna) || !validArch(e.dominantFunga)) return false;
|
||||||
|
if (!std::isfinite(e.floraProductivity) || !std::isfinite(e.faunaProductivity)
|
||||||
|
|| !std::isfinite(e.fungaProductivity)) return false;
|
||||||
|
}
|
||||||
|
if (!readVec(is, sCellEcoregion, cells.size())) return false;
|
||||||
|
if (sCellEcoregion.empty()) sCellEcoregion.assign(cells.size(), -1);
|
||||||
|
if (sCellEcoregion.size() != cells.size()) return false;
|
||||||
|
for (int v : sCellEcoregion) if (v < -1 || v >= (int)ecoRegions.size()) return false;
|
||||||
|
}
|
||||||
|
// v20: civilization settlements. buildGeometry() already cleared them; older saves stay empty
|
||||||
|
// (re-seeded on demand). sCellSettlement is rebuilt from the records (not stored).
|
||||||
|
if (hasSettlements) {
|
||||||
|
uint64_t ns = 0; readPod(is, ns);
|
||||||
|
if (!is || ns > 1000000) return false;
|
||||||
|
settlements.resize((size_t)ns);
|
||||||
|
for (Settlement& st : settlements) {
|
||||||
|
readPod(is, st.id); readPod(is, st.cell); readPod(is, st.bank); readPod(is, st.regionId);
|
||||||
|
readPod(is, st.population);
|
||||||
|
uint64_t L = 0; readPod(is, L);
|
||||||
|
if (!is || L > 256) return false;
|
||||||
|
st.name.resize((size_t)L);
|
||||||
|
if (L) is.read(&st.name[0], (std::streamsize)L);
|
||||||
|
if (!is || st.cell < 0 || st.cell >= (int)cells.size() || !std::isfinite(st.population)) return false;
|
||||||
|
}
|
||||||
|
sCellSettlement.assign(cells.size(), -1);
|
||||||
|
for (int k = 0; k < (int)settlements.size(); ++k) sCellSettlement[settlements[k].cell] = k;
|
||||||
|
}
|
||||||
computeBiotaDensity(); // derived density scalars for the colour views
|
computeBiotaDensity(); // derived density scalars for the colour views
|
||||||
return (bool)is;
|
return (bool)is;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,9 @@ struct WeatherSnapshot {
|
|||||||
std::vector<Volcano> volcanoes;
|
std::vector<Volcano> volcanoes;
|
||||||
uint32_t rng = 0, nextId = 0;
|
uint32_t rng = 0, nextId = 0;
|
||||||
uint32_t volRng = 0;
|
uint32_t volRng = 0;
|
||||||
|
// Civilization: settlement populations (the only mutable per-step civ state, since the set is
|
||||||
|
// fixed after placement). Restored on a step back so towns rewind/replay with the clock.
|
||||||
|
std::vector<double> settlementPop;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Plate {
|
struct Plate {
|
||||||
@ -374,4 +377,22 @@ struct PlanetConfig {
|
|||||||
double geoRiverMinDischarge = 80.0; // min mouth discharge for a named river
|
double geoRiverMinDischarge = 80.0; // min mouth discharge for a named river
|
||||||
int geoMaxRivers = 40; // cap on named rivers (largest by discharge)
|
int geoMaxRivers = 40; // cap on named rivers (largest by discharge)
|
||||||
int geoMaxPeaks = 40; // cap on named peaks (highest)
|
int geoMaxPeaks = 40; // cap on named peaks (highest)
|
||||||
|
|
||||||
|
// --- Civilization: settlements & habitability -- see PlanetCiv.cpp ----------
|
||||||
|
// Placed once on a settled world ("dawn of civilization"); population then grows/declines on the
|
||||||
|
// Live World clock toward a food-driven carrying capacity. Habitability blends climate comfort,
|
||||||
|
// water access and food (flora/fauna + ecoregion productivity).
|
||||||
|
int civMaxSettlements = 80; // cap on settlement sites
|
||||||
|
double civMinSpacingRadians = 0.10; // min angular separation between settlement sites (~640 km)
|
||||||
|
double civMinHabitability = 0.22; // don't place a settlement below this habitability
|
||||||
|
double civSeedPopulation = 250.0; // initial village population at placement
|
||||||
|
double civGrowthRate = 0.02; // logistic growth rate per year (toward carrying capacity)
|
||||||
|
double civMaxPopulation = 2.0e6; // population at habitability 1 (carrying-capacity scale)
|
||||||
|
double civTownPop = 5000.0; // population at/above which a settlement is a Town
|
||||||
|
double civCityPop = 100000.0;// population at/above which a settlement is a City
|
||||||
|
double civAbandonPop = 50.0; // below this a settlement is abandoned (dormant; can revive)
|
||||||
|
double civHabWaterWeight = 0.45; // habitability weight of water access (rivers/lakes/coast)
|
||||||
|
double civHabFoodWeight = 0.40; // habitability weight of food (flora/fauna + ecoregion)
|
||||||
|
double civHabTempOpt = 18.0; // C: most comfortable annual-mean temperature
|
||||||
|
double civHabElevPenalty = 2500.0; // m above which high terrain steeply reduces habitability
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,6 +31,8 @@ WeatherSnapshot Planet::captureWeather() const {
|
|||||||
s.humidity = sHumidity; s.cloud = sCloud; s.rain = sRain;
|
s.humidity = sHumidity; s.cloud = sCloud; s.rain = sRain;
|
||||||
s.storms = sStorms; s.rng = sWeatherRng; s.nextId = sStormNextId;
|
s.storms = sStorms; s.rng = sWeatherRng; s.nextId = sStormNextId;
|
||||||
s.volcanoes = volcanoes; s.volRng = sVolRng;
|
s.volcanoes = volcanoes; s.volRng = sVolRng;
|
||||||
|
s.settlementPop.reserve(settlements.size()); // civ: only population is mutable
|
||||||
|
for (const Settlement& st : settlements) s.settlementPop.push_back(st.population);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +40,8 @@ void Planet::restoreWeather(const WeatherSnapshot& s) {
|
|||||||
sHumidity = s.humidity; sCloud = s.cloud; sRain = s.rain;
|
sHumidity = s.humidity; sCloud = s.cloud; sRain = s.rain;
|
||||||
sStorms = s.storms; sWeatherRng = s.rng; sStormNextId = s.nextId;
|
sStorms = s.storms; sWeatherRng = s.rng; sStormNextId = s.nextId;
|
||||||
volcanoes = s.volcanoes; sVolRng = s.volRng;
|
volcanoes = s.volcanoes; sVolRng = s.volRng;
|
||||||
|
if (s.settlementPop.size() == settlements.size()) // restore populations (set is fixed)
|
||||||
|
for (size_t k = 0; k < settlements.size(); ++k) settlements[k].population = s.settlementPop[k];
|
||||||
sHasWeather = !sHumidity.empty();
|
sHasWeather = !sHumidity.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
151
test_civ.cpp
Normal file
151
test_civ.cpp
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// Headless test for civilization Step 2 (habitability + settlements). No display needed.
|
||||||
|
//
|
||||||
|
// g++ -std=c++17 -O2 -Isrc/sim test_civ.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \
|
||||||
|
// src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp src/sim/PlanetErosion.cpp \
|
||||||
|
// src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||||
|
// src/sim/PlanetLive.cpp src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp \
|
||||||
|
// src/sim/PlanetVolcano.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||||
|
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/NameGen.cpp \
|
||||||
|
// src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp \
|
||||||
|
// src/sim/PlanetIO.cpp -o /tmp/tc && /tmp/tc
|
||||||
|
//
|
||||||
|
// Verifies: habitability range/zeros; placement spacing/cap/land + unique names; food-driven growth
|
||||||
|
// and decline; tiers; determinism + RNG isolation; population snapshot round-trip; v20 save; reseed clear.
|
||||||
|
|
||||||
|
#include "Planet.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
static void check(bool cond, const char* what) {
|
||||||
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||||
|
if (!cond) ++failures;
|
||||||
|
}
|
||||||
|
static void settle(Planet& p, int maxSteps = 800) {
|
||||||
|
int run = 0;
|
||||||
|
for (int s = 0; s < maxSteps; ++s) { double mc = p.step(); if (mc < 2.0) { if (++run >= 3) break; } else run = 0; }
|
||||||
|
p.computeClimate(); p.classifyBiomes();
|
||||||
|
}
|
||||||
|
static void drift(Planet& p, int iters) {
|
||||||
|
p.drifting = true;
|
||||||
|
for (int k = 0; k < iters; ++k) { double dt = p.cflDtMy(); p.advect(dt); p.step(); p.erode(dt); if (k >= iters/2) p.hydrology(dt*0.2); }
|
||||||
|
p.computeClimate(); p.classifyBiomes();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 4242;
|
||||||
|
Planet p; p.generate(cfg); settle(p); drift(p, 400);
|
||||||
|
const int n = (int)p.cells.size();
|
||||||
|
const double sea = p.cfg.seaLevel;
|
||||||
|
const double yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
||||||
|
|
||||||
|
std::printf("Civ: habitability field\n");
|
||||||
|
p.computeHabitability();
|
||||||
|
const auto& H = p.habitability();
|
||||||
|
check((int)H.size() == n, "habitability sized n");
|
||||||
|
bool ranged = true, zeroWaterIce = true, anyHabitable = false;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (!(std::isfinite(H[i]) && H[i] >= 0.0 && H[i] <= 1.0)) ranged = false;
|
||||||
|
if ((p.cells[i].elevation <= sea || p.cells[i].biome == Biome::Ice) && H[i] != 0.0) zeroWaterIce = false;
|
||||||
|
if (H[i] > 0.3) anyHabitable = true;
|
||||||
|
}
|
||||||
|
check(ranged, "habitability in [0,1]");
|
||||||
|
check(zeroWaterIce, "habitability 0 on ocean/ice");
|
||||||
|
check(anyHabitable, "some land is habitable");
|
||||||
|
|
||||||
|
std::printf("Civ: placement\n");
|
||||||
|
p.placeSettlements();
|
||||||
|
const auto& S = p.settlements;
|
||||||
|
check(!S.empty(), "settlements placed");
|
||||||
|
bool onLand = true, aboveMin = true, capOk = (int)S.size() <= p.cfg.civMaxSettlements;
|
||||||
|
std::set<std::string> names; bool uniqueNames = true;
|
||||||
|
const double sepCos = std::cos(p.cfg.civMinSpacingRadians);
|
||||||
|
bool spaced = true;
|
||||||
|
for (size_t a = 0; a < S.size(); ++a) {
|
||||||
|
if (p.cells[S[a].cell].elevation <= sea) onLand = false;
|
||||||
|
if (p.habitability()[S[a].cell] < p.cfg.civMinHabitability - 1e-9) aboveMin = false;
|
||||||
|
if (!names.insert(S[a].name).second || S[a].name.empty()) uniqueNames = false;
|
||||||
|
for (size_t b = a + 1; b < S.size(); ++b)
|
||||||
|
if (p.cells[S[a].cell].unit.dot(p.cells[S[b].cell].unit) > sepCos + 1e-9) spaced = false;
|
||||||
|
if (p.cellSettlement()[S[a].cell] != (int)a) onLand = false; // index consistency
|
||||||
|
}
|
||||||
|
std::printf(" %d settlements\n", (int)S.size());
|
||||||
|
check(onLand, "settlements sit on land + cellSettlement index is consistent");
|
||||||
|
check(aboveMin, "settlements only on cells >= civMinHabitability");
|
||||||
|
check(spaced, "settlements respect the minimum spacing");
|
||||||
|
check(capOk, "settlement count within the cap");
|
||||||
|
check(uniqueNames, "settlement names are unique + non-empty");
|
||||||
|
|
||||||
|
std::printf("Civ: food-driven growth + decline\n");
|
||||||
|
{
|
||||||
|
int gi = 0; for (size_t k = 0; k < S.size(); ++k) if (p.habitability()[S[k].cell] > p.habitability()[S[gi].cell]) gi = (int)k;
|
||||||
|
double p0 = p.settlements[gi].population;
|
||||||
|
for (int k = 0; k < 400; ++k) p.stepCivilization(5.0 * yearH); // ~2000 yr of small steps
|
||||||
|
check(p.settlements[gi].population > p0 * 2.0, "a high-habitability settlement grows");
|
||||||
|
// Decline: push one well over its carrying capacity, then step -> it shrinks.
|
||||||
|
p.settlements[gi].population = 5.0e7;
|
||||||
|
double over = p.settlements[gi].population;
|
||||||
|
for (int k = 0; k < 400; ++k) p.stepCivilization(5.0 * yearH);
|
||||||
|
check(p.settlements[gi].population < over, "an over-capacity settlement declines toward its food limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Civ: tiers\n");
|
||||||
|
check(settleTierOf(100.0, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::Village
|
||||||
|
&& settleTierOf(p.cfg.civTownPop, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::Town
|
||||||
|
&& settleTierOf(p.cfg.civCityPop, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::City,
|
||||||
|
"tier thresholds (village/town/city)");
|
||||||
|
|
||||||
|
std::printf("Civ: determinism\n");
|
||||||
|
Planet q; q.generate(cfg); settle(q); drift(q, 400); q.placeSettlements();
|
||||||
|
bool same = (q.settlements.size() == S.size());
|
||||||
|
if (same) for (size_t k = 0; k < S.size(); ++k)
|
||||||
|
if (q.settlements[k].cell != p.settlements[k].cell || q.settlements[k].name != p.settlements[k].name) { same = false; break; }
|
||||||
|
check(same, "placeSettlements is deterministic");
|
||||||
|
|
||||||
|
std::printf("Civ: RNG isolation from tectonics\n");
|
||||||
|
Planet x; x.generate(cfg); settle(x);
|
||||||
|
Planet y; y.generate(cfg); settle(y);
|
||||||
|
for (int k = 0; k < 40; ++k) {
|
||||||
|
double dx = x.cflDtMy(); x.advect(dx); x.step(); x.erode(dx);
|
||||||
|
double dy = y.cflDtMy(); y.advect(dy); y.step(); y.erode(dy);
|
||||||
|
if (k == 20) { y.placeSettlements(); y.stepCivilization(yearH); }
|
||||||
|
}
|
||||||
|
bool terrainSame = true;
|
||||||
|
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
|
||||||
|
check(terrainSame, "placeSettlements/stepCivilization never perturb tectonic evolution");
|
||||||
|
|
||||||
|
std::printf("Civ: population snapshot round-trip\n");
|
||||||
|
{
|
||||||
|
WeatherSnapshot snap = p.captureWeather();
|
||||||
|
for (auto& st : p.settlements) st.population = 12345.0;
|
||||||
|
p.restoreWeather(snap);
|
||||||
|
bool restored = true;
|
||||||
|
for (size_t k = 0; k < p.settlements.size(); ++k) if (std::fabs(p.settlements[k].population - snap.settlementPop[k]) > 1e-9) restored = false;
|
||||||
|
check(snap.settlementPop.size() == p.settlements.size() && restored, "captureWeather/restoreWeather round-trips populations");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Civ: save v20 round-trip\n");
|
||||||
|
{
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true);
|
||||||
|
check(ok, "readState accepts a v20 stream");
|
||||||
|
bool match = (r.settlements.size() == p.settlements.size());
|
||||||
|
if (match) for (size_t k = 0; k < p.settlements.size(); ++k)
|
||||||
|
if (r.settlements[k].cell != p.settlements[k].cell || r.settlements[k].name != p.settlements[k].name
|
||||||
|
|| std::fabs(r.settlements[k].population - p.settlements[k].population) > 1e-6) { match = false; break; }
|
||||||
|
check(match, "settlements round-trip through save");
|
||||||
|
check(r.cellSettlement() == p.cellSettlement(), "cellSettlement index rebuilt on load");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Civ: reseed clears settlements\n");
|
||||||
|
p.generate(cfg);
|
||||||
|
check(p.settlements.empty() && (p.cellSettlement().empty() || p.cellSettlement()[0] == -1), "reseed clears the settlement set");
|
||||||
|
|
||||||
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CIV CHECKS PASSED\n", failures);
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
131
test_ecoregions.cpp
Normal file
131
test_ecoregions.cpp
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Headless test for the ecoregion atlas. No display needed.
|
||||||
|
|
||||||
|
#include "Planet.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
static void check(bool cond, const char* what) {
|
||||||
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||||
|
if (!cond) ++failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void settle(Planet& p, int maxSteps = 800) {
|
||||||
|
int run = 0;
|
||||||
|
for (int s = 0; s < maxSteps; ++s) {
|
||||||
|
double mc = p.step();
|
||||||
|
if (mc < 2.0) { if (++run >= 3) break; } else run = 0;
|
||||||
|
}
|
||||||
|
p.computeClimate();
|
||||||
|
p.classifyBiomes();
|
||||||
|
p.computeBiotaDensity();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sameBiota(const std::vector<CellBiota>& a, const std::vector<CellBiota>& b) {
|
||||||
|
if (a.size() != b.size()) return false;
|
||||||
|
auto eq = [](const std::vector<Organism>& x, const std::vector<Organism>& y) {
|
||||||
|
if (x.size() != y.size()) return false;
|
||||||
|
for (size_t k = 0; k < x.size(); ++k)
|
||||||
|
if (x[k].archetype != y[k].archetype || x[k].biome != y[k].biome) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < a.size(); ++i)
|
||||||
|
if (!eq(a[i].flora, b[i].flora) || !eq(a[i].fauna, b[i].fauna) || !eq(a[i].funga, b[i].funga))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
PlanetConfig cfg; cfg.seed = 9191; cfg.subdivisions = 5;
|
||||||
|
Planet p; p.generate(cfg); settle(p);
|
||||||
|
const int n = (int)p.cells.size();
|
||||||
|
|
||||||
|
std::printf("Ecoregions: extraction\n");
|
||||||
|
p.generateEcoregions();
|
||||||
|
const auto& E = p.ecoregions();
|
||||||
|
const auto& ce = p.cellEcoregion();
|
||||||
|
check(!E.empty(), "generateEcoregions produces regions");
|
||||||
|
check((int)ce.size() == n, "per-cell ecoregion array sized n");
|
||||||
|
|
||||||
|
bool mapped = true, indexOk = true, anchorsOk = true, namesOk = true, domOk = true;
|
||||||
|
std::set<std::string> names;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (p.cells[i].biome != Biome::Ice && ce[i] < 0) mapped = false;
|
||||||
|
if (ce[i] >= (int)E.size()) indexOk = false;
|
||||||
|
}
|
||||||
|
for (const Ecoregion& e : E) {
|
||||||
|
if (e.name.empty() || !names.insert(e.name).second) namesOk = false;
|
||||||
|
if (e.anchorCell < 0 || e.anchorCell >= n || p.cells[e.anchorCell].biome != e.biome
|
||||||
|
|| e.biome == Biome::Ice) anchorsOk = false;
|
||||||
|
auto validArch = [](int a) { return a == -1 || (a >= 0 && a < (int)biotaArchetypes().size()); };
|
||||||
|
if (!validArch(e.dominantFlora) || !validArch(e.dominantFauna) || !validArch(e.dominantFunga)) domOk = false;
|
||||||
|
if (!std::isfinite(e.floraProductivity) || !std::isfinite(e.faunaProductivity)
|
||||||
|
|| !std::isfinite(e.fungaProductivity)) domOk = false;
|
||||||
|
}
|
||||||
|
check(mapped, "every non-ice cell maps to an ecoregion");
|
||||||
|
check(indexOk, "per-cell ecoregion indices are valid");
|
||||||
|
check(anchorsOk, "ecoregion anchors match their biome");
|
||||||
|
check(namesOk, "ecoregion names are unique and non-empty");
|
||||||
|
check(domOk, "dominants/productivity are valid");
|
||||||
|
|
||||||
|
std::printf("Ecoregions: determinism\n");
|
||||||
|
Planet q; q.generate(cfg); settle(q); q.generateEcoregions();
|
||||||
|
bool same = (q.ecoregions().size() == E.size()) && (q.cellEcoregion() == ce);
|
||||||
|
if (same)
|
||||||
|
for (size_t i = 0; i < E.size(); ++i) {
|
||||||
|
const Ecoregion& a = E[i]; const Ecoregion& b = q.ecoregions()[i];
|
||||||
|
if (a.name != b.name || a.biome != b.biome || a.anchorCell != b.anchorCell
|
||||||
|
|| a.size != b.size || a.dominantFlora != b.dominantFlora
|
||||||
|
|| a.dominantFauna != b.dominantFauna || a.dominantFunga != b.dominantFunga) {
|
||||||
|
same = false; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(same, "same seed produces identical ecoregions");
|
||||||
|
|
||||||
|
std::printf("Ecoregions: RNG isolation\n");
|
||||||
|
{
|
||||||
|
Planet a; a.generate(cfg); settle(a); a.drifting = true;
|
||||||
|
Planet b; b.generate(cfg); settle(b); b.drifting = true; b.generateEcoregions();
|
||||||
|
double dt = a.cflDtMy();
|
||||||
|
for (int k = 0; k < 5; ++k) { a.advect(dt); a.step(); a.erode(dt);
|
||||||
|
b.advect(dt); b.step(); b.erode(dt); }
|
||||||
|
bool terrainSame = a.cells.size() == b.cells.size();
|
||||||
|
for (size_t i = 0; terrainSame && i < a.cells.size(); ++i)
|
||||||
|
if (a.cells[i].elevation != b.cells[i].elevation || a.cells[i].plateId != b.cells[i].plateId)
|
||||||
|
terrainSame = false;
|
||||||
|
check(terrainSame, "ecoregions do not perturb tectonic evolution");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Planet a; a.generate(cfg); settle(a); a.generateBiota();
|
||||||
|
Planet b; b.generate(cfg); settle(b); b.generateEcoregions(); b.generateBiota();
|
||||||
|
check(a.biotaPopulated() && b.biotaPopulated() && sameBiota(a.biota(), b.biota()),
|
||||||
|
"ecoregions do not perturb biota generation");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Ecoregions: save v19 round-trip\n");
|
||||||
|
{
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
bool ok = r.readState(ss);
|
||||||
|
check(ok, "readState accepts a v19 stream");
|
||||||
|
bool match = (r.ecoregions().size() == E.size()) && (r.cellEcoregion() == ce);
|
||||||
|
if (match)
|
||||||
|
for (size_t i = 0; i < E.size(); ++i)
|
||||||
|
if (r.ecoregions()[i].name != E[i].name || r.ecoregions()[i].biome != E[i].biome
|
||||||
|
|| r.ecoregions()[i].anchorCell != E[i].anchorCell) { match = false; break; }
|
||||||
|
check(match, "ecoregions round-trip through save");
|
||||||
|
|
||||||
|
ss.clear(); ss.seekg(0);
|
||||||
|
Planet old;
|
||||||
|
bool okOld = old.readState(ss, true, true, true, true, true, true, true, true, true, false);
|
||||||
|
check(okOld && old.ecoregions().empty(), "pre-v19 read leaves ecoregions empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("\n%s (%d failure%s)\n", failures ? "FAILURES" : "ALL ECOREGION CHECKS PASSED",
|
||||||
|
failures, failures == 1 ? "" : "s");
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@
|
|||||||
// src/sim/PlanetGeography.cpp src/sim/PlanetIO.cpp -o /tmp/tg && /tmp/tg
|
// src/sim/PlanetGeography.cpp src/sim/PlanetIO.cpp -o /tmp/tg && /tmp/tg
|
||||||
//
|
//
|
||||||
// Verifies: extraction (continents/oceans/ranges/rivers/lakes), per-cell membership consistency,
|
// Verifies: extraction (continents/oceans/ranges/rivers/lakes), per-cell membership consistency,
|
||||||
// names non-empty/unique/deterministic, RNG isolation from tectonics, and a v18 save round-trip.
|
// names non-empty/unique/deterministic, RNG isolation from tectonics, and a geography save round-trip.
|
||||||
|
|
||||||
#include "Planet.hpp"
|
#include "Planet.hpp"
|
||||||
#include "NameGen.hpp"
|
#include "NameGen.hpp"
|
||||||
@ -226,13 +226,13 @@ int main() {
|
|||||||
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
|
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
|
||||||
check(terrainSame, "generateGeography never perturbs tectonic evolution");
|
check(terrainSame, "generateGeography never perturbs tectonic evolution");
|
||||||
|
|
||||||
std::printf("Geography: save v18 round-trip\n");
|
std::printf("Geography: save round-trip\n");
|
||||||
{
|
{
|
||||||
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
p.writeState(ss);
|
p.writeState(ss);
|
||||||
Planet r;
|
Planet r;
|
||||||
bool ok = r.readState(ss, true, true, true, true, true, true, true, true);
|
bool ok = r.readState(ss, true, true, true, true, true, true, true, true);
|
||||||
check(ok, "readState accepts a v18 stream");
|
check(ok, "readState accepts a geography stream");
|
||||||
bool match = (r.geography().size() == F.size());
|
bool match = (r.geography().size() == F.size());
|
||||||
if (match)
|
if (match)
|
||||||
for (size_t i = 0; i < F.size(); ++i)
|
for (size_t i = 0; i < F.size(); ++i)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user