Add Biota stage: flora, fauna & funga (density + slot/point population)
World-Creation stage after biomes. Two layers (raylib-free engine):
- Per-cell density scalars (flora/fauna/funga in [0,1]) derived from the
climate fields each tick (drive color modes 8/9/0). Flora = Liebig-min of
temp & moisture; fauna ~ flora with carnivores gated on local prey; funga =
moisture/organic-matter-led + cold-tolerant. Zero on water/ice.
- On-demand discrete population (key L, saved as v7): each land cell draws
broad archetypes from a comprehensive table into a per-kind slot cap +
density-scaled point budget (size -> cost), weighted by biome/climate
suitability and a regional bonus for same-biome neighbours. Separate RNG
seeded from cfg.seed so generating biota never perturbs tectonic determinism.
Organisms are labelled by taxonomy (Family + Size + role, e.g. "Felidae
(Big, Carnivore)") with the full Class > Order > Family tree stored, never an
informal common name. Cell-info panel word-wraps + aggregates duplicates so the
lists no longer get cut off.
New: src/sim/PlanetBiota.{hpp,cpp} + PlanetFlora/Fauna/FungiGen.cpp, color
modes/colors, bio* config knobs, save v7 (older saves load with empty
population), test_biota.cpp (densities, fauna<=capacity, carnivore gating,
slot/point budgets, determinism + RNG isolation, v7 round-trip). Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
acc0e5eec9
commit
53e371bb2f
31
BUILD.md
31
BUILD.md
@ -3,7 +3,8 @@
|
|||||||
Fixed icosphere geometry; properties (elevation, plate, age, climate, biome) flow
|
Fixed icosphere geometry; properties (elevation, plate, age, climate, biome) flow
|
||||||
over it. World creation runs as continuous, overlapping stages on a geological clock:
|
over it. World creation runs as continuous, overlapping stages on a geological clock:
|
||||||
tectonics (boundary stress forms mountains/trenches) → continental drift & erosion →
|
tectonics (boundary stress forms mountains/trenches) → continental drift & erosion →
|
||||||
hydrology (rivers/lakes) → climate (temperature/precipitation) → biomes. Initial
|
hydrology (rivers/lakes) → climate (temperature/precipitation) → biomes → biota
|
||||||
|
(flora/fauna/funga). Initial
|
||||||
terrain forming is a generator (a couple hundred paced ticks, ~3 s, auto-pauses at
|
terrain forming is a generator (a couple hundred paced ticks, ~3 s, auto-pauses at
|
||||||
equilibrium), then drift/erosion/etc. continue. Press R to reseed, SPACE to pause.
|
equilibrium), then drift/erosion/etc. continue. Press R to reseed, SPACE to pause.
|
||||||
(The eventual goal is a separate slow real-time "Live World" weather/life mode.)
|
(The eventual goal is a separate slow real-time "Live World" weather/life mode.)
|
||||||
@ -39,11 +40,13 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
|||||||
click open tile detail panel (subtiles grid, hoverable) + overlay
|
click open tile detail panel (subtiles grid, hoverable) + overlay
|
||||||
C close the detail panel
|
C close the detail panel
|
||||||
1 .. 7 color by elevation / plate / age / crust type / biome / temperature / precipitation
|
1 .. 7 color by elevation / plate / age / crust type / biome / temperature / precipitation
|
||||||
|
8 / 9 / 0 color by biota density: flora / fauna / funga
|
||||||
B toggle plate borders (on by default)
|
B toggle plate borders (on by default)
|
||||||
D toggle per-plate drift arrows + P<id> labels (on by default)
|
D toggle per-plate drift arrows + P<id> labels (on by default)
|
||||||
G toggle lat/lon graticule (+ degree numbers on the 2D map edges)
|
G toggle lat/lon graticule (+ degree numbers on the 2D map edges)
|
||||||
J toggle rivers (Phase 2.5 hydrology)
|
J toggle rivers (Phase 2.5 hydrology)
|
||||||
H start/stop Phase 2.5 (hydrology: rivers, lakes, fluvial erosion)
|
H start/stop Phase 2.5 (hydrology: rivers, lakes, fluvial erosion)
|
||||||
|
L generate biota population (flora/fauna/funga; settled world; re-press regenerates)
|
||||||
SPACE pause while forming / re-evolve once settled (or the on-screen button)
|
SPACE pause while forming / re-evolve once settled (or the on-screen button)
|
||||||
[ / ] drift speed (My per real second, Phase 2/3)
|
[ / ] drift speed (My per real second, Phase 2/3)
|
||||||
S single tectonic tick
|
S single tectonic tick
|
||||||
@ -147,14 +150,38 @@ shadow, dry interiors) then diffuses it. Color modes 6 (temperature) / 7 (precip
|
|||||||
climateWindPasses 50 moisture-advection iterations (steady state)
|
climateWindPasses 50 moisture-advection iterations (steady state)
|
||||||
climateMoistureSmooth 12 precipitation diffusion passes (raise = smoother, more grass/forest)
|
climateMoistureSmooth 12 precipitation diffusion passes (raise = smoother, more grass/forest)
|
||||||
|
|
||||||
|
Biota (PlanetConfig): flora/fauna/funga. Density scalars drive color modes 8/9/0;
|
||||||
|
the discrete slot/point population is generated on demand (L) and saved (v7).
|
||||||
|
|
||||||
|
bioVegTempMin -5 C below this no plant growth
|
||||||
|
bioVegTempOpt 15 C at/above this temperature isn't limiting (flora)
|
||||||
|
bioVegMoistRef 0.5 normalized moisture where water isn't limiting (flora)
|
||||||
|
bioFaunaProductivity 0.9 herbivore carrying capacity per unit vegetation
|
||||||
|
bioCarnPreyMin 0.30 min local prey (fauna density) to support carnivores
|
||||||
|
bioCarnScale 1.0 carnivore weight ramp above the prey threshold
|
||||||
|
bioFungaMoistRef 0.4 normalized moisture where fungi aren't water-limited
|
||||||
|
bioFungaFloraWeight 0.6 how much fungi lean on flora (organic matter), 0..1
|
||||||
|
bioFungaTempMin -15 C above this fungi are not cold-limited (cold-tolerant)
|
||||||
|
bioRegionBonus 0.5 weight boost for archetypes present in same-biome neighbours
|
||||||
|
bioFloraSlots 12 max distinct flora per cell (point budget caps abundance)
|
||||||
|
bioFaunaSlots 10 max distinct fauna per cell
|
||||||
|
bioFungaSlots 8 max distinct funga per cell
|
||||||
|
bioFloraPoints 20 flora point budget at full density (scaled by density)
|
||||||
|
bioFaunaPoints 16 fauna point budget at full density
|
||||||
|
bioFungaPoints 14 funga point budget at full density
|
||||||
|
|
||||||
## 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 \
|
||||||
src/sim/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
|
src/sim/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
|
||||||
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
||||||
src/sim/PlanetBiomes.cpp src/sim/PlanetIO.cpp \
|
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||||
|
src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||||
|
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
||||||
-o /tmp/t && /tmp/t
|
-o /tmp/t && /tmp/t
|
||||||
|
|
||||||
|
# Biota suite: same source list, swap test_logic.cpp -> test_biota.cpp
|
||||||
|
|
||||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||||
determinism. Run after changing Planet::step().
|
determinism. Run after changing Planet::step().
|
||||||
|
|
||||||
|
|||||||
72
CLAUDE.md
72
CLAUDE.md
@ -40,10 +40,14 @@ dynamic weather and life.
|
|||||||
updates as terrain changes). Color modes `6`/`7`.
|
updates as terrain changes). Color modes `6`/`7`.
|
||||||
- **Biomes** *(done)* — per-cell `Cell.biome` (13 biomes incl. polar Ice) from elevation
|
- **Biomes** *(done)* — per-cell `Cell.biome` (13 biomes incl. polar Ice) from elevation
|
||||||
+ the climate fields (`Planet::classifyBiomes`), color mode `5`, saved per cell.
|
+ the climate fields (`Planet::classifyBiomes`), color mode `5`, saved per cell.
|
||||||
- **Fauna & flora** *(next, planned — see `docs/fauna-flora-plan.md`)* — derived
|
- **Biota — flora, fauna & funga** *(done — see `docs/fauna-flora-plan.md` +
|
||||||
carrying-capacity densities: vegetation (flora) + a herbivore/carnivore food chain (fauna),
|
`docs/fauna_generation_plan.md`)* — two layers: per-cell **density scalars** (flora,
|
||||||
computed from the climate fields each tick (the living/evolving ecosystem is reserved for
|
fauna, funga ∈ [0,1]) derived from the climate fields each tick (drive the colour views),
|
||||||
Live World). Other follow-ups: feed precipitation into hydrology rainfall; seasons (obliquity).
|
plus a discrete **slot/point population** of broad archetypes (Class/Order/Family/Size),
|
||||||
|
generated **on demand** (`L`) and **saved** (save v7). Fauna is a herbivore/carnivore/
|
||||||
|
omnivore food chain (predators gated on local prey); funga uses a flora-like but
|
||||||
|
moisture/organic-matter-led rule. The living/evolving ecosystem is reserved for Live World.
|
||||||
|
Other follow-ups: feed precipitation into hydrology rainfall; seasons (obliquity).
|
||||||
|
|
||||||
> Durable design context (module layout, save format, climate/biome model, conventions)
|
> Durable design context (module layout, save format, climate/biome model, conventions)
|
||||||
> lives in **`docs/design-notes.md`** — important because Claude's auto-memory does not
|
> lives in **`docs/design-notes.md`** — important because Claude's auto-memory does not
|
||||||
@ -256,6 +260,28 @@ Working and verified (logic tested headless):
|
|||||||
updating with `1`–`7`. The HUD title/status and the hydrology prompt were reworded to drop
|
updating with `1`–`7`. The HUD title/status and the hydrology prompt were reworded to drop
|
||||||
the rigid "Phase N" labels (now "World Creation: forming / drift & erosion / hydrology");
|
the rigid "Phase N" labels (now "World Creation: forming / drift & erosion / hydrology");
|
||||||
internal `phase*` names are unchanged. Render/text only — no sim/save/config change.
|
internal `phase*` names are unchanged. Render/text only — no sim/save/config change.
|
||||||
|
- **Biota (flora/fauna/funga):** the World-Creation stage after biomes. Two layers (src/sim,
|
||||||
|
raylib-free): (1) **density scalars** `sFloraDensity`/`sFaunaDensity`/`sFungaDensity` ∈ [0,1]
|
||||||
|
via `Planet::computeBiotaDensity()` — flora = NPP Liebig-min of temp & moisture (0 on
|
||||||
|
water/Ice), fauna = herbivore capacity ∝ flora with carnivores gated on local prey
|
||||||
|
(`bioCarnPreyMin`), funga = flora-like but moisture/organic-matter-led + cold-tolerant.
|
||||||
|
Derived each tick (like climate), drive color modes `8`/`9`/`0`. (2) A discrete
|
||||||
|
**slot/point population** `Planet::generateBiota()` (key `L`, on a settled world) — each land
|
||||||
|
cell draws broad **archetypes** from a comprehensive table (`biotaArchetypes()`, 36 entries
|
||||||
|
across Flora/Fauna/Funga, each with Class/Order/Family/Size + a biome mask + climate
|
||||||
|
tolerance) into a per-kind slot cap + a density-scaled point budget (Tiny=1…Huge=5 cost),
|
||||||
|
weighted by suitability and a **regional bonus** for archetypes already placed in same-biome
|
||||||
|
neighbours (homogeneous regions, variety at boundaries). Organisms are labelled by their
|
||||||
|
**taxonomy** — Family + Size + role (e.g. *Felidae (Big, Carnivore)*, with the full
|
||||||
|
*Class > Order > Family* tree in `organismTaxonomy()`), never an informal common name like
|
||||||
|
"big cat"; generalist families get a biome adjective (*Desert Muridae*). Uses a **separate
|
||||||
|
RNG** seeded from `cfg.seed` so generating biota never
|
||||||
|
perturbs tectonic determinism. Population is **saved** (`sBiota`, save **v7**); densities are
|
||||||
|
derived/not-saved. New files `PlanetBiota.{hpp,cpp}` + `PlanetFlora/Fauna/FungiGen.cpp`;
|
||||||
|
color modes `floraColor`/`faunaColor`/`fungaColor`; cell-info shows density % + the per-kind
|
||||||
|
organism list. `bio*` config knobs. Headless `test_biota.cpp`: density ranges/zeros, fauna≤
|
||||||
|
capacity, carnivore gating, slot/point budgets, determinism + RNG isolation, v7 round-trip,
|
||||||
|
pre-v7 loads empty. v7 reads v6-and-older (no biota block → empty population; press `L`).
|
||||||
- Mouse hover (in either view) shows per-cell info. Clicking a tile opens a
|
- Mouse hover (in either view) shows per-cell info. Clicking a tile opens a
|
||||||
right-side detail panel: tile info header + the tile's subgrid drawn as a
|
right-side detail panel: tile info header + the tile's subgrid drawn as a
|
||||||
flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res
|
flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res
|
||||||
@ -286,9 +312,16 @@ src/
|
|||||||
PlanetDrift.cpp cflDtMy/advect + plate lifecycle (fission/kick/baby/fuse)
|
PlanetDrift.cpp cflDtMy/advect + plate lifecycle (fission/kick/baby/fuse)
|
||||||
PlanetErosion.cpp erode() + adjustSeaLevel()
|
PlanetErosion.cpp erode() + adjustSeaLevel()
|
||||||
PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3)
|
PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3)
|
||||||
|
PlanetClimate.cpp computeClimate() (temperature + orographic precipitation)
|
||||||
|
PlanetBiomes.cpp classifyBiomes() (per-cell Cell.biome from elev + climate)
|
||||||
|
PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls
|
||||||
|
PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity
|
||||||
|
PlanetFloraGen.cpp computeFloraDensity + fillFlora
|
||||||
|
PlanetFaunaGen.cpp computeFaunaDensity + fillFauna (carnivores gated on prey)
|
||||||
|
PlanetFungiGen.cpp computeFungaDensity + fillFunga (moisture/organic-matter rule)
|
||||||
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/lake)
|
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
||||||
Map2D.* Equal Earth 2D map: positions + projection/draw helpers
|
Map2D.* Equal Earth 2D map: positions + projection/draw helpers
|
||||||
Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids
|
Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids
|
||||||
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
|
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
|
||||||
@ -344,9 +377,12 @@ raylib 5.5 is fetched automatically — do not vendor it.
|
|||||||
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 \
|
||||||
src/sim/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
|
src/sim/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp \
|
||||||
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp \
|
||||||
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetIO.cpp \
|
src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||||
|
src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||||
|
src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
|
||||||
-o /tmp/t && /tmp/t
|
-o /tmp/t && /tmp/t
|
||||||
```
|
```
|
||||||
|
(Swap `test_logic.cpp` for `test_biota.cpp` to run the Biota suite — same source list.)
|
||||||
|
|
||||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||||
the window (the engine lives in `src/sim` and is raylib-free, so it links without
|
the window (the engine lives in `src/sim` and is raylib-free, so it links without
|
||||||
@ -371,13 +407,14 @@ compute-shader port -- a Phase-2 effort.
|
|||||||
|
|
||||||
LMB drag orbit · wheel zoom · hover for cell info (3D or map) ·
|
LMB drag orbit · wheel zoom · hover for cell info (3D or map) ·
|
||||||
click a tile to open its detail panel (subtiles) · `C` close panel ·
|
click a tile to open its detail panel (subtiles) · `C` close panel ·
|
||||||
drag the 2D map to pan it east/west · `1`..`7` color by
|
drag the 2D map to pan it east/west · `1`..`0` color by
|
||||||
elevation/plate/age/crust-type/biome/temperature/precipitation (active mode shown
|
elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga
|
||||||
top-center of the globe) ·
|
(`8`/`9`/`0` = biota density; active mode shown top-center of the globe) ·
|
||||||
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
||||||
all in 3D + 2D) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) ·
|
all in 3D + 2D) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) ·
|
||||||
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
|
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
|
||||||
`H` toggle Phase 3 (hydrology) · `R` reseed ·
|
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
||||||
|
on a settled world; re-press regenerates) · `R` reseed ·
|
||||||
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
||||||
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
||||||
|
|
||||||
@ -395,9 +432,11 @@ 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 **6**; v2 adds the `[`/`]` drift rate, v3 a
|
save header is versioned (currently **7**; 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); newer-than-supported is rejected.
|
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); newer-than-supported is
|
||||||
|
rejected. Older saves (no biota block) load fine with an empty population (press `L`).
|
||||||
**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
|
||||||
config is parsed like `planet.cfg` (unknown keys ignored, missing keys keep defaults),
|
config is parsed like `planet.cfg` (unknown keys ignored, missing keys keep defaults),
|
||||||
written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves —
|
written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves —
|
||||||
@ -454,6 +493,15 @@ triangles (plates are fixed in phase 1).
|
|||||||
`climateContinentality` (inland drying), `climateMoistureSmooth` (diffusion passes →
|
`climateContinentality` (inland drying), `climateMoistureSmooth` (diffusion passes →
|
||||||
wet/dry transition zones; raise for smoother, more grassland/forest), `climateOceanMoisture`,
|
wet/dry transition zones; raise for smoother, more grassland/forest), `climateOceanMoisture`,
|
||||||
`climateOroRefHeight`, `climateWindPasses`. Temperature uses the `biome*` temp params.
|
`climateOroRefHeight`, `climateWindPasses`. Temperature uses the `biome*` temp params.
|
||||||
|
- Biota (`bio*` in PlanetConfig / `planet.cfg`) — density: `bioVegTempMin`/`bioVegTempOpt`/
|
||||||
|
`bioVegMoistRef` (flora temp/moisture limits), `bioFaunaProductivity` (animals per unit
|
||||||
|
flora), `bioCarnPreyMin`/`bioCarnScale` (carnivore prey gate + ramp), `bioFungaMoistRef`/
|
||||||
|
`bioFungaFloraWeight`/`bioFungaTempMin` (funga moisture/organic-matter/cold rules);
|
||||||
|
slot/point population: `bioFloraSlots`/`bioFaunaSlots`/`bioFungaSlots` (distinct-type cap),
|
||||||
|
`bioFloraPoints`/`bioFaunaPoints`/`bioFungaPoints` (point budget at full density, scaled by
|
||||||
|
it; Tiny=1…Huge=5), `bioRegionBonus` (how strongly a cell copies same-biome neighbours →
|
||||||
|
homogeneity vs variety). To add organisms, append to `biotaArchetypes()` in PlanetBiota.cpp
|
||||||
|
(append-only — indices are serialized in v7 saves).
|
||||||
- `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
|
||||||
|
|||||||
@ -26,6 +26,10 @@ add_executable(planetsim
|
|||||||
src/sim/PlanetHydrology.cpp
|
src/sim/PlanetHydrology.cpp
|
||||||
src/sim/PlanetBiomes.cpp
|
src/sim/PlanetBiomes.cpp
|
||||||
src/sim/PlanetClimate.cpp
|
src/sim/PlanetClimate.cpp
|
||||||
|
src/sim/PlanetBiota.cpp
|
||||||
|
src/sim/PlanetFloraGen.cpp
|
||||||
|
src/sim/PlanetFaunaGen.cpp
|
||||||
|
src/sim/PlanetFungiGen.cpp
|
||||||
src/sim/PlanetIO.cpp
|
src/sim/PlanetIO.cpp
|
||||||
# Viewer (raylib) -- src/render
|
# Viewer (raylib) -- src/render
|
||||||
src/render/Colors.cpp
|
src/render/Colors.cpp
|
||||||
|
|||||||
@ -32,15 +32,19 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
|||||||
steepest-descent→rivers, mass-conserving stream-power incision).
|
steepest-descent→rivers, mass-conserving stream-power incision).
|
||||||
- `PlanetClimate.cpp` — `computeClimate()` (temperature + orographic precipitation).
|
- `PlanetClimate.cpp` — `computeClimate()` (temperature + orographic precipitation).
|
||||||
- `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate).
|
- `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate).
|
||||||
|
- `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw +
|
||||||
|
`computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga).
|
||||||
|
- `PlanetFloraGen.cpp` / `PlanetFaunaGen.cpp` / `PlanetFungiGen.cpp` — per-kind density +
|
||||||
|
per-cell `fill*` (fauna gates carnivores on local prey; funga is moisture/organic-led).
|
||||||
- `PlanetIO.cpp` — text config + binary save/load.
|
- `PlanetIO.cpp` — text config + binary save/load.
|
||||||
- (`PlanetBiosphere.cpp` — fauna/flora, planned; see `fauna-flora-plan.md`.)
|
|
||||||
|
|
||||||
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),
|
||||||
`ViewerInput.cpp` (camera/picking/keys), `ViewerRender.cpp` (globe/map/panels/HUD/prompt),
|
`ViewerInput.cpp` (camera/picking/keys), `ViewerRender.cpp` (globe/map/panels/HUD/prompt),
|
||||||
plus topical helpers `Colors` / `Map2D` / `Overlays` / `Picking` / `Panels`.
|
plus topical helpers `Colors` / `Map2D` / `Overlays` / `Picking` / `Panels`.
|
||||||
|
|
||||||
Per-tick order in `Viewer::refreshView()`: `computeHydrology()` (if hydrology on) →
|
Per-tick order in `Viewer::refreshView()`: `computeHydrology()` (if hydrology on) →
|
||||||
`computeClimate()` → `classifyBiomes()` → (`computeBiosphere()` when added) → `recolor()`.
|
`computeClimate()` → `classifyBiomes()` → `computeBiotaDensity()` → `recolor()`. The discrete
|
||||||
|
biota *population* (`generateBiota()`) is NOT in this per-tick path — it's on-demand (key `L`).
|
||||||
|
|
||||||
## Core principle (do not violate)
|
## Core principle (do not violate)
|
||||||
|
|
||||||
@ -56,14 +60,33 @@ between world and model space must compensate with `rotateZ(v, ±tilt)` (src/ren
|
|||||||
Picking.cpp): 3D picking un-rotates the ray hit by `−tilt` before `nearestCell`; 3D plate
|
Picking.cpp): 3D picking un-rotates the ray hit by `−tilt` before `nearestCell`; 3D plate
|
||||||
labels rotate by `+tilt` before projecting. The 2D map + biome/climate are tilt-independent.
|
labels rotate by `+tilt` before projecting. The 2D map + biome/climate are tilt-independent.
|
||||||
|
|
||||||
## Save format (v6) — self-describing config
|
## Save format (v7) — self-describing config + biota population
|
||||||
|
|
||||||
`planet.save` stores `PlanetConfig` as a **self-describing key=value text block** (not a raw
|
`planet.save` stores `PlanetConfig` as a **self-describing key=value text block** (not a raw
|
||||||
POD dump), parsed like `planet.cfg` (`writeConfigFields`/`parseConfigStream` shared in
|
POD dump), parsed like `planet.cfg` (`writeConfigFields`/`parseConfigStream` shared in
|
||||||
PlanetIO.cpp), written at `precision(17)` so doubles round-trip exactly. Consequence:
|
PlanetIO.cpp), written at `precision(17)` so doubles round-trip exactly. Consequence:
|
||||||
**adding/removing PlanetConfig fields no longer breaks saves** (unknown keys ignored, missing
|
**adding/removing PlanetConfig fields no longer breaks saves** (unknown keys ignored, missing
|
||||||
keys keep defaults). v6 cannot load pre-v6 saves (one-time break; a length guard fails it
|
keys keep defaults). v6 cannot load pre-v6 saves (one-time break; a length guard fails it
|
||||||
gracefully). Per-cell `Cell.biome` is saved (a byte appended after `invader`).
|
gracefully). Per-cell `Cell.biome` is saved (a byte appended after `invader`). **v7** appends
|
||||||
|
the **biota population** (`sBiota`): a flag byte, then three `Organism{uint16 archetype, uint8
|
||||||
|
biome}` lists per cell. Densities are derived (not saved). Older saves without the block load
|
||||||
|
fine with an empty population (`readState(is, hasBiome, hasBiota)`; `hasBiota = ver>=7`).
|
||||||
|
|
||||||
|
## Biota (flora / fauna / funga) — density + slot/point population
|
||||||
|
|
||||||
|
Two layers (`PlanetBiota.cpp` + the three `*Gen.cpp`): (1) derived per-cell **density** scalars
|
||||||
|
(0..1) recomputed each tick like climate — flora = Liebig-min(temp, moisture), fauna ∝ flora
|
||||||
|
(carnivores gated on neighbourhood prey ≥ `bioCarnPreyMin`), funga = moisture/organic-matter-led
|
||||||
|
+ cold-tolerant; 0 on water/Ice. (2) On-demand discrete **population** `generateBiota()`: each
|
||||||
|
land cell draws broad archetypes from the comprehensive append-only `biotaArchetypes()` table
|
||||||
|
into a per-kind slot cap + density-scaled point budget (size → cost Tiny=1…Huge=5), weighted by
|
||||||
|
biome/climate suitability and a **regional bonus** for archetypes already in same-biome
|
||||||
|
neighbours (single index-ordered pass → homogeneous regions, boundary variety). Organisms are
|
||||||
|
labelled by **taxonomy** — Family + Size + role (full `Class > Order > Family` in
|
||||||
|
`organismTaxonomy()`), never informal common names ("Felidae", not "cat"); generalist families
|
||||||
|
get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seeded from `cfg.seed`** (not
|
||||||
|
`Planet::rngState`) so populating biota never perturbs tectonic determinism — asserted in
|
||||||
|
`test_biota.cpp`. The archetype table is **append-only** (indices are serialized in v7 saves).
|
||||||
|
|
||||||
## Climate + biome model (derived, not saved)
|
## Climate + biome model (derived, not saved)
|
||||||
|
|
||||||
@ -88,7 +111,9 @@ Engine is raylib-free, so logic is tested without a display. Build/run:
|
|||||||
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \
|
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \
|
||||||
src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp src/sim/PlanetErosion.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/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
|
||||||
|
src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
|
# test_biota.cpp uses the same source list (Biota suite).
|
||||||
```
|
```
|
||||||
(add new `src/sim/*.cpp` to that list as stages are added). `Planet::step()` passes are
|
(add new `src/sim/*.cpp` to that list as stages are added). `Planet::step()` passes are
|
||||||
data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).
|
data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).
|
||||||
|
|||||||
@ -1,82 +1,56 @@
|
|||||||
# Next step — Fauna & Flora (derived carrying-capacity densities)
|
# Biota — Flora · Fauna · Funga (density + slot/point population)
|
||||||
|
|
||||||
> Status: **planned, not yet implemented.** This is the next World-Creation stage after
|
> Status: **implemented.** This supersedes the original density-only sketch. The discrete
|
||||||
> climate. Approved design captured here so it can be picked up verbatim (e.g. after the
|
> slot/point design comes from `docs/fauna_generation_plan.md`; this doc records the unified,
|
||||||
> project moves to another machine — the auto-memory does not travel, this doc does).
|
> shipped design. Code: `src/sim/PlanetBiota.{hpp,cpp}` + `PlanetFloraGen/FaunaGen/FungiGen.cpp`;
|
||||||
|
> test `test_biota.cpp`; controls `8`/`9`/`0` (density views) + `L` (generate population).
|
||||||
|
|
||||||
## Decision summary
|
## Three kinds (Biota)
|
||||||
|
|
||||||
- **Derived carrying-capacity map**, not a live simulation: vegetation + wildlife
|
Living things are split into **Flora** (plants), **Fauna** (animals) and **Funga** (fungi).
|
||||||
**density scalars** recomputed each tick from the climate fields + terrain, exactly like
|
Funga is *not* a plant but is generated with a flora-like environmental system and its own
|
||||||
biomes/climate. It answers *"what could live here"*, not *"what is living/evolving here"*.
|
rules; all three share the same per-cell slot/point logic.
|
||||||
- The actual **time-evolving ecosystem** (population growth, migration, predator-prey,
|
|
||||||
extinction, seasons, species) is reserved for the future **Live World** real-time mode
|
|
||||||
(year/season timescales — not the My geological clock).
|
|
||||||
- Fauna uses a **herbivore + carnivore** food-chain split.
|
|
||||||
- Fields are **derived / not saved** (deterministic functions of climate) → **no
|
|
||||||
save-format change**. New config knobs ride the v6 self-describing save with no break.
|
|
||||||
|
|
||||||
## Part A — Biosphere fields (`src/sim/`, raylib-free)
|
## Two layers
|
||||||
|
|
||||||
- **`Planet.hpp`**: declare `void computeBiosphere();` + accessors
|
1. **Density scalars** (`sFloraDensity`/`sFaunaDensity`/`sFungaDensity` ∈ [0,1]) — derived from
|
||||||
`const std::vector<double>& vegetation()/herbivores()/carnivores() const`. Add scratch
|
the climate fields each tick (like biomes/climate), drive the colour views. 0 on water/Ice.
|
||||||
members `std::vector<double> sVeg, sHerb, sCarn;` (not saved).
|
- Flora = NPP-style **Liebig minimum** of a temperature factor
|
||||||
- **New `src/sim/PlanetBiosphere.cpp`** — `Planet::computeBiosphere()` (run *after*
|
`clamp((T−bioVegTempMin)/(bioVegTempOpt−bioVegTempMin),0,1)` and a moisture factor
|
||||||
`computeClimate()`; reads `sTemp` + `sMoist`):
|
`clamp(sMoist/bioVegMoistRef,0,1)`.
|
||||||
- **Vegetation (flora)** `sVeg[i] ∈ [0,1]`: `0` on water (`elev ≤ seaLevel`). On land, an
|
- Fauna = herbivore carrying capacity `clamp(floraDensity·bioFaunaProductivity,0,1)`;
|
||||||
NPP-style **Liebig minimum** of a temperature factor and a moisture factor:
|
carnivores in the *population* are gated on neighbourhood prey ≥ `bioCarnPreyMin`.
|
||||||
- `tF = clamp((sTemp[i] − bioVegTempMin)/(bioVegTempOpt − bioVegTempMin), 0, 1)`
|
- Funga = `min(moistFactor, bioFungaFloraWeight·floraDensity + (1−w)) · coldTolerance`
|
||||||
- `mF = clamp(sMoist[i]/bioVegMoistRef, 0, 1)`
|
(`bioFungaMoistRef`, `bioFungaTempMin`) — thrives moist + with organic matter, persists in
|
||||||
- `sVeg = min(tF, mF)` → lush warm-wet, ~0 in ice/desert/alpine (cold- or water-limited),
|
cold shade where flora thins, ~0 in hot dry desert.
|
||||||
matching the biomes.
|
2. **Discrete slot/point population** (`sBiota`, saved v7) — generated **on demand**
|
||||||
- **Herbivores** `sHerb[i] = clamp(sVeg[i] · bioHerbProductivity, 0, 1)` (capacity scales
|
(`generateBiota()`, key `L`, settled world). Each land cell draws broad **archetypes** (NOT
|
||||||
with plant productivity).
|
real species) from the comprehensive append-only table `biotaArchetypes()` — each carries a
|
||||||
- **Carnivores** `sCarn[i]`: present only where prey is abundant, ramping with it:
|
Class/Order/Family + Size (Tiny..Huge), a biome mask and a climate tolerance. A cell has a
|
||||||
`sCarn = sHerb > bioCarnPreyMin ? clamp((sHerb − bioCarnPreyMin)/(1 − bioCarnPreyMin)
|
per-kind **slot** cap (distinct types) and a density-scaled **point** budget (size → cost
|
||||||
· bioCarnScale, 0, 1) : 0` → concentrated in the richest regions (an energy-pyramid feel,
|
Tiny=1…Huge=5); archetypes are added until slots full or points exhausted. Selection is
|
||||||
visually distinct from the broader herbivore pattern).
|
weighted by biome/climate suitability × a **regional bonus** for archetypes already placed in
|
||||||
- **`src/sim/Planet.cpp` `generate()`**: call `computeBiosphere()` after `classifyBiomes()`.
|
already-filled, same-biome neighbours (homogeneous regions, variety at boundaries). Organisms
|
||||||
|
are **named by taxonomy** — Family + Size + role (full `Class > Order > Family` available),
|
||||||
|
never an informal common name ("Felidae", not "cat"; Felidae is the family, cat a common
|
||||||
|
name). Generalist families get a biome adjective ("Desert Muridae" / "Forest Muridae").
|
||||||
|
|
||||||
## Part B — Config knobs (`PlanetTypes.hpp` + `PlanetIO.cpp`)
|
## Config knobs (`bio*`, in `planet.cfg`)
|
||||||
|
|
||||||
Add to `PlanetConfig` + `CONFIG_FIELDS` + `validateConfig`:
|
`bioVegTempMin(-5)`, `bioVegTempOpt(15)`, `bioVegMoistRef(0.5)`, `bioFaunaProductivity(0.9)`,
|
||||||
|
`bioCarnPreyMin(0.30)`, `bioCarnScale(1.0)`, `bioFungaMoistRef(0.4)`, `bioFungaFloraWeight(0.6)`,
|
||||||
|
`bioFungaTempMin(-15)`, `bioRegionBonus(0.5)`, slot caps `bioFlora/Fauna/FungaSlots(12/10/8)`,
|
||||||
|
point budgets `bioFlora/Fauna/FungaPoints(20/16/14)`.
|
||||||
|
|
||||||
| key | default | meaning |
|
## Determinism & persistence
|
||||||
|---|---|---|
|
|
||||||
| `bioVegTempMin` | −5 °C | below this, no plant growth |
|
|
||||||
| `bioVegTempOpt` | 15 °C | at/above this, temperature isn't limiting |
|
|
||||||
| `bioVegMoistRef` | 0.5 | normalized moisture at which water isn't limiting |
|
|
||||||
| `bioHerbProductivity` | 0.9 | herbivore capacity per unit vegetation |
|
|
||||||
| `bioCarnPreyMin` | 0.30 | min herbivore density to support carnivores |
|
|
||||||
| `bioCarnScale` | 1.0 | carnivore density ramp above the prey threshold |
|
|
||||||
|
|
||||||
## Part C — Rendering (`src/render/`)
|
`generateBiota()` uses a **separate RNG seeded from `cfg.seed`** (not `Planet::rngState`), so it
|
||||||
|
never perturbs tectonic determinism (asserted in `test_biota.cpp`). The population is **saved**
|
||||||
- **`Colors.{hpp,cpp}`**: `ColorMode` += `Vegetation`, `Herbivores`, `Carnivores`; add
|
(save **v7**); densities are derived/not-saved. Older saves load with an empty population (press
|
||||||
`vegColor` (barren tan → lush green), `herbColor` (pale → amber/orange), `carnColor`
|
`L`). The archetype table is **append-only** — indices are serialized.
|
||||||
(pale → red/violet); extend `colorModeName`.
|
|
||||||
- **`Viewer.cpp`**: `recolor()` adds the three cases (read the accessors, empty-guarded);
|
|
||||||
`refreshView()` calls `planet.computeBiosphere()` after `classifyBiomes()`.
|
|
||||||
- **`ViewerInput.cpp`**: `KEY_EIGHT` → Vegetation, `KEY_NINE` → Herbivores, `KEY_ZERO` →
|
|
||||||
Carnivores (each `recolor()`). The top-center view-name label already shows the mode.
|
|
||||||
- **`ViewerRender.cpp` `renderHUD`**: controls line gains `8 veg 9 herbiv 0 carniv`.
|
|
||||||
- **`Panels.cpp` `cellInfo()`**: add a life line — `vegetation X% herbivores Y%
|
|
||||||
carnivores Z%` (guarded on the arrays being sized).
|
|
||||||
|
|
||||||
## Verification (headless — extend the `src/sim` test set with `PlanetBiosphere.cpp`)
|
|
||||||
|
|
||||||
- **Vegetation:** 0 on ocean & Ice cells; near-0 on Desert; high (>0.6) on warm-wet Forest;
|
|
||||||
all in [0,1]; finite; deterministic.
|
|
||||||
- **Fauna food chain:** `sHerb` correlates with `sVeg`; `sCarn[i] > 0 ⇒ sHerb[i] >
|
|
||||||
bioCarnPreyMin` and carnivore-positive cells are a strict subset of herbivore-rich cells;
|
|
||||||
all in [0,1]; deterministic.
|
|
||||||
- `test_logic`, biomes+config, climate suites still pass; app builds clean; no save change.
|
|
||||||
- **GUI:** `8` lush green continents thinning to barren in deserts/poles/peaks; `9` grazer
|
|
||||||
density following productivity; `0` predators concentrated in the richest belts; cell info
|
|
||||||
shows vegetation/herbivore/carnivore %.
|
|
||||||
|
|
||||||
## Out of scope (future / Live World)
|
## Out of scope (future / Live World)
|
||||||
|
|
||||||
Time-evolving populations (growth, migration, predator-prey, extinction), seasons, and
|
Time-evolving populations (growth, migration, predator-prey dynamics, extinction), seasons, and
|
||||||
species/typed organisms — all part of the future **Live World** real-time simulation that
|
true species/typed organisms — reserved for the **Live World** real-time mode. This stage
|
||||||
runs the finished planet at hours/days/weeks/months with dynamic weather + life.
|
answers *"what could live here"*, not *"what is living/evolving here"*.
|
||||||
|
|||||||
91
docs/fauna_generation_plan.md
Normal file
91
docs/fauna_generation_plan.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
Fauna Generation Plan
|
||||||
|
|
||||||
|
For fauna, I want the system to handle animals in broad ecological groups: predators, herbivores, and animals that fall somewhere in between.
|
||||||
|
1. Basic Animal Types
|
||||||
|
|
||||||
|
First, we should define basic animal types that can exist in many different environments. These are not specific species, but general animal categories.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
Rodents
|
||||||
|
Desert rodents in desert regions
|
||||||
|
Swamp rodents in swamp regions
|
||||||
|
Forest rodents in forest regions
|
||||||
|
|
||||||
|
The same approach should be used for other common animal groups that can appear across many biomes.
|
||||||
|
2. Animal Classification
|
||||||
|
|
||||||
|
For regional animals, we should describe them more by broad biological classification and size rather than by exact genus or species.
|
||||||
|
|
||||||
|
The classification should use something like:
|
||||||
|
|
||||||
|
Class
|
||||||
|
Order
|
||||||
|
Family
|
||||||
|
Size category
|
||||||
|
|
||||||
|
Example:
|
||||||
|
text
|
||||||
|
|
||||||
|
Animal: Cat-like predator
|
||||||
|
Class: Mammal
|
||||||
|
Order: Carnivora
|
||||||
|
Family: Felidae
|
||||||
|
Size: Small
|
||||||
|
|
||||||
|
A tiger-like animal would use the same family but have a larger size:
|
||||||
|
text
|
||||||
|
|
||||||
|
Animal: Tiger-like predator
|
||||||
|
Class: Mammal
|
||||||
|
Order: Carnivora
|
||||||
|
Family: Felidae
|
||||||
|
Size: Big
|
||||||
|
|
||||||
|
Size categories could be:
|
||||||
|
|
||||||
|
Tiny
|
||||||
|
Small
|
||||||
|
Medium
|
||||||
|
Big
|
||||||
|
Huge
|
||||||
|
|
||||||
|
If more biological information is needed, please ask me. You can also look up general classification information if needed.
|
||||||
|
3. Cell Population System
|
||||||
|
|
||||||
|
Each map cell should be populated using a slot and point system.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
Each cell has 10 fauna slots
|
||||||
|
Each cell has 20 fauna points
|
||||||
|
|
||||||
|
Every animal takes up a certain number of points depending on its size:
|
||||||
|
Size Point Cost
|
||||||
|
Tiny 1
|
||||||
|
Small 2
|
||||||
|
Medium 3
|
||||||
|
Big 4
|
||||||
|
Huge 5
|
||||||
|
|
||||||
|
Animals are added to the cell until either:
|
||||||
|
|
||||||
|
All slots are filled, or
|
||||||
|
All points are used
|
||||||
|
|
||||||
|
Once one of these limits is reached, the system stops adding animals to that cell.
|
||||||
|
4. Regional Distribution
|
||||||
|
|
||||||
|
To create a more homogeneous and natural distribution, the system should check neighboring cells when populating fauna.
|
||||||
|
|
||||||
|
The rules could be:
|
||||||
|
|
||||||
|
If neighboring cells have the same climate, there is a high probability that the same or similar animals appear there.
|
||||||
|
If neighboring cells have a different climate, it is more likely animals should be generated instead.
|
||||||
|
Similar biomes should share more fauna.
|
||||||
|
Very different biomes should have more distinct fauna.
|
||||||
|
|
||||||
|
This should help avoid every cell feeling completely random while still allowing variety between different regions.
|
||||||
|
5. Overall Goal
|
||||||
|
|
||||||
|
The goal is to create a fauna system that feels natural, biome-based, and regionally consistent, without needing to define every animal as an exact real-world species. Animals should be generated from broad biological groups, ecological roles, and size categories.
|
||||||
368
docs/flora_generation_plan.md
Normal file
368
docs/flora_generation_plan.md
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
Flora Generation Plan
|
||||||
|
|
||||||
|
For flora, I want the system to generate plant life based on biome, climate, terrain, water availability, and regional consistency. Like fauna, flora should not always be exact real-world species, but broader plant types that fit naturally into the environment.
|
||||||
|
1. Basic Plant Types
|
||||||
|
|
||||||
|
First, we should define basic plant categories that can appear across many environments, with variations depending on biome and climate.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
Grasses
|
||||||
|
Dry grass in savannas
|
||||||
|
Marsh grass in wetlands
|
||||||
|
Alpine grass in mountains
|
||||||
|
Shrubs
|
||||||
|
Desert shrubs
|
||||||
|
Thorny shrubs
|
||||||
|
Berry bushes
|
||||||
|
Trees
|
||||||
|
Tropical trees
|
||||||
|
Conifer trees
|
||||||
|
Deciduous trees
|
||||||
|
Mangrove trees
|
||||||
|
Fungi
|
||||||
|
Forest mushrooms
|
||||||
|
Swamp fungi
|
||||||
|
Cave fungi
|
||||||
|
|
||||||
|
These should work similarly to the “basic animal types” from the fauna system. A plant type can exist in many places, but its exact form changes depending on the environment.
|
||||||
|
2. Flora Classification
|
||||||
|
|
||||||
|
Flora should be described using broad plant groups rather than exact species.
|
||||||
|
|
||||||
|
Possible classification fields:
|
||||||
|
|
||||||
|
Plant group
|
||||||
|
Growth form
|
||||||
|
Size category
|
||||||
|
Climate preference
|
||||||
|
Water requirement
|
||||||
|
Terrain preference
|
||||||
|
Ecological role
|
||||||
|
|
||||||
|
Example:
|
||||||
|
text
|
||||||
|
|
||||||
|
Plant: Desert shrub
|
||||||
|
Group: Angiosperm
|
||||||
|
Growth Form: Shrub
|
||||||
|
Size: Small
|
||||||
|
Climate: Arid / Hot
|
||||||
|
Water Requirement: Low
|
||||||
|
Terrain: Sandy / Rocky
|
||||||
|
Role: Ground cover / Food source
|
||||||
|
|
||||||
|
Another example:
|
||||||
|
text
|
||||||
|
|
||||||
|
Plant: Giant rainforest tree
|
||||||
|
Group: Angiosperm
|
||||||
|
Growth Form: Tree
|
||||||
|
Size: Huge
|
||||||
|
Climate: Tropical / Humid
|
||||||
|
Water Requirement: High
|
||||||
|
Terrain: Soil-rich lowland
|
||||||
|
Role: Canopy / Habitat
|
||||||
|
|
||||||
|
Possible growth forms:
|
||||||
|
|
||||||
|
Grass
|
||||||
|
Moss
|
||||||
|
Fern
|
||||||
|
Shrub
|
||||||
|
Bush
|
||||||
|
Tree
|
||||||
|
Vine
|
||||||
|
Reed
|
||||||
|
Cactus / succulent
|
||||||
|
Fungus
|
||||||
|
Aquatic plant
|
||||||
|
|
||||||
|
Possible size categories:
|
||||||
|
|
||||||
|
Tiny
|
||||||
|
Small
|
||||||
|
Medium
|
||||||
|
Big
|
||||||
|
Huge
|
||||||
|
|
||||||
|
3. Biome-Based Flora
|
||||||
|
|
||||||
|
Each biome should have a set of likely plant types.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Desert
|
||||||
|
|
||||||
|
Likely flora:
|
||||||
|
|
||||||
|
Cacti / succulents
|
||||||
|
Dry shrubs
|
||||||
|
Thorn bushes
|
||||||
|
Sparse grasses
|
||||||
|
Drought-resistant flowers
|
||||||
|
|
||||||
|
Rare flora:
|
||||||
|
|
||||||
|
Oasis trees
|
||||||
|
Reeds near water
|
||||||
|
Desert fungi after rain
|
||||||
|
|
||||||
|
Forest
|
||||||
|
|
||||||
|
Likely flora:
|
||||||
|
|
||||||
|
Deciduous trees
|
||||||
|
Conifer trees
|
||||||
|
Ferns
|
||||||
|
Moss
|
||||||
|
Mushrooms
|
||||||
|
Berry bushes
|
||||||
|
|
||||||
|
Rare flora:
|
||||||
|
|
||||||
|
Ancient giant trees
|
||||||
|
Poisonous flowers
|
||||||
|
Medicinal herbs
|
||||||
|
|
||||||
|
Swamp / Wetland
|
||||||
|
|
||||||
|
Likely flora:
|
||||||
|
|
||||||
|
Reeds
|
||||||
|
Mangroves
|
||||||
|
Water lilies
|
||||||
|
Moss
|
||||||
|
Wetland grasses
|
||||||
|
Fungi
|
||||||
|
|
||||||
|
Rare flora:
|
||||||
|
|
||||||
|
Carnivorous plants
|
||||||
|
Giant swamp trees
|
||||||
|
Rare medicinal roots
|
||||||
|
|
||||||
|
Grassland / Steppe
|
||||||
|
|
||||||
|
Likely flora:
|
||||||
|
|
||||||
|
Grasses
|
||||||
|
Wildflowers
|
||||||
|
Small shrubs
|
||||||
|
Herbs
|
||||||
|
|
||||||
|
Rare flora:
|
||||||
|
|
||||||
|
Lone trees
|
||||||
|
Thorn bushes
|
||||||
|
Seasonal flowers
|
||||||
|
|
||||||
|
Mountain / Alpine
|
||||||
|
|
||||||
|
Likely flora:
|
||||||
|
|
||||||
|
Alpine grass
|
||||||
|
Lichens
|
||||||
|
Moss
|
||||||
|
Small shrubs
|
||||||
|
Hardy flowers
|
||||||
|
|
||||||
|
Rare flora:
|
||||||
|
|
||||||
|
Ancient mountain trees
|
||||||
|
Rare herbs
|
||||||
|
Snow-resistant plants
|
||||||
|
|
||||||
|
Tundra
|
||||||
|
|
||||||
|
Likely flora:
|
||||||
|
|
||||||
|
Moss
|
||||||
|
Lichens
|
||||||
|
Low shrubs
|
||||||
|
Cold-resistant grasses
|
||||||
|
|
||||||
|
Rare flora:
|
||||||
|
|
||||||
|
Seasonal flowers
|
||||||
|
Fungi
|
||||||
|
Hardy berry bushes
|
||||||
|
|
||||||
|
4. Cell Population System
|
||||||
|
|
||||||
|
Flora can also use a slot and point system, similar to fauna.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
Each cell has 15 flora slots
|
||||||
|
Each cell has 30 flora points
|
||||||
|
|
||||||
|
Plants are added until either the slots or points are filled.
|
||||||
|
|
||||||
|
Suggested point cost by size:
|
||||||
|
Size Point Cost
|
||||||
|
Tiny 1
|
||||||
|
Small 2
|
||||||
|
Medium 3
|
||||||
|
Big 5
|
||||||
|
Huge 8
|
||||||
|
|
||||||
|
Flora may need more slots than fauna because plants are usually more numerous and layered.
|
||||||
|
|
||||||
|
A cell could contain:
|
||||||
|
text
|
||||||
|
|
||||||
|
Cell Flora:
|
||||||
|
- Forest floor moss, Tiny, 1 point
|
||||||
|
- Ferns, Small, 2 points
|
||||||
|
- Berry bushes, Small, 2 points
|
||||||
|
- Deciduous trees, Big, 5 points
|
||||||
|
- Ancient oak-like trees, Huge, 8 points
|
||||||
|
|
||||||
|
The system stops adding plants once either:
|
||||||
|
|
||||||
|
All flora slots are filled, or
|
||||||
|
All flora points are used
|
||||||
|
|
||||||
|
5. Flora Layers
|
||||||
|
|
||||||
|
To make flora feel more natural, each cell can have vegetation layers.
|
||||||
|
|
||||||
|
Possible layers:
|
||||||
|
|
||||||
|
Ground layer
|
||||||
|
Herb layer
|
||||||
|
Shrub layer
|
||||||
|
Understory layer
|
||||||
|
Canopy layer
|
||||||
|
Aquatic layer
|
||||||
|
|
||||||
|
Not every biome needs every layer.
|
||||||
|
|
||||||
|
Example for a forest:
|
||||||
|
text
|
||||||
|
|
||||||
|
Ground Layer: Moss, fungi
|
||||||
|
Herb Layer: Ferns, flowers
|
||||||
|
Shrub Layer: Berry bushes
|
||||||
|
Understory Layer: Young trees
|
||||||
|
Canopy Layer: Large trees
|
||||||
|
|
||||||
|
Example for a desert:
|
||||||
|
text
|
||||||
|
|
||||||
|
Ground Layer: Dry grass, small succulents
|
||||||
|
Shrub Layer: Thorn bushes
|
||||||
|
Tree Layer: Rare oasis trees
|
||||||
|
|
||||||
|
This helps prevent weird combinations, like a dense rainforest canopy appearing in a dry desert cell.
|
||||||
|
6. Climate and Terrain Rules
|
||||||
|
|
||||||
|
Flora should be strongly influenced by environmental conditions.
|
||||||
|
|
||||||
|
Important factors:
|
||||||
|
|
||||||
|
Temperature
|
||||||
|
Rainfall
|
||||||
|
Soil quality
|
||||||
|
Terrain type
|
||||||
|
Elevation
|
||||||
|
Nearby water
|
||||||
|
Sunlight
|
||||||
|
Seasonality
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
High rainfall increases trees, moss, ferns, and fungi.
|
||||||
|
Low rainfall increases cacti, succulents, dry shrubs, and sparse grasses.
|
||||||
|
Cold climate increases moss, lichen, conifers, and low shrubs.
|
||||||
|
High elevation reduces large trees and favors alpine plants.
|
||||||
|
Wet terrain increases reeds, aquatic plants, mangroves, and swamp trees.
|
||||||
|
Poor soil reduces plant density.
|
||||||
|
Fertile soil increases plant diversity and size.
|
||||||
|
|
||||||
|
7. Regional Distribution
|
||||||
|
|
||||||
|
Like fauna, flora should check neighboring cells to create a more natural and homogeneous distribution.
|
||||||
|
|
||||||
|
Rules could be:
|
||||||
|
|
||||||
|
Same biome and same climate: high probability of sharing the same flora.
|
||||||
|
Same climate but different terrain: moderate probability of similar flora.
|
||||||
|
Different climate: generate new flora.
|
||||||
|
Nearby rivers, lakes, and coasts can spread wetland or aquatic plants.
|
||||||
|
Mountain ranges, deserts, and oceans can act as barriers to plant spread.
|
||||||
|
|
||||||
|
This means forests should gradually change into grasslands or swamps instead of switching completely from one cell to the next.
|
||||||
|
8. Rarity and Special Plants
|
||||||
|
|
||||||
|
The system should support common, uncommon, rare, and unique flora.
|
||||||
|
|
||||||
|
Possible rarity levels:
|
||||||
|
|
||||||
|
Common
|
||||||
|
Uncommon
|
||||||
|
Rare
|
||||||
|
Very rare
|
||||||
|
Unique
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
text
|
||||||
|
|
||||||
|
Common: Grass, moss, reeds
|
||||||
|
Uncommon: Berry bushes, medicinal herbs
|
||||||
|
Rare: Carnivorous plants, glowing mushrooms
|
||||||
|
Very Rare: Ancient trees, magical flowers
|
||||||
|
Unique: World-tree fragment, legendary herb
|
||||||
|
|
||||||
|
Rarity can be affected by biome and world rules.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
Carnivorous plants are rare in swamps.
|
||||||
|
Glowing mushrooms are rare in caves or dark forests.
|
||||||
|
Ancient trees are rare in old forests.
|
||||||
|
Medicinal herbs are uncommon in mountains or forests.
|
||||||
|
|
||||||
|
9. Ecological Role
|
||||||
|
|
||||||
|
Each plant type should have an ecological role. This can later connect flora to fauna, crafting, survival, or gameplay systems.
|
||||||
|
|
||||||
|
Possible roles:
|
||||||
|
|
||||||
|
Food source
|
||||||
|
Shelter
|
||||||
|
Nesting material
|
||||||
|
Medicine
|
||||||
|
Poison
|
||||||
|
Crafting material
|
||||||
|
Building material
|
||||||
|
Fuel
|
||||||
|
Soil stabilizer
|
||||||
|
Water indicator
|
||||||
|
Magical / special resource
|
||||||
|
|
||||||
|
Example:
|
||||||
|
text
|
||||||
|
|
||||||
|
Plant: Berry bush
|
||||||
|
Growth Form: Bush
|
||||||
|
Size: Small
|
||||||
|
Climate: Temperate
|
||||||
|
Water Requirement: Medium
|
||||||
|
Role: Food source for animals and humans
|
||||||
|
|
||||||
|
Example:
|
||||||
|
text
|
||||||
|
|
||||||
|
Plant: Thorn shrub
|
||||||
|
Growth Form: Shrub
|
||||||
|
Size: Small
|
||||||
|
Climate: Arid
|
||||||
|
Water Requirement: Low
|
||||||
|
Role: Shelter for small animals, natural barrier
|
||||||
|
|
||||||
|
10. Overall Goal
|
||||||
|
|
||||||
|
The goal is to create a flora system that feels natural, biome-based, and regionally consistent. Plants should be generated from broad categories, growth forms, ecological roles, and environmental requirements rather than needing to define every exact species.
|
||||||
|
|
||||||
|
The flora system should also support later gameplay features such as animal habitats, food chains, crafting materials, medicine, poison, and rare discoveries.
|
||||||
@ -88,10 +88,40 @@ const char* colorModeName(ColorMode m) {
|
|||||||
case ColorMode::Biome: return "Biome";
|
case ColorMode::Biome: return "Biome";
|
||||||
case ColorMode::Temperature: return "Temperature";
|
case ColorMode::Temperature: return "Temperature";
|
||||||
case ColorMode::Precip: return "Precipitation";
|
case ColorMode::Precip: return "Precipitation";
|
||||||
|
case ColorMode::FloraDensity: return "Flora density";
|
||||||
|
case ColorMode::FaunaDensity: return "Fauna density";
|
||||||
|
case ColorMode::FungaDensity: return "Funga density";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Two-colour density ramp helper: barren -> rich.
|
||||||
|
static Color ramp2(double d01, const unsigned char lo[3], const unsigned char hi[3]) {
|
||||||
|
double t = std::clamp(d01, 0.0, 1.0);
|
||||||
|
auto L = [&](int c) { return (unsigned char)(lo[c] + (hi[c] - lo[c]) * t); };
|
||||||
|
return Color{ L(0), L(1), L(2), 255 };
|
||||||
|
}
|
||||||
|
|
||||||
|
Color floraColor(double d01) { // barren tan -> lush green
|
||||||
|
static const unsigned char lo[3] = { 200, 190, 150 }, hi[3] = { 25, 120, 35 };
|
||||||
|
return ramp2(d01, lo, hi);
|
||||||
|
}
|
||||||
|
Color faunaColor(double d01) { // pale -> amber -> red
|
||||||
|
double t = std::clamp(d01, 0.0, 1.0);
|
||||||
|
static const unsigned char key[3][3] = {
|
||||||
|
{ 225, 220, 195 }, // 0.0 pale
|
||||||
|
{ 220, 160, 60 }, // 0.5 amber
|
||||||
|
{ 180, 55, 40 }, // 1.0 red
|
||||||
|
};
|
||||||
|
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 fungaColor(double d01) { // pale -> violet/brown
|
||||||
|
static const unsigned char lo[3] = { 215, 205, 210 }, hi[3] = { 110, 55, 120 };
|
||||||
|
return ramp2(d01, lo, hi);
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
// Cell color mapping for the viewer. Pure functions of cell properties.
|
// Cell color mapping for the viewer. Pure functions of cell properties.
|
||||||
|
|
||||||
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip };
|
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
||||||
|
FloraDensity, FaunaDensity, FungaDensity };
|
||||||
|
|
||||||
Color elevationColor(double e, double seaLevel);
|
Color elevationColor(double e, double seaLevel);
|
||||||
Color plateColor(int id);
|
Color plateColor(int id);
|
||||||
@ -21,3 +22,8 @@ const char* colorModeName(ColorMode m);
|
|||||||
// (tan dry -> green -> blue wet).
|
// (tan dry -> green -> blue wet).
|
||||||
Color tempColor(double celsius);
|
Color tempColor(double celsius);
|
||||||
Color precipColor(double moist01);
|
Color precipColor(double moist01);
|
||||||
|
// Biota density ramps (0..1): flora barren->lush green, fauna pale->amber/red,
|
||||||
|
// funga pale->violet/brown.
|
||||||
|
Color floraColor(double d01);
|
||||||
|
Color faunaColor(double d01);
|
||||||
|
Color fungaColor(double d01);
|
||||||
|
|||||||
@ -1,10 +1,34 @@
|
|||||||
#include "Panels.hpp"
|
#include "Panels.hpp"
|
||||||
#include "Colors.hpp" // elevationColor (subtile grid)
|
#include "Colors.hpp" // elevationColor (subtile grid)
|
||||||
|
#include "PlanetBiota.hpp" // organismName / sizeName / roleName
|
||||||
#include "Projection.hpp" // dirToLonLat
|
#include "Projection.hpp" // dirToLonLat
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
// Draw `text` word-wrapped to `maxW` pixels starting at (x,y); continuation lines
|
||||||
|
// are indented. Returns the y after the last line; stops drawing past `maxY` (but
|
||||||
|
// keeps advancing y so callers can detect the overflow). Long biota lists would
|
||||||
|
// otherwise run off the right edge of the cell-info panel.
|
||||||
|
static int drawWrapped(const std::string& text, int x, int y, int font, Color col,
|
||||||
|
int maxW, int lineH, int maxY) {
|
||||||
|
std::istringstream iss(text);
|
||||||
|
std::string word, line;
|
||||||
|
int indent = 0;
|
||||||
|
auto flush = [&]() {
|
||||||
|
if (!line.empty()) { if (y + lineH <= maxY) DrawText(line.c_str(), x + indent, y, font, col);
|
||||||
|
y += lineH; line.clear(); indent = 14; }
|
||||||
|
};
|
||||||
|
while (iss >> word) {
|
||||||
|
std::string test = line.empty() ? word : line + " " + word;
|
||||||
|
if (MeasureText(test.c_str(), font) > maxW - indent && !line.empty()) { flush(); line = word; }
|
||||||
|
else line = test;
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
// elev/age come from the display snapshot so the readout matches what is drawn.
|
// elev/age come from the display snapshot so the readout matches what is drawn.
|
||||||
static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, double age) {
|
static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, double age) {
|
||||||
@ -32,6 +56,39 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
|||||||
L.push_back(std::string(TextFormat("river: discharge %.0f", p.discharge()[i])));
|
L.push_back(std::string(TextFormat("river: discharge %.0f", p.discharge()[i])));
|
||||||
if (sized(p.lakeDepth()) && p.lakeDepth()[i] > p.cfg.biomeLakeMinDepth && elev > p.cfg.seaLevel)
|
if (sized(p.lakeDepth()) && p.lakeDepth()[i] > p.cfg.biomeLakeMinDepth && elev > p.cfg.seaLevel)
|
||||||
L.push_back(std::string(TextFormat("lake: depth %.0f m", p.lakeDepth()[i])));
|
L.push_back(std::string(TextFormat("lake: depth %.0f m", p.lakeDepth()[i])));
|
||||||
|
// Biota: density scalars (present after computeBiotaDensity()) + the discrete
|
||||||
|
// population list (present once generateBiota()/L has run).
|
||||||
|
if (sized(p.floraDensity()) && sized(p.faunaDensity()) && sized(p.fungaDensity()))
|
||||||
|
L.push_back(std::string(TextFormat("flora %.0f%% fauna %.0f%% funga %.0f%%",
|
||||||
|
p.floraDensity()[i] * 100.0, p.faunaDensity()[i] * 100.0, p.fungaDensity()[i] * 100.0)));
|
||||||
|
if (p.biotaPopulated() && i < (int)p.biota().size()) {
|
||||||
|
const CellBiota& cb = p.biota()[i];
|
||||||
|
// Each organism reads as Family (Size, Role) -- proper taxonomy, never an
|
||||||
|
// informal common name; generalists carry a biome adjective ("Forest Felidae").
|
||||||
|
// Identical archetypes in a cell aggregate to "... xN" so the list stays clean.
|
||||||
|
auto listKind = [&](const char* tag, const std::vector<Organism>& v) {
|
||||||
|
if (v.empty()) return;
|
||||||
|
std::vector<std::pair<Organism, int>> uniq; // representative + count, first-seen order
|
||||||
|
for (const Organism& o : v) {
|
||||||
|
bool found = false;
|
||||||
|
for (auto& u : uniq) if (u.first.archetype == o.archetype) { ++u.second; found = true; break; }
|
||||||
|
if (!found) uniq.push_back({o, 1});
|
||||||
|
}
|
||||||
|
std::string s = tag;
|
||||||
|
int shown = (int)std::min<size_t>(uniq.size(), 6);
|
||||||
|
for (int k = 0; k < shown; ++k) {
|
||||||
|
const BiotaArchetype& a = biotaArchetypes()[uniq[k].first.archetype];
|
||||||
|
s += (k ? ", " : " ") + organismName(uniq[k].first) +
|
||||||
|
" (" + sizeName(a.size) + ", " + roleName(a.role) + ")";
|
||||||
|
if (uniq[k].second > 1) s += TextFormat(" x%d", uniq[k].second);
|
||||||
|
}
|
||||||
|
if ((int)uniq.size() > shown) s += TextFormat(", +%d more", (int)uniq.size() - shown);
|
||||||
|
L.push_back(s);
|
||||||
|
};
|
||||||
|
listKind("Flora:", cb.flora);
|
||||||
|
listKind("Fauna:", cb.fauna);
|
||||||
|
listKind("Funga:", cb.funga);
|
||||||
|
}
|
||||||
return L;
|
return L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,9 +103,10 @@ void drawDetailPanel(const Planet& p, const std::shared_ptr<SubGrid>& sg,
|
|||||||
ty += 28;
|
ty += 28;
|
||||||
// Cramped above the subtile grid -> stop before overlapping it (the full list is
|
// Cramped above the subtile grid -> stop before overlapping it (the full list is
|
||||||
// always shown in the top-right hover panel, which has room).
|
// always shown in the top-right hover panel, which has room).
|
||||||
|
int infoMaxW = (int)(panel.x + panel.width) - tx - 10;
|
||||||
for (auto& s : cellInfo(p, macro, macroElev, macroAge)) {
|
for (auto& s : cellInfo(p, macro, macroElev, macroAge)) {
|
||||||
if (ty + 18 > (int)grid.y) break;
|
if (ty + 16 > (int)grid.y) break;
|
||||||
DrawText(s.c_str(), tx, ty, 15, Color{210, 210, 220, 255}); ty += 18;
|
ty = drawWrapped(s, tx, ty, 14, Color{210, 210, 220, 255}, infoMaxW, 16, (int)grid.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sg || sg->res < 2) return;
|
if (!sg || sg->res < 2) return;
|
||||||
@ -97,8 +155,11 @@ void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hovered < 0) { DrawText("(selected tile)", x, y, 18, Color{210, 180, 120, 255}); y += 30; }
|
if (hovered < 0) { DrawText("(selected tile)", x, y, 18, Color{210, 180, 120, 255}); y += 30; }
|
||||||
|
int maxW = (int)(r.x + r.width) - x - 14; // wrap to the panel's inner width
|
||||||
|
int maxY = (int)(r.y + r.height) - 10; // clamp to the panel bottom
|
||||||
for (auto& s : cellInfo(p, shown, p.cells[shown].elevation, p.cells[shown].geoAge)) {
|
for (auto& s : cellInfo(p, shown, p.cells[shown].elevation, p.cells[shown].geoAge)) {
|
||||||
DrawText(s.c_str(), x, y, 24, Color{215, 220, 230, 255}); y += 32;
|
y = drawWrapped(s, x, y, 20, Color{215, 220, 230, 255}, maxW, 26, maxY);
|
||||||
|
if (y > maxY) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -97,6 +97,9 @@ void Viewer::recolor() {
|
|||||||
double maxAge = 1.0; for (const auto& c : planet.cells) maxAge = std::max(maxAge, c.geoAge);
|
double maxAge = 1.0; for (const auto& c : planet.cells) maxAge = std::max(maxAge, c.geoAge);
|
||||||
const std::vector<double>& temp = planet.temperature();
|
const std::vector<double>& temp = planet.temperature();
|
||||||
const std::vector<double>& moist = planet.moisture(); // 0..1, already robustly normalized
|
const std::vector<double>& moist = planet.moisture(); // 0..1, already robustly normalized
|
||||||
|
const std::vector<double>& flora = planet.floraDensity();
|
||||||
|
const std::vector<double>& fauna = planet.faunaDensity();
|
||||||
|
const std::vector<double>& funga = planet.fungaDensity();
|
||||||
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) {
|
||||||
@ -112,6 +115,9 @@ void Viewer::recolor() {
|
|||||||
case ColorMode::Biome: vcolors[i] = biomeColor(planet.cells[i].biome); break;
|
case ColorMode::Biome: vcolors[i] = biomeColor(planet.cells[i].biome); break;
|
||||||
case ColorMode::Temperature: vcolors[i] = temp.empty() ? Color{90,90,90,255} : tempColor(temp[i]); break;
|
case ColorMode::Temperature: vcolors[i] = temp.empty() ? Color{90,90,90,255} : tempColor(temp[i]); break;
|
||||||
case ColorMode::Precip: vcolors[i] = moist.empty() ? Color{90,90,90,255} : precipColor(moist[i]); break;
|
case ColorMode::Precip: vcolors[i] = moist.empty() ? Color{90,90,90,255} : precipColor(moist[i]); break;
|
||||||
|
case ColorMode::FloraDensity: vcolors[i] = flora.empty() ? Color{90,90,90,255} : floraColor(flora[i]); break;
|
||||||
|
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255} : faunaColor(fauna[i]); break;
|
||||||
|
case ColorMode::FungaDensity: vcolors[i] = funga.empty() ? Color{90,90,90,255} : fungaColor(funga[i]); break;
|
||||||
default: vcolors[i] = elevationColor(planet.cells[i].elevation, planet.cfg.seaLevel);
|
default: vcolors[i] = elevationColor(planet.cells[i].elevation, planet.cfg.seaLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,6 +134,7 @@ void Viewer::refreshView() {
|
|||||||
if (phase3) planet.computeHydrology(); // refresh lakes/rivers for the view
|
if (phase3) planet.computeHydrology(); // refresh lakes/rivers for the view
|
||||||
planet.computeClimate(); // temperature + precipitation fields
|
planet.computeClimate(); // temperature + precipitation fields
|
||||||
planet.classifyBiomes(); // keep cell.biome current (reads the climate)
|
planet.classifyBiomes(); // keep cell.biome current (reads the climate)
|
||||||
|
planet.computeBiotaDensity(); // flora/fauna/funga density (population is on-demand)
|
||||||
recolor();
|
recolor();
|
||||||
if (settled) { // Phase 2: plates moved -> boundaries moved
|
if (settled) { // Phase 2: plates moved -> boundaries moved
|
||||||
buildBorders(planet, borderR, borders, ridgeBorders);
|
buildBorders(planet, borderR, borders, ridgeBorders);
|
||||||
@ -188,7 +195,7 @@ void Viewer::loadGame(const char* path) {
|
|||||||
if (ver >= 2) is.read(reinterpret_cast<char*>(&dr), sizeof dr);
|
if (ver >= 2) is.read(reinterpret_cast<char*>(&dr), sizeof dr);
|
||||||
if (ver >= 3) is.read(reinterpret_cast<char*>(&p3), sizeof p3);
|
if (ver >= 3) is.read(reinterpret_cast<char*>(&p3), sizeof p3);
|
||||||
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)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: per-cell biome
|
if (!planet.readState(is, ver >= 4, ver >= 7)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota
|
||||||
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
|
||||||
|
|||||||
@ -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 = 6; // v6: self-describing (text) config block; v4: +biome; v3: +phase3
|
static constexpr uint32_t SAVE_VERSION = 7; // v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3
|
||||||
const char* CONFIG_PATH = "planet.cfg";
|
const char* CONFIG_PATH = "planet.cfg";
|
||||||
const char* SAVE_PATH = "planet.save";
|
const char* SAVE_PATH = "planet.save";
|
||||||
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
||||||
|
|||||||
@ -104,6 +104,9 @@ void Viewer::handleInput() {
|
|||||||
if (IsKeyPressed(KEY_FIVE)) { mode = ColorMode::Biome; recolor(); }
|
if (IsKeyPressed(KEY_FIVE)) { mode = ColorMode::Biome; recolor(); }
|
||||||
if (IsKeyPressed(KEY_SIX)) { mode = ColorMode::Temperature; recolor(); }
|
if (IsKeyPressed(KEY_SIX)) { mode = ColorMode::Temperature; recolor(); }
|
||||||
if (IsKeyPressed(KEY_SEVEN)) { mode = ColorMode::Precip; recolor(); }
|
if (IsKeyPressed(KEY_SEVEN)) { mode = ColorMode::Precip; recolor(); }
|
||||||
|
if (IsKeyPressed(KEY_EIGHT)) { mode = ColorMode::FloraDensity; recolor(); }
|
||||||
|
if (IsKeyPressed(KEY_NINE)) { mode = ColorMode::FaunaDensity; recolor(); }
|
||||||
|
if (IsKeyPressed(KEY_ZERO)) { mode = ColorMode::FungaDensity; recolor(); }
|
||||||
if (IsKeyPressed(KEY_B)) showBorders = !showBorders;
|
if (IsKeyPressed(KEY_B)) showBorders = !showBorders;
|
||||||
if (IsKeyPressed(KEY_D)) showDrift = !showDrift;
|
if (IsKeyPressed(KEY_D)) showDrift = !showDrift;
|
||||||
if (IsKeyPressed(KEY_G)) showGrat = !showGrat;
|
if (IsKeyPressed(KEY_G)) showGrat = !showGrat;
|
||||||
@ -116,6 +119,12 @@ void Viewer::handleInput() {
|
|||||||
else { phase3PromptAt = elapsedMy + planet.cfg.phase3AfterMy; rivers.clear(); bigRivers.clear(); setStatus("Hydrology OFF"); }
|
else { phase3PromptAt = elapsedMy + planet.cfg.phase3AfterMy; rivers.clear(); bigRivers.clear(); setStatus("Hydrology OFF"); }
|
||||||
refreshView();
|
refreshView();
|
||||||
}
|
}
|
||||||
|
if (IsKeyPressed(KEY_L) && settled) { // generate / regenerate biota population
|
||||||
|
planet.generateBiota();
|
||||||
|
if (mode != ColorMode::FaunaDensity && mode != ColorMode::FungaDensity)
|
||||||
|
{ mode = ColorMode::FloraDensity; recolor(); }
|
||||||
|
setStatus("Biota generated (flora/fauna/funga)");
|
||||||
|
}
|
||||||
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
|
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
|
||||||
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
||||||
if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled)
|
if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled)
|
||||||
|
|||||||
@ -178,11 +178,11 @@ 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");
|
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp 7 precip 8 flora 9 fauna 0 funga");
|
||||||
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s]",
|
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s]",
|
||||||
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off"));
|
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off"));
|
||||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | R reseed | +/-",
|
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | R reseed | +/-",
|
||||||
phase3 ? "on" : "off"));
|
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off"));
|
||||||
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
||||||
if (!statusMsg.empty() && GetTime() < statusUntil) {
|
if (!statusMsg.empty() && GetTime() < statusUntil) {
|
||||||
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
|
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
|
||||||
|
|||||||
@ -35,6 +35,7 @@ void Planet::generate(const PlanetConfig& c) {
|
|||||||
seedInitialRelief();
|
seedInitialRelief();
|
||||||
computeClimate(); // temperature + precipitation fields (biomes read these)
|
computeClimate(); // temperature + precipitation fields (biomes read these)
|
||||||
classifyBiomes(); // give the fresh world an initial biome per cell
|
classifyBiomes(); // give the fresh world an initial biome per cell
|
||||||
|
computeBiotaDensity(); // derived flora/fauna/funga density (population is on-demand)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the icosphere and copy fixed geometry (unit direction + neighbor
|
// Build the icosphere and copy fixed geometry (unit direction + neighbor
|
||||||
@ -47,6 +48,8 @@ void Planet::buildGeometry() {
|
|||||||
cells[i].unit = sphere.positions[i];
|
cells[i].unit = sphere.positions[i];
|
||||||
cells[i].neighbors = sphere.neighbors[i];
|
cells[i].neighbors = sphere.neighbors[i];
|
||||||
}
|
}
|
||||||
|
sBiota.assign(cells.size(), {}); // empty biota population until generateBiota()
|
||||||
|
sHasBiota = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Planet::assignPlates() {
|
void Planet::assignPlates() {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#include "Vec3.hpp"
|
#include "Vec3.hpp"
|
||||||
#include "IcoSphere.hpp"
|
#include "IcoSphere.hpp"
|
||||||
#include "PlanetTypes.hpp" // Cell, Plate, SubGrid/SubCell, PlanetConfig
|
#include "PlanetTypes.hpp" // Cell, Plate, SubGrid/SubCell, PlanetConfig
|
||||||
|
#include "PlanetBiota.hpp" // BiotaKind, Organism, CellBiota
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@ -58,6 +59,20 @@ public:
|
|||||||
// fields (temperature + normalized precipitation). Derived + written back into
|
// fields (temperature + normalized precipitation). Derived + written back into
|
||||||
// cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves.
|
// cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves.
|
||||||
void classifyBiomes();
|
void classifyBiomes();
|
||||||
|
|
||||||
|
// Biota stage (flora/fauna/funga). computeBiotaDensity() builds the derived
|
||||||
|
// per-cell density scalars (0..1) each tick (like climate; not saved); call it
|
||||||
|
// after classifyBiomes(). generateBiota() does the on-demand slot/point fill of
|
||||||
|
// the discrete population into sBiota (saved) -- NOT called per tick. See
|
||||||
|
// PlanetBiota.cpp + PlanetFloraGen/FaunaGen/FungiGen.cpp.
|
||||||
|
void computeBiotaDensity();
|
||||||
|
void generateBiota();
|
||||||
|
bool biotaPopulated() const;
|
||||||
|
const std::vector<double>& floraDensity() const { return sFloraDensity; }
|
||||||
|
const std::vector<double>& faunaDensity() const { return sFaunaDensity; }
|
||||||
|
const std::vector<double>& fungaDensity() const { return sFungaDensity; }
|
||||||
|
const std::vector<CellBiota>& biota() const { return sBiota; }
|
||||||
|
|
||||||
// Derived hydrology fields (recomputed each route; not saved). Empty until
|
// Derived hydrology fields (recomputed each route; not saved). Empty until
|
||||||
// the first computeHydrology()/hydrology() call.
|
// the first computeHydrology()/hydrology() call.
|
||||||
const std::vector<double>& lakeDepth() const { return sLakeDepth; }
|
const std::vector<double>& lakeDepth() const { return sLakeDepth; }
|
||||||
@ -73,7 +88,8 @@ public:
|
|||||||
void writeState(std::ostream& os) const;
|
void writeState(std::ostream& os) const;
|
||||||
// hasBiome: whether the stream carries the per-cell biome byte (save v4+). For
|
// hasBiome: whether the stream carries the per-cell biome byte (save v4+). For
|
||||||
// older saves (v3) pass false -- biomes are reclassified after the cells load.
|
// older saves (v3) pass false -- biomes are reclassified after the cells load.
|
||||||
bool readState(std::istream& is, bool hasBiome = true);
|
// hasBiota: whether the stream carries the biota population block (save v7+).
|
||||||
|
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true);
|
||||||
|
|
||||||
// Helpers for rendering / info.
|
// Helpers for rendering / info.
|
||||||
double cellWidthMeters() const; // approx lateral cell spacing
|
double cellWidthMeters() const; // approx lateral cell spacing
|
||||||
@ -110,6 +126,17 @@ private:
|
|||||||
// Phase-3 hydrology helpers (see hydrology()).
|
// Phase-3 hydrology helpers (see hydrology()).
|
||||||
void routeFlow(); // depression-fill -> lakes, flow, discharge
|
void routeFlow(); // depression-fill -> lakes, flow, discharge
|
||||||
|
|
||||||
|
// Biota helpers (PlanetFloraGen/FaunaGen/FungiGen.cpp). compute*Density write the
|
||||||
|
// derived scalars; fill* draw the per-cell population (nbr = already-filled,
|
||||||
|
// same-biome neighbours, for regional consistency).
|
||||||
|
void computeFloraDensity();
|
||||||
|
void computeFaunaDensity();
|
||||||
|
void computeFungaDensity();
|
||||||
|
double neighbourhoodPrey(int i) const; // mean fauna density over i + neighbours
|
||||||
|
std::vector<Organism> fillFlora(int i, const std::vector<int>& nbr, uint32_t& rng);
|
||||||
|
std::vector<Organism> fillFauna(int i, const std::vector<int>& nbr, uint32_t& rng);
|
||||||
|
std::vector<Organism> fillFunga(int i, const std::vector<int>& nbr, uint32_t& rng);
|
||||||
|
|
||||||
int driftIter = 0; // counts advect() calls (gates periodic checks)
|
int driftIter = 0; // counts advect() calls (gates periodic checks)
|
||||||
int erodeIter = 0; // counts erode() calls (gates sea-level control)
|
int erodeIter = 0; // counts erode() calls (gates sea-level control)
|
||||||
std::vector<int> sPrevCount; // per-plate cell count at the previous check
|
std::vector<int> sPrevCount; // per-plate cell count at the previous check
|
||||||
@ -129,6 +156,12 @@ private:
|
|||||||
std::vector<double> sTemp, sPrecip, sMoist;
|
std::vector<double> sTemp, sPrecip, sMoist;
|
||||||
std::vector<Vec3> sWind;
|
std::vector<Vec3> sWind;
|
||||||
std::vector<int> sUpwind;
|
std::vector<int> sUpwind;
|
||||||
|
|
||||||
|
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||||
|
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
||||||
|
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
||||||
|
std::vector<CellBiota> sBiota;
|
||||||
|
bool sHasBiota = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Human-editable config file (key = value text). All PlanetConfig input
|
// Human-editable config file (key = value text). All PlanetConfig input
|
||||||
|
|||||||
244
src/sim/PlanetBiota.cpp
Normal file
244
src/sim/PlanetBiota.cpp
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
#include "Planet.hpp"
|
||||||
|
#include "PlanetBiota.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <initializer_list>
|
||||||
|
|
||||||
|
// --- Biota stage: archetype library + shared slot/point machinery + driver ----
|
||||||
|
// The per-kind environmental rules + density live in PlanetFloraGen/FaunaGen/
|
||||||
|
// FungiGen.cpp; this file holds the comprehensive archetype table, the display
|
||||||
|
// helpers, the shared draw (slot/point) routine, and the on-demand generateBiota()
|
||||||
|
// driver that fills the discrete population with neighbour-aware regional spread.
|
||||||
|
|
||||||
|
// Comprehensive archetype library. APPEND-ONLY: indices are serialized inside
|
||||||
|
// saved Organisms, so never reorder or delete entries (add new ones at the end).
|
||||||
|
const std::vector<BiotaArchetype>& biotaArchetypes() {
|
||||||
|
using K = BiotaKind; using R = EcoRole; using S = SizeClass; using B = Biome;
|
||||||
|
static const std::vector<BiotaArchetype> T = [] {
|
||||||
|
auto M = [](std::initializer_list<B> bs) {
|
||||||
|
uint32_t m = 0; for (B b : bs) m |= 1u << (unsigned)b; return m;
|
||||||
|
};
|
||||||
|
std::vector<BiotaArchetype> a;
|
||||||
|
// ---- Flora ----------------------------------------------------------
|
||||||
|
a.push_back({ "Broadleaf tree", K::Flora, R::Tree, "Magnoliopsida", "Fagales", "Fagaceae",
|
||||||
|
S::Big, M({B::Forest, B::Hills, B::Wetland, B::Grassland}), 4.0, 35.0, 0.45, false });
|
||||||
|
a.push_back({ "Conifer", K::Flora, R::Tree, "Pinopsida", "Pinales", "Pinaceae",
|
||||||
|
S::Big, M({B::Taiga, B::Forest, B::Mountains, B::Hills}), -12.0, 20.0, 0.28, false });
|
||||||
|
a.push_back({ "Tropical palm", K::Flora, R::Tree, "Liliopsida", "Arecales", "Arecaceae",
|
||||||
|
S::Medium, M({B::Forest, B::Beach, B::Wetland}), 18.0, 42.0, 0.40, false });
|
||||||
|
a.push_back({ "Tall grass", K::Flora, R::Grass, "Liliopsida", "Poales", "Poaceae",
|
||||||
|
S::Tiny, M({B::Grassland, B::Savanna, B::Wetland}), 2.0, 38.0, 0.25, false });
|
||||||
|
a.push_back({ "Steppe grass", K::Flora, R::Grass, "Liliopsida", "Poales", "Poaceae",
|
||||||
|
S::Tiny, M({B::Grassland, B::Savanna, B::Tundra, B::Desert}), -6.0, 38.0, 0.10, true });
|
||||||
|
a.push_back({ "Scrub", K::Flora, R::Shrub, "Magnoliopsida", "Lamiales", "Lamiaceae",
|
||||||
|
S::Small, M({B::Savanna, B::Grassland, B::Desert, B::Hills, B::Tundra}), -6.0, 40.0, 0.08, true });
|
||||||
|
a.push_back({ "Succulent", K::Flora, R::Succulent, "Magnoliopsida", "Caryophyllales", "Cactaceae",
|
||||||
|
S::Small, M({B::Desert, B::Savanna}), 4.0, 48.0, 0.0, false });
|
||||||
|
a.push_back({ "Reed", K::Flora, R::Aquatic, "Liliopsida", "Poales", "Cyperaceae",
|
||||||
|
S::Small, M({B::Wetland, B::Beach}), 2.0, 36.0, 0.50, false });
|
||||||
|
a.push_back({ "Mangrove", K::Flora, R::Tree, "Magnoliopsida", "Malpighiales", "Rhizophoraceae",
|
||||||
|
S::Medium, M({B::Beach, B::Wetland}), 16.0, 40.0, 0.40, false });
|
||||||
|
a.push_back({ "Fern", K::Flora, R::Shrub, "Polypodiopsida", "Polypodiales", "Polypodiaceae",
|
||||||
|
S::Tiny, M({B::Forest, B::Taiga, B::Wetland}), 0.0, 32.0, 0.40, false });
|
||||||
|
a.push_back({ "Cushion moss", K::Flora, R::Moss, "Bryopsida", "Bryales", "Bryaceae",
|
||||||
|
S::Tiny, M({B::Tundra, B::Mountains, B::Taiga}), -28.0, 10.0, 0.15, false });
|
||||||
|
a.push_back({ "Alpine wildflower", K::Flora, R::Shrub, "Magnoliopsida", "Saxifragales", "Saxifragaceae",
|
||||||
|
S::Tiny, M({B::Mountains, B::Hills, B::Tundra}), -16.0, 16.0, 0.20, true });
|
||||||
|
a.push_back({ "Giant canopy tree", K::Flora, R::Tree, "Magnoliopsida", "Malvales", "Malvaceae",
|
||||||
|
S::Huge, M({B::Forest}), 18.0, 40.0, 0.60, false }); // rainforest emergent
|
||||||
|
a.push_back({ "Berry bush", K::Flora, R::Shrub, "Magnoliopsida", "Rosales", "Rosaceae",
|
||||||
|
S::Small, M({B::Forest, B::Taiga, B::Grassland, B::Hills}), -8.0, 28.0, 0.35, false });
|
||||||
|
a.push_back({ "Wildflower", K::Flora, R::Shrub, "Magnoliopsida", "Asterales", "Asteraceae",
|
||||||
|
S::Tiny, M({B::Grassland, B::Savanna, B::Hills, B::Forest}), 2.0, 35.0, 0.20, false });
|
||||||
|
a.push_back({ "Water lily", K::Flora, R::Aquatic, "Magnoliopsida", "Nymphaeales", "Nymphaeaceae",
|
||||||
|
S::Tiny, M({B::Wetland}), 8.0, 36.0, 0.60, false });
|
||||||
|
// ---- Fauna (named by Family; `name` is the family vernacular, display uses
|
||||||
|
// the Family rank -- e.g. Felidae, never "cat") -------------------
|
||||||
|
a.push_back({ "Murid", K::Fauna, R::Herbivore, "Mammalia", "Rodentia", "Muridae",
|
||||||
|
S::Tiny, M({B::Grassland, B::Savanna, B::Forest, B::Taiga, B::Desert,
|
||||||
|
B::Wetland, B::Tundra, B::Hills, B::Mountains, B::Beach}), -12.0, 42.0, 0.0, true });
|
||||||
|
a.push_back({ "Leporid", K::Fauna, R::Herbivore, "Mammalia", "Lagomorpha", "Leporidae",
|
||||||
|
S::Small, M({B::Grassland, B::Savanna, B::Tundra, B::Forest, B::Desert}), -16.0, 38.0, 0.05, true });
|
||||||
|
a.push_back({ "Cervid", K::Fauna, R::Herbivore, "Mammalia", "Artiodactyla", "Cervidae",
|
||||||
|
S::Medium, M({B::Forest, B::Grassland, B::Savanna, B::Taiga, B::Hills}), -12.0, 35.0, 0.20, false });
|
||||||
|
a.push_back({ "Bovid", K::Fauna, R::Herbivore, "Mammalia", "Artiodactyla", "Bovidae",
|
||||||
|
S::Big, M({B::Grassland, B::Savanna}), 0.0, 40.0, 0.18, false });
|
||||||
|
a.push_back({ "Proboscid", K::Fauna, R::Herbivore, "Mammalia", "Proboscidea", "Elephantidae",
|
||||||
|
S::Huge, M({B::Savanna, B::Grassland, B::Forest}), 10.0, 42.0, 0.30, false });
|
||||||
|
a.push_back({ "Suid", K::Fauna, R::Omnivore, "Mammalia", "Artiodactyla", "Suidae",
|
||||||
|
S::Medium, M({B::Forest, B::Wetland, B::Grassland, B::Hills}), -2.0, 36.0, 0.25, false });
|
||||||
|
a.push_back({ "Ursid", K::Fauna, R::Omnivore, "Mammalia", "Carnivora", "Ursidae",
|
||||||
|
S::Big, M({B::Forest, B::Taiga, B::Mountains, B::Tundra}), -22.0, 26.0, 0.20, false });
|
||||||
|
a.push_back({ "Cercopithecid", K::Fauna, R::Omnivore, "Mammalia", "Primates", "Cercopithecidae",
|
||||||
|
S::Medium, M({B::Forest, B::Wetland}), 15.0, 40.0, 0.45, false });
|
||||||
|
a.push_back({ "Felid (small)", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Felidae",
|
||||||
|
S::Small, M({B::Forest, B::Grassland, B::Savanna, B::Desert, B::Wetland, B::Hills}), 0.0, 42.0, 0.10, true });
|
||||||
|
a.push_back({ "Felid (large)", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Felidae",
|
||||||
|
S::Big, M({B::Savanna, B::Grassland, B::Forest, B::Hills}), 5.0, 42.0, 0.18, true });
|
||||||
|
a.push_back({ "Canid", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Canidae",
|
||||||
|
S::Medium, M({B::Grassland, B::Savanna, B::Forest, B::Taiga, B::Tundra,
|
||||||
|
B::Desert, B::Hills, B::Mountains}), -26.0, 40.0, 0.05, true });
|
||||||
|
a.push_back({ "Mustelid", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Mustelidae",
|
||||||
|
S::Tiny, M({B::Forest, B::Taiga, B::Wetland, B::Grassland, B::Tundra}), -22.0, 30.0, 0.15, false });
|
||||||
|
a.push_back({ "Varanid", K::Fauna, R::Carnivore, "Reptilia", "Squamata", "Varanidae",
|
||||||
|
S::Small, M({B::Desert, B::Savanna}), 12.0, 50.0, 0.0, false });
|
||||||
|
a.push_back({ "Ranid", K::Fauna, R::Omnivore, "Amphibia", "Anura", "Ranidae",
|
||||||
|
S::Tiny, M({B::Wetland, B::Forest, B::Beach}), 5.0, 35.0, 0.50, false });
|
||||||
|
a.push_back({ "Phasianid", K::Fauna, R::Herbivore, "Aves", "Galliformes", "Phasianidae",
|
||||||
|
S::Medium, M({B::Grassland, B::Savanna, B::Tundra, B::Desert, B::Forest}), -12.0, 42.0, 0.05, true });
|
||||||
|
a.push_back({ "Accipitrid", K::Fauna, R::Carnivore, "Aves", "Accipitriformes", "Accipitridae",
|
||||||
|
S::Small, M({B::Grassland, B::Savanna, B::Forest, B::Mountains, B::Tundra,
|
||||||
|
B::Desert, B::Hills, B::Wetland}), -16.0, 42.0, 0.0, true });
|
||||||
|
// ---- Funga ----------------------------------------------------------
|
||||||
|
a.push_back({ "Decomposer mushroom", K::Funga, R::Decomposer, "Agaricomycetes", "Agaricales", "Agaricaceae",
|
||||||
|
S::Tiny, M({B::Forest, B::Taiga, B::Wetland, B::Grassland, B::Hills}), -6.0, 32.0, 0.35, true });
|
||||||
|
a.push_back({ "Mould", K::Funga, R::Decomposer, "Eurotiomycetes", "Eurotiales", "Aspergillaceae",
|
||||||
|
S::Tiny, M({B::Wetland, B::Forest, B::Beach}), 0.0, 40.0, 0.45, false });
|
||||||
|
a.push_back({ "Mycorrhizal fungus", K::Funga, R::Mycorrhizal, "Agaricomycetes", "Boletales", "Boletaceae",
|
||||||
|
S::Tiny, M({B::Forest, B::Taiga, B::Hills}), -12.0, 30.0, 0.30, false });
|
||||||
|
a.push_back({ "Bracket fungus", K::Funga, R::Decomposer, "Agaricomycetes", "Polyporales", "Polyporaceae",
|
||||||
|
S::Small, M({B::Forest, B::Taiga}), -6.0, 28.0, 0.40, false });
|
||||||
|
a.push_back({ "Lichen", K::Funga, R::Lichen, "Lecanoromycetes", "Lecanorales", "Parmeliaceae",
|
||||||
|
S::Tiny, M({B::Tundra, B::Mountains, B::Taiga, B::Beach}), -32.0, 16.0, 0.10, true });
|
||||||
|
a.push_back({ "Slime mould", K::Funga, R::Decomposer, "Myxomycetes", "Physarales", "Physaraceae",
|
||||||
|
S::Tiny, M({B::Forest, B::Wetland}), 5.0, 32.0, 0.50, false });
|
||||||
|
a.push_back({ "Parasitic blight", K::Funga, R::Parasite, "Sordariomycetes", "Hypocreales", "Clavicipitaceae",
|
||||||
|
S::Tiny, M({B::Forest, B::Grassland, B::Savanna, B::Wetland}), 0.0, 38.0, 0.30, false });
|
||||||
|
a.push_back({ "Puffball", K::Funga, R::Decomposer, "Agaricomycetes", "Agaricales", "Lycoperdaceae",
|
||||||
|
S::Small, M({B::Grassland, B::Savanna, B::Tundra}), -10.0, 32.0, 0.20, true });
|
||||||
|
return a;
|
||||||
|
}();
|
||||||
|
return T;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pointCost(SizeClass s) { return (int)s + 1; } // Tiny=1 .. Huge=5
|
||||||
|
|
||||||
|
const char* sizeName(SizeClass s) {
|
||||||
|
switch (s) { case SizeClass::Tiny: return "Tiny"; case SizeClass::Small: return "Small";
|
||||||
|
case SizeClass::Medium: return "Medium"; case SizeClass::Big: return "Big";
|
||||||
|
case SizeClass::Huge: return "Huge"; }
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* roleName(EcoRole r) {
|
||||||
|
switch (r) {
|
||||||
|
case EcoRole::Grass: return "Grass"; case EcoRole::Shrub: return "Shrub";
|
||||||
|
case EcoRole::Tree: return "Tree"; case EcoRole::Succulent: return "Succulent";
|
||||||
|
case EcoRole::Moss: return "Moss"; case EcoRole::Aquatic: return "Aquatic";
|
||||||
|
case EcoRole::Herbivore: return "Herbivore"; case EcoRole::Carnivore: return "Carnivore";
|
||||||
|
case EcoRole::Omnivore: return "Omnivore"; case EcoRole::Decomposer: return "Decomposer";
|
||||||
|
case EcoRole::Mycorrhizal: return "Mycorrhizal"; case EcoRole::Lichen: return "Lichen";
|
||||||
|
case EcoRole::Parasite: return "Parasite";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* kindName(BiotaKind k) {
|
||||||
|
switch (k) { case BiotaKind::Flora: return "Flora"; case BiotaKind::Fauna: return "Fauna";
|
||||||
|
case BiotaKind::Funga: return "Funga"; }
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Adjective prefix for generalist display names ("Desert rodent" etc.).
|
||||||
|
const char* biomeAdjective(Biome b) {
|
||||||
|
switch (b) {
|
||||||
|
case Biome::Desert: return "Desert ";
|
||||||
|
case Biome::Forest: return "Forest ";
|
||||||
|
case Biome::Wetland: return "Swamp ";
|
||||||
|
case Biome::Savanna: return "Savanna ";
|
||||||
|
case Biome::Grassland: return "Steppe ";
|
||||||
|
case Biome::Taiga: return "Boreal ";
|
||||||
|
case Biome::Tundra: return "Tundra ";
|
||||||
|
case Biome::Mountains: return "Mountain ";
|
||||||
|
case Biome::Hills: return "Hill ";
|
||||||
|
case Biome::Beach: return "Coastal ";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display label is built from the TAXONOMY (Family), not an informal common name --
|
||||||
|
// e.g. a Felidae of Big size reads "Felidae", never "big cat" (cat is a common name,
|
||||||
|
// Felidae is the family). Generalists get a biome adjective ("Forest Felidae").
|
||||||
|
std::string organismName(const Organism& o) {
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
if (o.archetype >= AR.size()) return "?";
|
||||||
|
const BiotaArchetype& a = AR[o.archetype];
|
||||||
|
std::string fam = a.familyName;
|
||||||
|
return a.generalist ? std::string(biomeAdjective((Biome)o.biome)) + fam : fam;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full Class > Order > Family taxonomy string (the "tree" path) for an organism.
|
||||||
|
std::string organismTaxonomy(const Organism& o) {
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
if (o.archetype >= AR.size()) return "?";
|
||||||
|
const BiotaArchetype& a = AR[o.archetype];
|
||||||
|
return std::string(a.className) + " > " + a.orderName + " > " + a.familyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
double biotaSuitability(const BiotaArchetype& a, Biome biome, double temp, double moist) {
|
||||||
|
if (!(a.biomeMask & (1u << (unsigned)biome))) return 0.0;
|
||||||
|
double tf = 1.0;
|
||||||
|
if (temp < a.tempMin) tf = std::max(0.0, 1.0 - (a.tempMin - temp) / 8.0);
|
||||||
|
else if (temp > a.tempMax) tf = std::max(0.0, 1.0 - (temp - a.tempMax) / 8.0);
|
||||||
|
double mf = (moist >= a.moistMin) ? 1.0 : std::max(0.0, 1.0 - (a.moistMin - moist) / 0.2);
|
||||||
|
return tf * mf;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Organism> biotaDraw(const std::vector<BiotaCandidate>& cands, Biome biome,
|
||||||
|
int maxSlots, int maxPoints, uint32_t& rng) {
|
||||||
|
std::vector<Organism> out;
|
||||||
|
if (cands.empty() || maxSlots <= 0 || maxPoints <= 0) return out;
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
double total = 0.0; for (const auto& c : cands) total += c.weight;
|
||||||
|
if (total <= 0.0) return out;
|
||||||
|
int minCost = 99; for (const auto& c : cands) minCost = std::min(minCost, pointCost(AR[c.arch].size));
|
||||||
|
int slots = maxSlots, pts = maxPoints, guard = maxSlots * 8;
|
||||||
|
while (slots > 0 && pts >= minCost && guard-- > 0) {
|
||||||
|
double r = biotaRndf(rng) * total, acc = 0.0; int pick = cands.back().arch;
|
||||||
|
for (const auto& c : cands) { acc += c.weight; if (r <= acc) { pick = c.arch; break; } }
|
||||||
|
int cost = pointCost(AR[pick].size);
|
||||||
|
if (cost <= pts) { out.push_back({ (uint16_t)pick, (uint8_t)biome }); --slots; pts -= cost; }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Planet driver -----------------------------------------------------------
|
||||||
|
|
||||||
|
void Planet::computeBiotaDensity() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if ((int)sTemp.size() != n || (int)sMoist.size() != n) computeClimate(); // safety
|
||||||
|
computeFloraDensity();
|
||||||
|
computeFaunaDensity(); // reads sFloraDensity
|
||||||
|
computeFungaDensity(); // reads sFloraDensity
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Planet::biotaPopulated() const { return sHasBiota; }
|
||||||
|
|
||||||
|
// On-demand: fill the discrete slot/point population. Deterministic (separate RNG
|
||||||
|
// seeded from cfg.seed, so it never perturbs the tectonic stream). Regional
|
||||||
|
// consistency comes from biasing each cell's draws toward archetypes already
|
||||||
|
// placed in its already-filled, same-biome neighbours (single index-ordered pass).
|
||||||
|
void Planet::generateBiota() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if ((int)sFloraDensity.size() != n || (int)sFaunaDensity.size() != n ||
|
||||||
|
(int)sFungaDensity.size() != n) computeBiotaDensity();
|
||||||
|
sBiota.assign(n, {});
|
||||||
|
sHasBiota = false;
|
||||||
|
const double sea = cfg.seaLevel;
|
||||||
|
uint32_t rng = cfg.seed ? (cfg.seed ^ 0xB107A5EDu) : 0xB107A5EDu;
|
||||||
|
std::vector<char> done(n, 0);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) { done[i] = 1; continue; }
|
||||||
|
std::vector<int> nbr; // already-filled neighbours sharing this biome
|
||||||
|
for (int j : cells[i].neighbors)
|
||||||
|
if (done[j] && cells[j].biome == cells[i].biome) nbr.push_back(j);
|
||||||
|
sBiota[i].flora = fillFlora(i, nbr, rng);
|
||||||
|
sBiota[i].fauna = fillFauna(i, nbr, rng);
|
||||||
|
sBiota[i].funga = fillFunga(i, nbr, rng);
|
||||||
|
if (!sBiota[i].flora.empty() || !sBiota[i].fauna.empty() || !sBiota[i].funga.empty())
|
||||||
|
sHasBiota = true;
|
||||||
|
done[i] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/sim/PlanetBiota.hpp
Normal file
81
src/sim/PlanetBiota.hpp
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "PlanetTypes.hpp" // Biome
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// --- Biota stage: Flora / Fauna / Funga --------------------------------------
|
||||||
|
// Two layers (see docs/fauna-flora-plan.md + docs/fauna_generation_plan.md):
|
||||||
|
// * density scalars (0..1) per kind, derived from climate each tick (drive the
|
||||||
|
// colour views) -- "how full a cell gets";
|
||||||
|
// * a discrete slot/point POPULATION of broad archetypes, generated on demand
|
||||||
|
// and saved -- "what fills it".
|
||||||
|
// Raylib-free, deterministic. Geometry never moves: biota is per-cell data flowed
|
||||||
|
// over the fixed grid (see CLAUDE.md core principle).
|
||||||
|
|
||||||
|
enum class BiotaKind : uint8_t { Flora, Fauna, Funga };
|
||||||
|
|
||||||
|
// Size category -> point cost (Tiny=1 .. Huge=5). Keep order stable (serialized
|
||||||
|
// indirectly via the archetype table).
|
||||||
|
enum class SizeClass : uint8_t { Tiny, Small, Medium, Big, Huge };
|
||||||
|
|
||||||
|
// Functional / trophic role. Grouped by kind; append new roles at the end.
|
||||||
|
enum class EcoRole : uint8_t {
|
||||||
|
Grass, Shrub, Tree, Succulent, Moss, Aquatic, // flora growth forms
|
||||||
|
Herbivore, Carnivore, Omnivore, // fauna trophic roles
|
||||||
|
Decomposer, Mycorrhizal, Lichen, Parasite // funga roles
|
||||||
|
};
|
||||||
|
|
||||||
|
// A broad biological group (NOT a real species). The world is populated by
|
||||||
|
// drawing these into cells; a "generalist" gets a biome adjective at display time
|
||||||
|
// ("Desert rodent" / "Forest rodent"), per the fauna doc.
|
||||||
|
struct BiotaArchetype {
|
||||||
|
const char* name;
|
||||||
|
BiotaKind kind;
|
||||||
|
EcoRole role;
|
||||||
|
const char* className; // taxonomy labels (broad, illustrative)
|
||||||
|
const char* orderName;
|
||||||
|
const char* familyName;
|
||||||
|
SizeClass size;
|
||||||
|
uint32_t biomeMask; // OR of (1u << (int)Biome) for the biomes it inhabits
|
||||||
|
double tempMin, tempMax; // climate tolerance, deg C
|
||||||
|
double moistMin; // min normalized moisture (0..1)
|
||||||
|
bool generalist; // display name gets a biome adjective
|
||||||
|
};
|
||||||
|
|
||||||
|
// One placed organism (saved). `archetype` indexes the global table; `biome` is
|
||||||
|
// the cell's biome at placement time (for the display adjective).
|
||||||
|
struct Organism { uint16_t archetype; uint8_t biome; };
|
||||||
|
|
||||||
|
// Per-cell population, one list per kind (saved as part of the planet state).
|
||||||
|
struct CellBiota { std::vector<Organism> flora, fauna, funga; };
|
||||||
|
|
||||||
|
// Global archetype table (append-only / never reorder -- indices are serialized,
|
||||||
|
// same convention as the Biome enum). Defined in PlanetBiota.cpp.
|
||||||
|
const std::vector<BiotaArchetype>& biotaArchetypes();
|
||||||
|
|
||||||
|
// Helpers (PlanetBiota.cpp).
|
||||||
|
int pointCost(SizeClass s); // 1..5
|
||||||
|
const char* sizeName(SizeClass s);
|
||||||
|
const char* roleName(EcoRole r);
|
||||||
|
const char* kindName(BiotaKind k);
|
||||||
|
std::string organismName(const Organism& o); // family-based label (generalists get a biome adjective)
|
||||||
|
std::string organismTaxonomy(const Organism& o); // "Class > Order > Family" path
|
||||||
|
|
||||||
|
// Environmental suitability of an archetype in a cell (0 = can't live here).
|
||||||
|
double biotaSuitability(const BiotaArchetype& a, Biome biome, double temp, double moist);
|
||||||
|
|
||||||
|
// One candidate archetype + its draw weight for a cell.
|
||||||
|
struct BiotaCandidate { int arch; double weight; };
|
||||||
|
|
||||||
|
// Slot/point fill: draw archetypes weighted by `weight` until slots full OR points
|
||||||
|
// exhausted OR nothing affordable remains. Deterministic given `rng`.
|
||||||
|
std::vector<Organism> biotaDraw(const std::vector<BiotaCandidate>& cands, Biome biome,
|
||||||
|
int maxSlots, int maxPoints, uint32_t& rng);
|
||||||
|
|
||||||
|
// Local xorshift RNG (kept separate from Planet::rngState so generating biota
|
||||||
|
// never perturbs tectonic determinism).
|
||||||
|
inline uint32_t biotaRnd(uint32_t& s) {
|
||||||
|
uint32_t x = s; x ^= x << 13; x ^= x >> 17; x ^= x << 5; s = x; return x;
|
||||||
|
}
|
||||||
|
inline double biotaRndf(uint32_t& s) { return (biotaRnd(s) & 0xFFFFFF) / double(0x1000000); }
|
||||||
66
src/sim/PlanetFaunaGen.cpp
Normal file
66
src/sim/PlanetFaunaGen.cpp
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#include "Planet.hpp"
|
||||||
|
#include "PlanetBiota.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
// --- Biota: Fauna (animals) --------------------------------------------------
|
||||||
|
// Density tracks herbivore carrying capacity (~ flora productivity); carnivores
|
||||||
|
// are gated on local prey abundance so predators concentrate in the richest belts
|
||||||
|
// (an energy pyramid). fillFauna() places herbivores/omnivores from the prey pool,
|
||||||
|
// then carnivores only where the neighbourhood prey clears bioCarnPreyMin.
|
||||||
|
|
||||||
|
void Planet::computeFaunaDensity() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if ((int)sFloraDensity.size() != n) computeFloraDensity();
|
||||||
|
sFaunaDensity.assign(n, 0.0);
|
||||||
|
const double sea = cfg.seaLevel;
|
||||||
|
const double prod = cfg.bioFaunaProductivity;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) continue;
|
||||||
|
// Herbivore capacity scales with plant productivity; overall animal
|
||||||
|
// richness mostly tracks it (herbivores are the bulk of the biomass).
|
||||||
|
sFaunaDensity[i] = std::clamp(sFloraDensity[i] * prod, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local prey abundance = mean fauna density over the cell + its neighbours.
|
||||||
|
double Planet::neighbourhoodPrey(int i) const {
|
||||||
|
double sum = sFaunaDensity[i]; int c = 1;
|
||||||
|
for (int j : cells[i].neighbors) { sum += sFaunaDensity[j]; ++c; }
|
||||||
|
return sum / c;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Organism> Planet::fillFauna(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
||||||
|
double dens = sFaunaDensity[i];
|
||||||
|
int pts = (int)std::lround(cfg.bioFaunaPoints * dens);
|
||||||
|
if (pts <= 0) return {};
|
||||||
|
Biome biome = cells[i].biome;
|
||||||
|
double temp = sTemp[i], moist = sMoist[i];
|
||||||
|
// Predators present only where prey is abundant; ramp scales their weight.
|
||||||
|
double prey = neighbourhoodPrey(i);
|
||||||
|
bool allowCarn = prey > cfg.bioCarnPreyMin;
|
||||||
|
double carnRamp = allowCarn
|
||||||
|
? std::clamp((prey - cfg.bioCarnPreyMin) / std::max(1e-6, 1.0 - cfg.bioCarnPreyMin)
|
||||||
|
* cfg.bioCarnScale, 0.0, 1.0)
|
||||||
|
: 0.0;
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
std::vector<BiotaCandidate> cands;
|
||||||
|
for (int a = 0; a < (int)AR.size(); ++a) {
|
||||||
|
if (AR[a].kind != BiotaKind::Fauna) continue;
|
||||||
|
bool carn = AR[a].role == EcoRole::Carnivore;
|
||||||
|
if (carn && !allowCarn) continue;
|
||||||
|
double s = biotaSuitability(AR[a], biome, temp, moist);
|
||||||
|
if (s <= 0.0) continue;
|
||||||
|
if (carn) s *= carnRamp; // rarer predators in poorer regions
|
||||||
|
if (s <= 0.0) continue;
|
||||||
|
double pres = 0.0;
|
||||||
|
if (!nbr.empty()) {
|
||||||
|
int hit = 0;
|
||||||
|
for (int j : nbr)
|
||||||
|
for (const Organism& o : sBiota[j].fauna) if (o.archetype == a) { ++hit; break; }
|
||||||
|
pres = (double)hit / nbr.size();
|
||||||
|
}
|
||||||
|
cands.push_back({ a, s * (1.0 + cfg.bioRegionBonus * pres) });
|
||||||
|
}
|
||||||
|
return biotaDraw(cands, biome, cfg.bioFaunaSlots, pts, rng);
|
||||||
|
}
|
||||||
48
src/sim/PlanetFloraGen.cpp
Normal file
48
src/sim/PlanetFloraGen.cpp
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#include "Planet.hpp"
|
||||||
|
#include "PlanetBiota.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
// --- Biota: Flora (plants) ---------------------------------------------------
|
||||||
|
// Density = an NPP-style Liebig minimum of a temperature factor and a moisture
|
||||||
|
// factor (lush warm-wet, ~0 in ice/desert/alpine), 0 on water. fillFlora() draws
|
||||||
|
// plant archetypes suited to the cell's biome/climate into its slot/point budget.
|
||||||
|
|
||||||
|
void Planet::computeFloraDensity() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if ((int)sTemp.size() != n || (int)sMoist.size() != n) computeClimate();
|
||||||
|
sFloraDensity.assign(n, 0.0);
|
||||||
|
const double sea = cfg.seaLevel;
|
||||||
|
const double tMin = cfg.bioVegTempMin, tOpt = cfg.bioVegTempOpt;
|
||||||
|
const double mRef = std::max(1e-6, cfg.bioVegMoistRef);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) continue;
|
||||||
|
double tf = std::clamp((sTemp[i] - tMin) / std::max(1e-6, tOpt - tMin), 0.0, 1.0);
|
||||||
|
double mf = std::clamp(sMoist[i] / mRef, 0.0, 1.0);
|
||||||
|
sFloraDensity[i] = std::min(tf, mf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Organism> Planet::fillFlora(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
||||||
|
double dens = sFloraDensity[i];
|
||||||
|
int pts = (int)std::lround(cfg.bioFloraPoints * dens);
|
||||||
|
if (pts <= 0) return {};
|
||||||
|
Biome biome = cells[i].biome;
|
||||||
|
double temp = sTemp[i], moist = sMoist[i];
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
std::vector<BiotaCandidate> cands;
|
||||||
|
for (int a = 0; a < (int)AR.size(); ++a) {
|
||||||
|
if (AR[a].kind != BiotaKind::Flora) continue;
|
||||||
|
double s = biotaSuitability(AR[a], biome, temp, moist);
|
||||||
|
if (s <= 0.0) continue;
|
||||||
|
double pres = 0.0; // regional consistency: same archetype next door
|
||||||
|
if (!nbr.empty()) {
|
||||||
|
int hit = 0;
|
||||||
|
for (int j : nbr)
|
||||||
|
for (const Organism& o : sBiota[j].flora) if (o.archetype == a) { ++hit; break; }
|
||||||
|
pres = (double)hit / nbr.size();
|
||||||
|
}
|
||||||
|
cands.push_back({ a, s * (1.0 + cfg.bioRegionBonus * pres) });
|
||||||
|
}
|
||||||
|
return biotaDraw(cands, biome, cfg.bioFloraSlots, pts, rng);
|
||||||
|
}
|
||||||
53
src/sim/PlanetFungiGen.cpp
Normal file
53
src/sim/PlanetFungiGen.cpp
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#include "Planet.hpp"
|
||||||
|
#include "PlanetBiota.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
// --- Biota: Funga (fungi) ----------------------------------------------------
|
||||||
|
// Fungi are NOT plants but share the environmental + slot/point machinery (see the
|
||||||
|
// user's note). Their rules differ from flora: moisture-led and organic-matter-led
|
||||||
|
// (they feed on dead/living flora), with a colder tolerance than plants -- so they
|
||||||
|
// thrive in moist forests/wetlands, persist in cold shade where flora thins, and
|
||||||
|
// fall to ~0 in hot dry deserts.
|
||||||
|
|
||||||
|
void Planet::computeFungaDensity() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if ((int)sFloraDensity.size() != n) computeFloraDensity();
|
||||||
|
sFungaDensity.assign(n, 0.0);
|
||||||
|
const double sea = cfg.seaLevel;
|
||||||
|
const double mRef = std::max(1e-6, cfg.bioFungaMoistRef);
|
||||||
|
const double tMin = cfg.bioFungaTempMin, w = std::clamp(cfg.bioFungaFloraWeight, 0.0, 1.0);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) continue;
|
||||||
|
double mf = std::clamp(sMoist[i] / mRef, 0.0, 1.0);
|
||||||
|
// Cold-tolerant: full down to tMin, tapering below it.
|
||||||
|
double tf = (sTemp[i] >= tMin) ? 1.0 : std::clamp(1.0 - (tMin - sTemp[i]) / 10.0, 0.0, 1.0);
|
||||||
|
// Substrate: leans on flora (organic matter) but always some dead matter.
|
||||||
|
double organic = w * sFloraDensity[i] + (1.0 - w);
|
||||||
|
sFungaDensity[i] = std::clamp(std::min(mf, organic) * tf, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Organism> Planet::fillFunga(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
||||||
|
double dens = sFungaDensity[i];
|
||||||
|
int pts = (int)std::lround(cfg.bioFungaPoints * dens);
|
||||||
|
if (pts <= 0) return {};
|
||||||
|
Biome biome = cells[i].biome;
|
||||||
|
double temp = sTemp[i], moist = sMoist[i];
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
std::vector<BiotaCandidate> cands;
|
||||||
|
for (int a = 0; a < (int)AR.size(); ++a) {
|
||||||
|
if (AR[a].kind != BiotaKind::Funga) continue;
|
||||||
|
double s = biotaSuitability(AR[a], biome, temp, moist);
|
||||||
|
if (s <= 0.0) continue;
|
||||||
|
double pres = 0.0;
|
||||||
|
if (!nbr.empty()) {
|
||||||
|
int hit = 0;
|
||||||
|
for (int j : nbr)
|
||||||
|
for (const Organism& o : sBiota[j].funga) if (o.archetype == a) { ++hit; break; }
|
||||||
|
pres = (double)hit / nbr.size();
|
||||||
|
}
|
||||||
|
cands.push_back({ a, s * (1.0 + cfg.bioRegionBonus * pres) });
|
||||||
|
}
|
||||||
|
return biotaDraw(cands, biome, cfg.bioFungaSlots, pts, rng);
|
||||||
|
}
|
||||||
@ -29,9 +29,14 @@
|
|||||||
D(biomeLakeMinDepth) \
|
D(biomeLakeMinDepth) \
|
||||||
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
|
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
|
||||||
D(climateOroRefHeight) D(climateContinentality) \
|
D(climateOroRefHeight) D(climateContinentality) \
|
||||||
|
D(bioVegTempMin) D(bioVegTempOpt) D(bioVegMoistRef) D(bioFaunaProductivity) \
|
||||||
|
D(bioCarnPreyMin) D(bioCarnScale) D(bioFungaMoistRef) D(bioFungaFloraWeight) \
|
||||||
|
D(bioFungaTempMin) D(bioRegionBonus) \
|
||||||
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(climateWindPasses) I(climateMoistureSmooth) \
|
||||||
|
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||||
|
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) \
|
||||||
U(seed)
|
U(seed)
|
||||||
|
|
||||||
// Write all config fields as `key = value` lines (no header). Shared by the text
|
// Write all config fields as `key = value` lines (no header). Shared by the text
|
||||||
@ -158,6 +163,16 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.climateOrographic, 0.0, 50.0, "climateOrographic"));
|
E(rng(cfg.climateOrographic, 0.0, 50.0, "climateOrographic"));
|
||||||
E(rng(cfg.climateOroRefHeight, 1.0, 1.0e5, "climateOroRefHeight"));
|
E(rng(cfg.climateOroRefHeight, 1.0, 1.0e5, "climateOroRefHeight"));
|
||||||
E(rng(cfg.climateContinentality, 0.0, 1.0, "climateContinentality"));
|
E(rng(cfg.climateContinentality, 0.0, 1.0, "climateContinentality"));
|
||||||
|
E(rng(cfg.bioVegTempMin, -40.0, 30.0, "bioVegTempMin"));
|
||||||
|
E(rng(cfg.bioVegTempOpt, -20.0, 50.0, "bioVegTempOpt"));
|
||||||
|
E(rng(cfg.bioVegMoistRef, 0.01, 1.0, "bioVegMoistRef"));
|
||||||
|
E(rng(cfg.bioFaunaProductivity, 0.0, 2.0, "bioFaunaProductivity"));
|
||||||
|
E(rng(cfg.bioCarnPreyMin, 0.0, 1.0, "bioCarnPreyMin"));
|
||||||
|
E(rng(cfg.bioCarnScale, 0.0, 5.0, "bioCarnScale"));
|
||||||
|
E(rng(cfg.bioFungaMoistRef, 0.01, 1.0, "bioFungaMoistRef"));
|
||||||
|
E(rng(cfg.bioFungaFloraWeight, 0.0, 1.0, "bioFungaFloraWeight"));
|
||||||
|
E(rng(cfg.bioFungaTempMin, -50.0, 20.0, "bioFungaTempMin"));
|
||||||
|
E(rng(cfg.bioRegionBonus, 0.0, 10.0, "bioRegionBonus"));
|
||||||
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"));
|
||||||
@ -169,6 +184,12 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(irng(cfg.seaLevelEvery, 1, 100000, "seaLevelEvery"));
|
E(irng(cfg.seaLevelEvery, 1, 100000, "seaLevelEvery"));
|
||||||
E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses"));
|
E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses"));
|
||||||
E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth"));
|
E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth"));
|
||||||
|
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
||||||
|
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
||||||
|
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
||||||
|
E(irng(cfg.bioFloraPoints, 1, 100000, "bioFloraPoints"));
|
||||||
|
E(irng(cfg.bioFaunaPoints, 1, 100000, "bioFaunaPoints"));
|
||||||
|
E(irng(cfg.bioFungaPoints, 1, 100000, "bioFungaPoints"));
|
||||||
|
|
||||||
if (cfg.oceanBase >= cfg.continentBase)
|
if (cfg.oceanBase >= cfg.continentBase)
|
||||||
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
||||||
@ -227,9 +248,18 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
writeVec(os, sPrevCount);
|
writeVec(os, sPrevCount);
|
||||||
writeVec(os, sStaleStreak);
|
writeVec(os, sStaleStreak);
|
||||||
writeVec(os, sFreePlateIds);
|
writeVec(os, sFreePlateIds);
|
||||||
|
// v7: discrete biota population (sBiota). A flag byte gates the block so a
|
||||||
|
// not-yet-populated world stays compact; otherwise three Organism lists per cell.
|
||||||
|
uint8_t hasBio = sHasBiota ? 1 : 0; writePod(os, hasBio);
|
||||||
|
if (hasBio) {
|
||||||
|
uint64_t nb = sBiota.size(); writePod(os, nb);
|
||||||
|
for (const CellBiota& cb : sBiota) {
|
||||||
|
writeVec(os, cb.flora); writeVec(os, cb.fauna); writeVec(os, cb.funga);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Planet::readState(std::istream& is, bool hasBiome) {
|
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota) {
|
||||||
// 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
|
||||||
// current defaults. The length guard rejects pre-v6 (raw-POD-config) saves.
|
// current defaults. The length guard rejects pre-v6 (raw-POD-config) saves.
|
||||||
@ -259,5 +289,20 @@ bool Planet::readState(std::istream& is, bool hasBiome) {
|
|||||||
readVec(is, sStaleStreak);
|
readVec(is, sStaleStreak);
|
||||||
readVec(is, sFreePlateIds);
|
readVec(is, sFreePlateIds);
|
||||||
if (!hasBiome) classifyBiomes(); // old (v3) save: reclassify from loaded state
|
if (!hasBiome) classifyBiomes(); // old (v3) save: reclassify from loaded state
|
||||||
|
// v7: discrete biota population. buildGeometry() already sized sBiota empty;
|
||||||
|
// older saves (hasBiota=false) just keep the empty population (press L to fill).
|
||||||
|
sHasBiota = false;
|
||||||
|
if (hasBiota) {
|
||||||
|
uint8_t hasBio = 0; readPod(is, hasBio);
|
||||||
|
if (hasBio) {
|
||||||
|
uint64_t nb = 0; readPod(is, nb);
|
||||||
|
if (!is || nb != sBiota.size()) return false;
|
||||||
|
for (CellBiota& cb : sBiota) {
|
||||||
|
readVec(is, cb.flora); readVec(is, cb.fauna); readVec(is, cb.funga);
|
||||||
|
if (!cb.flora.empty() || !cb.fauna.empty() || !cb.funga.empty()) sHasBiota = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
computeBiotaDensity(); // derived density scalars for the colour views
|
||||||
return (bool)is;
|
return (bool)is;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -187,4 +187,24 @@ struct PlanetConfig {
|
|||||||
double climateContinentality = 0.05; // moisture lost per land cell crossed (dries interiors)
|
double climateContinentality = 0.05; // moisture lost per land cell crossed (dries interiors)
|
||||||
int climateWindPasses = 50; // moisture-advection iterations (steady state)
|
int climateWindPasses = 50; // moisture-advection iterations (steady state)
|
||||||
int climateMoistureSmooth = 12; // precipitation diffusion passes (wet/dry transition zones)
|
int climateMoistureSmooth = 12; // precipitation diffusion passes (wet/dry transition zones)
|
||||||
|
|
||||||
|
// --- Biota: flora / fauna / funga (see PlanetBiota.cpp + *Gen.cpp) -------
|
||||||
|
// Density scalars (derived each tick) drive the colour views; the discrete
|
||||||
|
// slot/point population (generated on demand, saved) draws archetypes by size.
|
||||||
|
double bioVegTempMin = -5.0; // C below which plants don't grow
|
||||||
|
double bioVegTempOpt = 15.0; // C at/above which temperature isn't limiting
|
||||||
|
double bioVegMoistRef = 0.5; // normalized moisture where water isn't limiting
|
||||||
|
double bioFaunaProductivity = 0.9; // herbivore capacity per unit vegetation
|
||||||
|
double bioCarnPreyMin = 0.30; // min local prey (fauna density) to support carnivores
|
||||||
|
double bioCarnScale = 1.0; // carnivore weight ramp above the prey threshold
|
||||||
|
double bioFungaMoistRef = 0.4; // normalized moisture where fungi aren't water-limited
|
||||||
|
double bioFungaFloraWeight = 0.6; // how much fungi lean on flora (organic matter) 0..1
|
||||||
|
double bioFungaTempMin = -15.0; // C above which fungi are not cold-limited (cold-tolerant)
|
||||||
|
double bioRegionBonus = 0.5; // weight boost for archetypes present in same-biome neighbours
|
||||||
|
int bioFloraSlots = 12; // max distinct flora per cell (point budget caps abundance)
|
||||||
|
int bioFaunaSlots = 10; // max distinct fauna per cell
|
||||||
|
int bioFungaSlots = 8; // max distinct funga per cell
|
||||||
|
int bioFloraPoints = 20; // flora point budget at full density (scaled by density)
|
||||||
|
int bioFaunaPoints = 16; // fauna point budget at full density
|
||||||
|
int bioFungaPoints = 14; // funga point budget at full density
|
||||||
};
|
};
|
||||||
|
|||||||
154
test_biota.cpp
Normal file
154
test_biota.cpp
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// Headless logic test for the Biota stage (flora / fauna / funga). No display.
|
||||||
|
//
|
||||||
|
// g++ -std=c++17 -O2 -Isrc/sim test_biota.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||||
|
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
||||||
|
// src/sim/PlanetIO.cpp -o /tmp/tb && /tmp/tb
|
||||||
|
//
|
||||||
|
// Verifies: density ranges + zeros on water/ice, fauna<=flora capacity, carnivores
|
||||||
|
// only where prey is sufficient, slot/point budgets respected, determinism + RNG
|
||||||
|
// isolation from tectonics, and save v7 round-trip (plus v6-style read leaving the
|
||||||
|
// population empty).
|
||||||
|
|
||||||
|
#include "Planet.hpp"
|
||||||
|
#include "PlanetBiota.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#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 = 4242; cfg.subdivisions = 5;
|
||||||
|
Planet p; p.generate(cfg);
|
||||||
|
settle(p);
|
||||||
|
const int n = (int)p.cells.size();
|
||||||
|
const double sea = p.cfg.seaLevel;
|
||||||
|
|
||||||
|
// --- Density fields ------------------------------------------------------
|
||||||
|
const auto& fl = p.floraDensity(); const auto& fa = p.faunaDensity(); const auto& fu = p.fungaDensity();
|
||||||
|
check((int)fl.size() == n && (int)fa.size() == n && (int)fu.size() == n, "density fields sized n");
|
||||||
|
bool ranged = true, zerosOnWaterIce = true, faunaCap = true, faunaZero = true;
|
||||||
|
bool anyFloraHigh = false, anyFunga = false;
|
||||||
|
const double prod = p.cfg.bioFaunaProductivity;
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
for (double d : {fl[i], fa[i], fu[i]}) if (!(std::isfinite(d) && d >= 0.0 && d <= 1.0)) ranged = false;
|
||||||
|
bool waterIce = (p.cells[i].elevation <= sea) || (p.cells[i].biome == Biome::Ice);
|
||||||
|
if (waterIce && (fl[i] != 0.0 || fa[i] != 0.0 || fu[i] != 0.0)) zerosOnWaterIce = false;
|
||||||
|
if (fa[i] > fl[i] * prod + 1e-9) faunaCap = false; // fauna <= herbivore capacity
|
||||||
|
if (fl[i] == 0.0 && fa[i] != 0.0) faunaZero = false; // no animals without plants
|
||||||
|
if (p.cells[i].biome == Biome::Forest && fl[i] > 0.6) anyFloraHigh = true;
|
||||||
|
if (fu[i] > 0.05) anyFunga = true;
|
||||||
|
}
|
||||||
|
check(ranged, "all densities finite in [0,1]");
|
||||||
|
check(zerosOnWaterIce, "flora/fauna/funga = 0 on ocean & ice");
|
||||||
|
check(faunaCap, "fauna density <= flora * productivity");
|
||||||
|
check(faunaZero, "no fauna where flora is zero");
|
||||||
|
check(anyFloraHigh, "some forest cells are lush (flora > 0.6)");
|
||||||
|
check(anyFunga, "funga present somewhere");
|
||||||
|
|
||||||
|
// --- Discrete population: slots/points + carnivore gating ----------------
|
||||||
|
p.generateBiota();
|
||||||
|
check(p.biotaPopulated(), "generateBiota() populates a land world");
|
||||||
|
const auto& B = p.biota();
|
||||||
|
bool slotsOk = true, pointsOk = true, carnGated = true, onLand = true;
|
||||||
|
auto cost = [&](const std::vector<Organism>& v) { int s = 0; const auto& AR = biotaArchetypes();
|
||||||
|
for (auto& o : v) s += pointCost(AR[o.archetype].size); return s; };
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
const CellBiota& cb = B[i];
|
||||||
|
if (p.cells[i].elevation <= sea || p.cells[i].biome == Biome::Ice) {
|
||||||
|
if (!cb.flora.empty() || !cb.fauna.empty() || !cb.funga.empty()) onLand = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((int)cb.flora.size() > p.cfg.bioFloraSlots ||
|
||||||
|
(int)cb.fauna.size() > p.cfg.bioFaunaSlots ||
|
||||||
|
(int)cb.funga.size() > p.cfg.bioFungaSlots) slotsOk = false;
|
||||||
|
if (cost(cb.flora) > (int)std::lround(p.cfg.bioFloraPoints * fl[i]) ||
|
||||||
|
cost(cb.fauna) > (int)std::lround(p.cfg.bioFaunaPoints * fa[i]) ||
|
||||||
|
cost(cb.funga) > (int)std::lround(p.cfg.bioFungaPoints * fu[i])) pointsOk = false;
|
||||||
|
// Carnivore present => local prey (mean fauna density over i + neighbours) clears the threshold.
|
||||||
|
bool hasCarn = false;
|
||||||
|
for (const Organism& o : cb.fauna)
|
||||||
|
if (biotaArchetypes()[o.archetype].role == EcoRole::Carnivore) hasCarn = true;
|
||||||
|
if (hasCarn) {
|
||||||
|
double sum = fa[i]; int c = 1;
|
||||||
|
for (int j : p.cells[i].neighbors) { sum += fa[j]; ++c; }
|
||||||
|
if (sum / c <= p.cfg.bioCarnPreyMin) carnGated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(onLand, "no organisms on ocean/ice cells");
|
||||||
|
check(slotsOk, "per-cell organism count <= slot budget");
|
||||||
|
check(pointsOk, "per-cell point cost <= density-scaled point budget");
|
||||||
|
check(carnGated, "carnivores only where neighbourhood prey > bioCarnPreyMin");
|
||||||
|
|
||||||
|
// --- Determinism: same seed -> identical population ----------------------
|
||||||
|
std::vector<CellBiota> first = p.biota();
|
||||||
|
p.generateBiota();
|
||||||
|
check(sameBiota(first, p.biota()), "generateBiota() is deterministic (re-run identical)");
|
||||||
|
|
||||||
|
// --- RNG isolation: generating biota must not perturb tectonics ----------
|
||||||
|
{
|
||||||
|
Planet a; a.generate(cfg); settle(a); a.drifting = true;
|
||||||
|
Planet b; b.generate(cfg); settle(b); b.drifting = true;
|
||||||
|
b.generateBiota(); // only b generates biota
|
||||||
|
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 identical = a.cells.size() == b.cells.size();
|
||||||
|
for (size_t i = 0; identical && i < a.cells.size(); ++i)
|
||||||
|
if (a.cells[i].elevation != b.cells[i].elevation || a.cells[i].plateId != b.cells[i].plateId)
|
||||||
|
identical = false;
|
||||||
|
check(identical, "biota generation does not change tectonic evolution (separate RNG)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Save v7 round-trip + v6-style read (empty population) ---------------
|
||||||
|
{
|
||||||
|
std::ostringstream os(std::ios::binary);
|
||||||
|
p.writeState(os);
|
||||||
|
std::string blob = os.str();
|
||||||
|
Planet q; std::istringstream is(blob, std::ios::binary);
|
||||||
|
bool ok = q.readState(is, /*hasBiome*/true, /*hasBiota*/true);
|
||||||
|
check(ok && q.biotaPopulated() && sameBiota(p.biota(), q.biota()), "save v7 round-trips the biota population");
|
||||||
|
|
||||||
|
Planet r; std::istringstream is2(blob, std::ios::binary);
|
||||||
|
bool ok2 = r.readState(is2, /*hasBiome*/true, /*hasBiota*/false); // old (pre-v7) read path
|
||||||
|
check(ok2 && !r.biotaPopulated(), "pre-v7 read leaves population empty (loads fine)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("\n%s (%d failure%s)\n", failures ? "FAILURES" : "ALL PASS",
|
||||||
|
failures, failures == 1 ? "" : "s");
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user