Add stateful volcano lifecycle and event log
This commit is contained in:
parent
275511713c
commit
0c8dbed3d7
38
BUILD.md
38
BUILD.md
@ -77,9 +77,10 @@ CLI flags (applied before the first load/generate):
|
|||||||
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
||||||
auto-created on first run, reload live with F2. Range-checked on
|
auto-created on first run, reload live with F2. Range-checked on
|
||||||
load; an invalid file reverts to safe defaults (not overwritten).
|
load; an invalid file reverts to safe defaults (not overwritten).
|
||||||
planet.save binary snapshot (versioned, currently v13: +Live World clock rate; v12
|
planet.save binary snapshot (versioned, currently v16: +event log; v15 +stateful
|
||||||
+step-back history (~40 frames, so a load can rewind storms); v11 +weather
|
volcanoes; v13 +Live World clock rate; v12 +step-back history
|
||||||
systems/storms; v10 +weather fields; v9 +moons; v8 +Live World clock; v7 +biota): seed + config + full planet state; F5
|
(~40 frames, so a load can rewind storms); v11 +weather systems/storms;
|
||||||
|
v10 +weather fields; v9 +moons; v8 +Live World clock; v7 +biota): seed + config + full planet state; F5
|
||||||
writes it, F9 reloads and resumes deterministically. As of v6
|
writes it, F9 reloads and resumes deterministically. As of v6
|
||||||
the config is stored as a self-describing key=value block (like
|
the config is stored as a self-describing key=value block (like
|
||||||
planet.cfg), so adding/removing config fields no longer breaks saves
|
planet.cfg), so adding/removing config fields no longer breaks saves
|
||||||
@ -230,18 +231,34 @@ saved v10. Evaporate over warm seas -> advect along the wind -> condense -> rain
|
|||||||
weatherSystemRain 1.6 /h rain at a system core
|
weatherSystemRain 1.6 /h rain at a system core
|
||||||
weatherHurricaneStr 0.6 strength above which a tropical system is a hurricane/typhoon
|
weatherHurricaneStr 0.6 strength above which a tropical system is a hurricane/typhoon
|
||||||
|
|
||||||
|
Live info tabs (Live World): the panel beside the 2D map has Sky, Tides, Weather and Events.
|
||||||
|
Events are a saved, capped journal (newest 200) for storm genesis/intensification, volcano
|
||||||
|
dormancy, eruptions and volcanic-island breaches; clicking an event selects and centres its cell.
|
||||||
|
|
||||||
Volcanoes (PlanetConfig, Live World, key V): placed by tectonic context on entering Live World,
|
Volcanoes (PlanetConfig, Live World, key V): placed by tectonic context on entering Live World,
|
||||||
erupt on the live clock, build submarine vents into new islands (eruption state is a pure
|
then integrated as stateful lifecycle agents: grow, go dormant, explode, puff ash, and regrow
|
||||||
function of liveTime, so the stepper rewinds it). Saved v14.
|
weaker. Step-back snapshots include volcano state. Saved v15+.
|
||||||
|
|
||||||
volcanoProbRidge 0.55 per-cell placement prob on a young spreading-ridge cell
|
volcanoProbRidge 0.55 per-cell placement prob on a young spreading-ridge cell
|
||||||
volcanoProbBorder 0.06 per-cell placement prob on a normal plate-border cell
|
volcanoProbBorder 0.06 per-cell placement prob on a normal plate-border cell
|
||||||
volcanoProbInterior 0.003 per-cell placement prob elsewhere (hotspots)
|
volcanoProbInterior 0.003 per-cell placement prob elsewhere (hotspots)
|
||||||
volcanoMaxCount 60 global cap (reservoir-sampled, ratios preserved)
|
volcanoMaxCount 60 global cap (reservoir-sampled, ratios preserved)
|
||||||
volcanoBuildStep 130 m cone/island growth per eruption pulse
|
volcanoBuildRate 0.02 m/h of growth at activity=1 while growing
|
||||||
volcanoMaxHeight 3200 m max height built above a vent's base elevation
|
volcanoFreeHeight 1000 m absolute height below which vents cannot go dormant
|
||||||
volcanoEruptFreq 0.05 eruption pulses per (hour * activity) -- cadence
|
volcanoInitialBuildMax 2500 m max pre-built height when Live World starts
|
||||||
volcanoAshCloud 0.9 cloud cover injected at the vent per erupting hour
|
volcanoMaxHeight 3200 m built height where dormancy becomes certain
|
||||||
|
volcanoDormancyRate 1.0 per-year dormancy hazard scale above free height
|
||||||
|
volcanoDormantMinYears 120 minimum dormancy before explosion
|
||||||
|
volcanoDormantMaxYears 1200 maximum dormancy before explosion
|
||||||
|
volcanoExplodeDropFrac 0.20 fraction of built height shaved by explosion
|
||||||
|
volcanoActivityDecay 0.70 activity multiplier after each explosion
|
||||||
|
volcanoDeadActivity 0.05 growth stops at/below this activity
|
||||||
|
volcanoBlastRadius 0.09 radian radius of the instant explosion ash blast
|
||||||
|
volcanoBlastCloud 1.5 cloud cover added inside the blast
|
||||||
|
volcanoAshMinYears 0.5 minimum sustained ash emission after explosion
|
||||||
|
volcanoAshMaxYears 3.0 maximum sustained ash emission after explosion
|
||||||
|
volcanoAshPuffCellsPerWeek 2.0 average local cells puffed per week
|
||||||
|
volcanoAshCloud 0.9 cloud cover injected by sustained ash
|
||||||
volcanoAshCooling 6 C peak local cooling under an active ash plume
|
volcanoAshCooling 6 C peak local cooling under an active ash plume
|
||||||
|
|
||||||
## Headless logic test (no display)
|
## Headless logic test (no display)
|
||||||
@ -256,7 +273,8 @@ function of liveTime, so the stepper rewinds it). Saved v14.
|
|||||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
|
|
||||||
# Biota / Live World / Ocean / Weather / Volcano suites: same source list, swap test_logic.cpp ->
|
# Biota / Live World / Ocean / Weather / Volcano suites: same source list, swap test_logic.cpp ->
|
||||||
# test_biota.cpp, test_live.cpp, test_ocean.cpp, test_weather.cpp or test_volcano.cpp
|
# test_biota.cpp, test_live.cpp, test_ocean.cpp, test_weather.cpp or test_volcano.cpp.
|
||||||
|
# The CMake build also includes test_events for the viewer event journal.
|
||||||
|
|
||||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||||
determinism. Run after changing Planet::step().
|
determinism. Run after changing Planet::step().
|
||||||
|
|||||||
56
CLAUDE.md
56
CLAUDE.md
@ -98,13 +98,12 @@ the fixed-grid Eulerian model + the climate fields are the groundwork for it.
|
|||||||
- **Volcanoes & volcanic islands** *(done — see `PlanetVolcano.cpp`)* — on entering Live World a
|
- **Volcanoes & volcanic islands** *(done — see `PlanetVolcano.cpp`)* — on entering Live World a
|
||||||
one-time pass (`placeVolcanoes`, separate RNG → tectonic determinism intact) seeds volcanoes by
|
one-time pass (`placeVolcanoes`, separate RNG → tectonic determinism intact) seeds volcanoes by
|
||||||
tectonic context: **very high** probability on young spreading-ridge / "new-plate" cells (baby
|
tectonic context: **very high** probability on young spreading-ridge / "new-plate" cells (baby
|
||||||
plates), **medium** on normal plate borders, **low** elsewhere (hotspots). On the live clock
|
plates), **medium** on normal plate borders, **low** elsewhere (hotspots). They are stateful
|
||||||
(`stepVolcanoes`) they **erupt**; submarine vents build their cell up and **breach sea level into
|
lifecycle agents: some start pre-built, growing vents can breach submarine cells into volcanic
|
||||||
new volcanic islands**, land vents grow cones, and each eruption injects a drifting **ash cloud**
|
islands, tall vents can go dormant, dormant vents explode and shave their peak, then puff ash while
|
||||||
(into the weather field) + **local cooling**. Eruption state (built height + intensity) is a **pure
|
regrowing weaker. Volcano state + `sVolRng` are captured in the Live World step-back snapshot, so
|
||||||
function of `liveTime`** (like insolation/tides/seasons), so the live stepper rewinds islands &
|
`,`/`.` reverses height, dormancy, explosions and ash timers. Rendered as growing, dormant and
|
||||||
eruptions for free (no extra snapshot state). Rendered as cone markers + an eruption glow/ash-plume
|
post-explosion cone markers (3D + 2D, key `V`); saved (v15). Knobs `volcano*`.
|
||||||
flare (3D + 2D, key `V`); saved (v14). Knobs `volcano*`.
|
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
@ -441,7 +440,7 @@ Working and verified (logic tested headless):
|
|||||||
and dissipate behind the system. A tropical system past `weatherHurricaneStr` is a
|
and dissipate behind the system. A tropical system past `weatherHurricaneStr` is a
|
||||||
hurricane/typhoon. Render: an animated cyclonic **spiral marker** per system (red + eye for
|
hurricane/typhoon. Render: an animated cyclonic **spiral marker** per system (red + eye for
|
||||||
cyclones, blue lows; spins with `liveTime`·hemisphere) in 3D + 2D, HUD system/cyclone counts,
|
cyclones, blue lows; spins with `liveTime`·hemisphere) in 3D + 2D, HUD system/cyclone counts,
|
||||||
and a storm list (basin-named) in the Sky & tides panel — all under `K`. `test_weather.cpp`
|
and a storm list (basin-named) in the Live info `Weather` tab — all under `K`. `test_weather.cpp`
|
||||||
adds: systems spawn, move between steps, thicken cloud, RNG isolation, determinism.
|
adds: systems spawn, move between steps, thicken cloud, RNG isolation, determinism.
|
||||||
- **Live World viewer controls — storm follow-cam, 2D map zoom, clock stepper:** (1) **`Y`** cycles
|
- **Live World viewer controls — storm follow-cam, 2D map zoom, clock stepper:** (1) **`Y`** cycles
|
||||||
the 3D camera to **follow a storm** (by descending strength, off after the last). Tracked by a
|
the 3D camera to **follow a storm** (by descending strength, off after the last). Tracked by a
|
||||||
@ -464,6 +463,11 @@ Working and verified (logic tested headless):
|
|||||||
(v12)** so a load can rewind storms past the saved moment; a load also drops any stale pre-load
|
(v12)** so a load can rewind storms past the saved moment; a load also drops any stale pre-load
|
||||||
history. With no recorded past (e.g. immediately after a pre-v12 load) `,` rewinds the sky only and
|
history. With no recorded past (e.g. immediately after a pre-v12 load) `,` rewinds the sky only and
|
||||||
says so. `S` in Live World aliases the forward step.
|
says so. `S` in Live World aliases the forward step.
|
||||||
|
- **Live World event log:** `liveInfoRect` is a tabbed panel (`Sky`, `Tides`, `Weather`, `Events`).
|
||||||
|
The viewer-owned event journal is saved in v16, capped to the newest 200 entries, and records storm
|
||||||
|
formation/intensification plus volcano dormancy, eruptions and island breaches. Clicking an event
|
||||||
|
selects its cell, releases storm follow-cam, rotates the 3D view to it, and centres the 2D map at
|
||||||
|
the current zoom.
|
||||||
- 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
|
||||||
@ -571,7 +575,7 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
|||||||
```
|
```
|
||||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
||||||
`test_weather.cpp` or `test_volcano.cpp` to run the Biota / Live World / Ocean / Weather /
|
`test_weather.cpp` or `test_volcano.cpp` to run the Biota / Live World / Ocean / Weather /
|
||||||
Volcano suites — same source list.)
|
Volcano suites — same source list. CMake also builds `test_events` for the viewer event journal.)
|
||||||
|
|
||||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||||
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
|
||||||
@ -629,10 +633,10 @@ line animate over whatever colour mode is active; the HUD shows a `Year/Day/HH:M
|
|||||||
**1–3 moons** orbit (sun-lit phases, orbit rings, solar/lunar eclipses) and, with the distant
|
**1–3 moons** orbit (sun-lit phases, orbit rings, solar/lunar eclipses) and, with the distant
|
||||||
sun, raise tides — `T` colours the coastline by the live tide level (amber low ↔ cyan high).
|
sun, raise tides — `T` colours the coastline by the live tide level (amber low ↔ cyan high).
|
||||||
`K` shows moving weather (clouds, rain, drifting storms / hurricanes). **Volcanoes** are placed by
|
`K` shows moving weather (clouds, rain, drifting storms / hurricanes). **Volcanoes** are placed by
|
||||||
tectonic context on entry and erupt on the clock — submarine ones build into new **volcanic islands**;
|
tectonic context on entry and run a stateful lifecycle — submarine ones can build into new
|
||||||
`V` toggles the cone/eruption markers. `Y` makes the 3D camera **follow a storm** (cycles by strength,
|
**volcanic islands**; `V` toggles the cone/eruption markers. `Y` makes the 3D camera **follow a storm** (cycles by strength,
|
||||||
off after the last); `.`/`,` step the clock forward/back by one rate-unit (back rewinds the sky **and**
|
off after the last); `.`/`,` step the clock forward/back by one rate-unit (back rewinds the sky **and**
|
||||||
volcanoes/islands, which are pure functions of the clock). Mouse-wheel over the 2D map zooms (drag pans).
|
weather/storms/volcano lifecycle via snapshots). Mouse-wheel over the 2D map zooms (drag pans).
|
||||||
|
|
||||||
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
|
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
|
||||||
file (both applied before the initial load/generate).
|
file (both applied before the initial load/generate).
|
||||||
@ -651,14 +655,17 @@ clock — a flag byte + `liveTime`, v9 appends the **moons** block, v10 appends
|
|||||||
humidity/cloud/rain, flag-gated, v11 also persists the **weather systems** + RNG so a load resumes
|
humidity/cloud/rain, flag-gated, v11 also persists the **weather systems** + RNG so a load resumes
|
||||||
active storms, v12 appends the most recent **step-back frames** — `wxSaveMax`(40) weather snapshots
|
active storms, v12 appends the most recent **step-back frames** — `wxSaveMax`(40) weather snapshots
|
||||||
— so a load can rewind storms past the saved moment, v13 appends the Live World clock rate, v14
|
— so a load can rewind storms past the saved moment, v13 appends the Live World clock rate, v14
|
||||||
appends the **volcanoes** block — the placed `Volcano` set + its RNG, gated by a flag byte);
|
appends the old pure-function **volcanoes** block, v15 replaces it with stateful volcano lifecycle
|
||||||
|
agents plus volcano state in step-back frames, and v16 appends the saved **event journal**;
|
||||||
newer-than-supported is
|
newer-than-supported is
|
||||||
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
||||||
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
||||||
saves spin weather up live; pre-v11 saves load with no active storms (they respawn); pre-v12 saves
|
saves spin weather up live; pre-v11 saves load with no active storms (they respawn); pre-v12 saves
|
||||||
load with no step-back history (you can still step forward then back); pre-v13 saves resume with
|
load with no step-back history (you can still step forward then back); pre-v13 saves resume with
|
||||||
the default live clock rate; pre-v14 saves load with no volcanoes (placed on the next Live World
|
the default live clock rate; pre-v14 saves load with no volcanoes (placed on the next Live World
|
||||||
entry). A load drops any **stale** pre-load `wxUndo` history and reloads the
|
entry); v14 volcanoes are discarded and reseeded as v15 lifecycle agents, with old history skipped;
|
||||||
|
pre-v16 saves load with an empty event journal.
|
||||||
|
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
||||||
saved one.
|
saved one.
|
||||||
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
||||||
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),
|
||||||
@ -668,8 +675,9 @@ a one-time break; a length guard makes that fail gracefully.) `writeConfigFields
|
|||||||
|
|
||||||
Layout (1920x1080): left column 70% wide = 3D globe (top, 60% h, RenderTexture
|
Layout (1920x1080): left column 70% wide = 3D globe (top, 60% h, RenderTexture
|
||||||
1344x648) + 2D Equal Earth map (bottom, 40% h, **left-aligned**, with the freed space at
|
1344x648) + 2D Equal Earth map (bottom, 40% h, **left-aligned**, with the freed space at
|
||||||
its right holding the Live-World **"Sky & tides" panel** — `liveInfoRect`, `renderLiveInfo`:
|
its right holding the Live-World tabbed info panel — `liveInfoRect`, `renderLiveInfo`:
|
||||||
per-moon phase discs + a selected coastal tile's tidal phase); right column 30% wide = cell
|
Sky moon phase discs, selected coastal-tile tide phase, active weather systems, and the clickable
|
||||||
|
saved event journal); right column 30% wide = cell
|
||||||
info (top 50% h) + subareas (bottom 50% h). 3D hover uses a custom camera ray with the
|
info (top 50% h) + subareas (bottom 50% h). 3D hover uses a custom camera ray with the
|
||||||
3D viewport (1344x648 at the origin -- GetScreenToWorldRay assumes the full
|
3D viewport (1344x648 at the origin -- GetScreenToWorldRay assumes the full
|
||||||
screen, wrong here); 2D hover uses EqualEarth::inverse (minus the `mapLon` pan).
|
screen, wrong here); 2D hover uses EqualEarth::inverse (minus the `mapLon` pan).
|
||||||
@ -769,13 +777,15 @@ triangles (plates are fixed in phase 1).
|
|||||||
`volcanoProbRidge` (0.55), `volcanoProbBorder` (0.06), `volcanoProbInterior` (0.003) are the
|
`volcanoProbRidge` (0.55), `volcanoProbBorder` (0.06), `volcanoProbInterior` (0.003) are the
|
||||||
per-cell placement probabilities for young-ridge / plate-border / interior cells (raise for more
|
per-cell placement probabilities for young-ridge / plate-border / interior cells (raise for more
|
||||||
vents of that kind), `volcanoMaxCount` (60) caps the total (reservoir-sampled so the ratios hold).
|
vents of that kind), `volcanoMaxCount` (60) caps the total (reservoir-sampled so the ratios hold).
|
||||||
Eruption/island growth — `volcanoBuildStep` (130 m/pulse) + `volcanoMaxHeight` (3200 m cap above
|
Lifecycle — `volcanoInitialBuildMax` (2500 m) pre-builds some vents on entry, `volcanoBuildRate`
|
||||||
base) set how tall a cone/island gets, `volcanoEruptFreq` (0.05 pulses per hour·activity) the
|
(0.02 m/h at activity 1) grows active vents, `volcanoFreeHeight` (1000 m absolute) protects deep
|
||||||
cadence (raise for faster, more frequent eruptions — at a high live-clock rate islands build in
|
vents from dormancy, and `volcanoMaxHeight` (3200 m built) is the soft height where dormancy becomes
|
||||||
seconds). Eruption FX — `volcanoAshCloud` (0.9, ash cover injected into the weather field per
|
certain. Dormancy/explosions — `volcanoDormancyRate` (1/year scale),
|
||||||
erupting hour) and `volcanoAshCooling` (6 °C, peak local cooling under an active plume). All build
|
`volcanoDormantMinYears`/`MaxYears` (120/1200), `volcanoExplodeDropFrac` (0.20),
|
||||||
+ eruption state is a pure function of `liveTime` (PlanetVolcano.cpp); marker sizes/colours are
|
`volcanoActivityDecay` (0.70), and `volcanoDeadActivity` (0.05). Ash FX —
|
||||||
render constants (ViewerRender.cpp), not config.
|
`volcanoBlastRadius` (0.09 rad), `volcanoBlastCloud` (1.5), `volcanoAshMinYears`/`MaxYears`
|
||||||
|
(0.5/3), `volcanoAshPuffCellsPerWeek` (2), `volcanoAshCloud` (0.9), and
|
||||||
|
`volcanoAshCooling` (6 °C). Marker sizes/colours are render constants (ViewerRender.cpp).
|
||||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||||
knob for how fast/high relief builds.
|
knob for how fast/high relief builds.
|
||||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||||
|
|||||||
@ -75,3 +75,11 @@ foreach(test_name logic biota ocean live weather volcano)
|
|||||||
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
||||||
add_test(NAME ${test_name} COMMAND test_${test_name})
|
add_test(NAME ${test_name} COMMAND test_${test_name})
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
add_executable(test_events test_events.cpp ${RENDER_SOURCES})
|
||||||
|
target_include_directories(test_events PRIVATE src/sim src/render)
|
||||||
|
target_link_libraries(test_events PRIVATE planetsim_sim raylib)
|
||||||
|
if(UNIX AND NOT APPLE)
|
||||||
|
target_link_libraries(test_events PRIVATE m pthread dl)
|
||||||
|
endif()
|
||||||
|
add_test(NAME events COMMAND test_events)
|
||||||
|
|||||||
@ -228,31 +228,30 @@ can't be re-derived from the loaded moment.
|
|||||||
|
|
||||||
## Volcanoes & volcanic islands (Live World)
|
## Volcanoes & volcanic islands (Live World)
|
||||||
|
|
||||||
`PlanetVolcano.cpp`. A `Volcano` is a fixed point on the grid (one `cell`), not a moving agent. On
|
`PlanetVolcano.cpp`. A `Volcano` is a fixed point on the grid (one `cell`) and a stateful lifecycle
|
||||||
**entering Live World** `placeVolcanoes(liveTime)` seeds a set once, by tectonic context: a cell is
|
agent. On **entering Live World** `placeVolcanoes(liveTime)` seeds a set once, by tectonic context: a cell is
|
||||||
**ridge** if its plate is `baby` or a neighbour's is (the `buildBorders` baby test), else **border**
|
**ridge** if its plate is `baby` or a neighbour's is (the `buildBorders` baby test), else **border**
|
||||||
if a neighbour has a different `plateId`, else **interior**; placement probability is
|
if a neighbour has a different `plateId`, else **interior**; placement probability is
|
||||||
`volcanoProbRidge` ≫ `volcanoProbBorder` ≫ `volcanoProbInterior`, reservoir-sampled to
|
`volcanoProbRidge` ≫ `volcanoProbBorder` ≫ `volcanoProbInterior`, reservoir-sampled to
|
||||||
`volcanoMaxCount` (an unbiased subset, ratios preserved). A separate RNG (`sVolRng = cfg.seed ^
|
`volcanoMaxCount` (an unbiased subset, ratios preserved). A separate RNG (`sVolRng = cfg.seed ^
|
||||||
0x70C4F12A`) keeps the tectonic stream untouched. Each vent stores its pre-live `baseElev` + `tStart`.
|
0x70C4F12A`) keeps the tectonic stream untouched. Each vent stores `baseElev`, current `built`
|
||||||
|
height, `phase` (growing/dormant), dormancy and ash timers, ash carry, and decaying `activity`.
|
||||||
|
|
||||||
The crucial design point: **eruption state is a pure function of `liveTime`** (like
|
The crucial design point is now the opposite of the original v14 implementation: **volcanoes are
|
||||||
insolation/tides/seasons, never an integration). `volcanoBuilt(v,t) = min(maxHeight, floor((t−tStart)
|
integrated forward**, not pure functions of `liveTime`. `stepVolcanoes(dt)` grows active vents by
|
||||||
·eruptFreq·activity)·buildStep)` (discrete pulses stepping the cone up, monotonic) and
|
`volcanoBuildRate * activity`, lets tall vents go dormant above `volcanoFreeHeight`, explodes dormant
|
||||||
`volcanoErupting(v,t)` is a flare decaying through each pulse cycle. `stepVolcanoes(dt, liveTime)`
|
vents after a long timer, shaves `volcanoExplodeDropFrac` of built height, starts sustained ash
|
||||||
(called each frame in `liveAdvance`, after `stepWeather`) just reasserts `cells[v.cell].elevation =
|
emission, and decays activity so old volcanoes settle. It always reasserts
|
||||||
baseElev + built` — safe & complete because in Live World nothing else moves elevation. A submarine
|
`cells[v.cell].elevation = baseElev + built`; submarine vents crossing sea level breach into islands
|
||||||
vent crossing sea level **breaches** into an island (`oceanic=false`, `biome=Beach`, viewer
|
and can re-submerge when a restored snapshot has less built height. Explosions stamp a wide local ash
|
||||||
`refreshView`s); crossing back down (on a step back) re-submerges it. Because the state is derived
|
blast into `sCloud`/`sHumidity` and sustained puffs continue while `ashTimer` runs.
|
||||||
from `liveTime`, the **live stepper rewinds islands & eruptions for free** — no per-cell snapshot, no
|
|
||||||
volcano undo history. The only integrated side-effect is the **ash plume**: an eruption adds cloud to
|
Because this is stochastic state, **step-back snapshots include volcanoes + `sVolRng`** alongside
|
||||||
`sCloud`/`sHumidity` (drifts downwind via the weather cycle) + subtracts `volcanoAshCooling` from
|
weather. A backward step restores weather, storms, volcano lifecycle state and RNG, then
|
||||||
`sLiveTemp` at the vent — the cloud reverts via the existing weather snapshot, the cooling is itself
|
`stepVolcanoes(0)` reasserts terrain without advancing. Rendered as growing red/orange cones,
|
||||||
re-derived each frame. Rendered as a cone (taller/redder as it builds) + an orange glow/ash-plume
|
dormant grey quiet cones, and bright post-explosion plume markers (3D + 2D, key `V`). **Saved v15**:
|
||||||
flare when erupting (3D `DrawCylinderEx` inside the tilt matrix; 2D `DrawPoly` triangle), key `V`.
|
the stateful `Volcano` set + `sVolRng`; v14's old pure-function block is consumed and discarded so
|
||||||
**Saved v14**: the placed `Volcano` set + `sVolRng` in `writeState`/`readState` (flag-gated like
|
volcanoes reseed on the next Live World entry. Knobs: `volcano*`.
|
||||||
moons/weather); pre-v14 saves load with none and place them on the next Live World entry. Knobs:
|
|
||||||
`volcano*`.
|
|
||||||
|
|
||||||
## Live World viewer controls (follow-cam, 2D zoom, clock stepper)
|
## Live World viewer controls (follow-cam, 2D zoom, clock stepper)
|
||||||
|
|
||||||
@ -281,6 +280,20 @@ Three viewer-only controls over the Live World sim:
|
|||||||
(the ring is bounded, ~one snapshot per real second since the interval scales with `liveRate`). The
|
(the ring is bounded, ~one snapshot per real second since the interval scales with `liveRate`). The
|
||||||
restored snapshot includes the storm RNG, so re-stepping forward replays deterministically.
|
restored snapshot includes the storm RNG, so re-stepping forward replays deterministically.
|
||||||
|
|
||||||
|
## Live World event journal
|
||||||
|
|
||||||
|
`Viewer` owns a saved, bounded event journal (`WorldEvent`, newest 200) shown in the tabbed
|
||||||
|
`liveInfoRect` panel beside the 2D map (`Sky` / `Tides` / `Weather` / `Events`). It is intentionally
|
||||||
|
viewer-level state: the sim emits no UI strings, and the log is **not** part of step-back history.
|
||||||
|
Rewinding restores weather/storms/volcanoes, but the journal remains the observer's record.
|
||||||
|
|
||||||
|
Events are detected in `Viewer::liveAdvance` by comparing before/after Live World state: weather
|
||||||
|
system formation, tropical systems crossing hurricane/typhoon strength, volcano dormancy, dormant
|
||||||
|
volcano eruptions, and submarine volcanoes breaching into islands. Clicking an event calls
|
||||||
|
`focusCell`: select/rebuild the cell detail, release storm follow-cam, rotate the 3D camera to the
|
||||||
|
cell using the same axial-tilt convention as picking, and centre the 2D map at the current zoom.
|
||||||
|
Save **v16** appends the event log; pre-v16 saves load with an empty journal.
|
||||||
|
|
||||||
## Headless testing
|
## Headless testing
|
||||||
|
|
||||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||||
|
|||||||
@ -75,14 +75,20 @@ 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])));
|
||||||
// Volcano (Live World): built height = current elevation above the captured baseElev
|
// Volcano (Live World): lifecycle phase and current built height.
|
||||||
// (eruption flares are shown by the marker; the live clock isn't available here).
|
|
||||||
for (const Volcano& vc : p.volcanoes) {
|
for (const Volcano& vc : p.volcanoes) {
|
||||||
if (vc.cell != i) continue;
|
if (vc.cell != i) continue;
|
||||||
const char* kn = vc.kind == 0 ? "ridge" : vc.kind == 1 ? "border" : "hotspot";
|
const char* kn = vc.kind == 0 ? "ridge" : vc.kind == 1 ? "border" : "hotspot";
|
||||||
double built = c.elevation - vc.baseElev; if (built < 0.0) built = 0.0;
|
if (vc.ashTimer > 0.0) {
|
||||||
L.push_back(std::string(TextFormat("volcano: %s activity %.0f%% +%.0f m built",
|
L.push_back(std::string(TextFormat("volcano: %s erupting +%.0f m activity %.0f%%",
|
||||||
kn, vc.activity * 100.0, built)));
|
kn, vc.built, vc.activity * 100.0)));
|
||||||
|
} else if (vc.phase == 1) {
|
||||||
|
L.push_back(std::string(TextFormat("volcano: %s dormant %.0f y +%.0f m",
|
||||||
|
kn, vc.timer / (24.0 * 365.25), vc.built)));
|
||||||
|
} else {
|
||||||
|
L.push_back(std::string(TextFormat("volcano: %s growing +%.0f m activity %.0f%%",
|
||||||
|
kn, vc.built, vc.activity * 100.0)));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Biota: density scalars (present after computeBiotaDensity()) + the discrete
|
// Biota: density scalars (present after computeBiotaDensity()) + the discrete
|
||||||
|
|||||||
@ -6,6 +6,19 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char* weatherEventName(const WeatherSystem& ws, const Planet& p) {
|
||||||
|
double lon = 0.0, lat = 0.0;
|
||||||
|
dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
||||||
|
if (ws.tropical && ws.strength >= p.cfg.weatherHurricaneStr)
|
||||||
|
return (lon > -0.5 && lon < 2.4) ? "Typhoon" : "Hurricane";
|
||||||
|
return ws.tropical ? "Tropical low" : "Low";
|
||||||
|
}
|
||||||
|
int eventSeverityForWeather(const WeatherSystem& ws, const Planet& p) {
|
||||||
|
return (ws.tropical && ws.strength >= p.cfg.weatherHurricaneStr) ? 2 : (ws.tropical ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Viewer::init(int argc, char** argv) {
|
bool Viewer::init(int argc, char** argv) {
|
||||||
uint32_t cliSeed = 0; // 0 = no --seed given
|
uint32_t cliSeed = 0; // 0 = no --seed given
|
||||||
for (int a = 1; a < argc; ++a) {
|
for (int a = 1; a < argc; ++a) {
|
||||||
@ -229,7 +242,8 @@ void Viewer::regenWorld() { // after generate(): geometry change
|
|||||||
buildMap2D(planet, mapRect, map2D);
|
buildMap2D(planet, mapRect, map2D);
|
||||||
selectedCell = -1; subgrids.clear();
|
selectedCell = -1; subgrids.clear();
|
||||||
settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false;
|
settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false;
|
||||||
liveWorld = false; followId = 0; wxUndo.clear(); // reseed/regen drops back to World Creation
|
liveWorld = false; followId = 0; wxUndo.clear(); events.clear(); nextEventId = 1; // reseed/regen drops back to World Creation
|
||||||
|
liveInfoTab = 0; eventRowRects.clear(); eventRowIndices.clear();
|
||||||
planet.drifting = false; // Phase 1: original forming behavior
|
planet.drifting = false; // Phase 1: original forming behavior
|
||||||
phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy;
|
phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy;
|
||||||
rivers.clear(); bigRivers.clear();
|
rivers.clear(); bigRivers.clear();
|
||||||
@ -249,6 +263,103 @@ void Viewer::pauseAction() { paused = !paused; } // pause/resume forming or dr
|
|||||||
|
|
||||||
void Viewer::setStatus(const std::string& m) { statusMsg = m; statusUntil = GetTime() + 3.0; }
|
void Viewer::setStatus(const std::string& m) { statusMsg = m; statusUntil = GetTime() + 3.0; }
|
||||||
|
|
||||||
|
void Viewer::appendEvent(uint8_t kind, uint8_t severity, double timeHours, int cell, uint32_t sourceId,
|
||||||
|
const std::string& title, const std::string& detail) {
|
||||||
|
if (cell < 0 || cell >= (int)planet.cells.size()) return;
|
||||||
|
WorldEvent e;
|
||||||
|
e.id = nextEventId++;
|
||||||
|
e.kind = kind;
|
||||||
|
e.severity = severity;
|
||||||
|
e.timeHours = timeHours;
|
||||||
|
e.cell = cell;
|
||||||
|
e.sourceId = sourceId;
|
||||||
|
e.title = title;
|
||||||
|
e.detail = detail;
|
||||||
|
events.push_back(std::move(e));
|
||||||
|
if ((int)events.size() > EVENT_LOG_MAX)
|
||||||
|
events.erase(events.begin(), events.begin() + ((int)events.size() - EVENT_LOG_MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
||||||
|
const std::vector<Volcano>& beforeVolcanoes) {
|
||||||
|
auto beforeStorm = [&](uint32_t id) -> const WeatherSystem* {
|
||||||
|
for (const WeatherSystem& ws : beforeStorms) if (ws.id == id) return &ws;
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
for (const WeatherSystem& ws : planet.storms()) {
|
||||||
|
const WeatherSystem* old = beforeStorm(ws.id);
|
||||||
|
int cell = nearestCell(planet, Vec3{ws.pos.x, ws.pos.y, ws.pos.z});
|
||||||
|
double lon = 0.0, lat = 0.0; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
||||||
|
std::string loc = std::string(TextFormat("%+.0f lat, %+.0f lon", lat * 180.0 / M_PI, lon * 180.0 / M_PI));
|
||||||
|
if (!old) {
|
||||||
|
const char* name = weatherEventName(ws, planet);
|
||||||
|
appendEvent(1, (uint8_t)eventSeverityForWeather(ws, planet), liveTime, cell, ws.id,
|
||||||
|
std::string(name) + " formed",
|
||||||
|
std::string(TextFormat("%.0f%% strength, %s", ws.strength * 100.0, loc.c_str())));
|
||||||
|
} else if (ws.tropical && old->strength < planet.cfg.weatherHurricaneStr
|
||||||
|
&& ws.strength >= planet.cfg.weatherHurricaneStr) {
|
||||||
|
const char* name = weatherEventName(ws, planet);
|
||||||
|
appendEvent(1, 2, liveTime, cell, ws.id,
|
||||||
|
std::string(name) + " intensified",
|
||||||
|
std::string(TextFormat("%.0f%% strength, %s", ws.strength * 100.0, loc.c_str())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto beforeVolcano = [&](uint32_t id) -> const Volcano* {
|
||||||
|
for (const Volcano& v : beforeVolcanoes) if (v.id == id) return &v;
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
for (const Volcano& v : planet.volcanoes) {
|
||||||
|
const Volcano* old = beforeVolcano(v.id);
|
||||||
|
if (!old || v.cell < 0 || v.cell >= (int)planet.cells.size()) continue;
|
||||||
|
const char* kind = v.kind == 0 ? "Ridge volcano" : v.kind == 1 ? "Border volcano" : "Hotspot volcano";
|
||||||
|
double oldElev = old->baseElev + old->built;
|
||||||
|
double newElev = v.baseElev + v.built;
|
||||||
|
if (old->submarine && oldElev <= planet.cfg.seaLevel && newElev > planet.cfg.seaLevel) {
|
||||||
|
appendEvent(2, 1, liveTime, v.cell, v.id, "Volcanic island formed",
|
||||||
|
std::string(TextFormat("%s breached sea level (+%.0f m built)", kind, v.built)));
|
||||||
|
}
|
||||||
|
if (old->phase != 1 && v.phase == 1) {
|
||||||
|
appendEvent(2, 1, liveTime, v.cell, v.id, "Volcano went dormant",
|
||||||
|
std::string(TextFormat("%s, +%.0f m built", kind, v.built)));
|
||||||
|
}
|
||||||
|
if (old->phase == 1 && v.phase == 0 && v.ashTimer > 0.0) {
|
||||||
|
appendEvent(2, 2, liveTime, v.cell, v.id, "Volcano erupted",
|
||||||
|
std::string(TextFormat("%s exploded, +%.0f m remains", kind, v.built)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewer::focusCell(int idx, const std::string& status) {
|
||||||
|
if (idx < 0 || idx >= (int)planet.cells.size()) return;
|
||||||
|
selectedCell = idx;
|
||||||
|
rebuildSub();
|
||||||
|
followId = 0;
|
||||||
|
Vec3 wd = rotateZ(planet.cells[idx].unit, planet.cfg.axialTilt);
|
||||||
|
camPitch = std::clamp((float)std::asin(std::clamp(wd.y, -1.0, 1.0)), -1.5f, 1.5f);
|
||||||
|
camYaw = (float)std::atan2(wd.x, wd.z);
|
||||||
|
cam.position = { camDist * cosf(camPitch) * sinf(camYaw),
|
||||||
|
camDist * sinf(camPitch),
|
||||||
|
camDist * cosf(camPitch) * cosf(camYaw) };
|
||||||
|
double lon = map2D.lon.empty() ? 0.0 : map2D.lon[idx];
|
||||||
|
double lat = map2D.lat.empty() ? 0.0 : map2D.lat[idx];
|
||||||
|
mapLon = wrapPi(-lon);
|
||||||
|
if (mapZoom <= 1.0001) {
|
||||||
|
mapPanX = mapPanY = 0.0;
|
||||||
|
} else {
|
||||||
|
double x = 0.0, y = 0.0;
|
||||||
|
EqualEarth::forward(0.0, lat, x, y);
|
||||||
|
double hh = EqualEarth::halfHeight();
|
||||||
|
double h = mapRect.height * mapZoom;
|
||||||
|
double targetY = mapRect.y + (0.5 - y / hh * 0.5) * h + (mapRect.height - h) * 0.5;
|
||||||
|
mapPanY = std::clamp(mapRect.y + mapRect.height * 0.5 - targetY,
|
||||||
|
-(h - mapRect.height) * 0.5, (h - mapRect.height) * 0.5);
|
||||||
|
double w = mapRect.width * mapZoom;
|
||||||
|
mapPanX = std::clamp(0.0, -(w - mapRect.width) * 0.5, (w - mapRect.width) * 0.5);
|
||||||
|
}
|
||||||
|
if (!status.empty()) setStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
// F5: write seed + config + full planet state. F9: read it back and resume.
|
// F5: write seed + config + full planet state. F9: read it back and resume.
|
||||||
void Viewer::saveGame(const char* path) {
|
void Viewer::saveGame(const char* path) {
|
||||||
std::ofstream os(path, std::ios::binary);
|
std::ofstream os(path, std::ios::binary);
|
||||||
@ -266,7 +377,10 @@ void Viewer::saveGame(const char* path) {
|
|||||||
os.write(reinterpret_cast<const char*>(&liveRate), sizeof liveRate); // v13: live clock rate
|
os.write(reinterpret_cast<const char*>(&liveRate), sizeof liveRate); // v13: live clock rate
|
||||||
planet.writeState(os);
|
planet.writeState(os);
|
||||||
// v12: persist the most recent step-back frames so a load can rewind storms past the moment.
|
// v12: persist the most recent step-back frames so a load can rewind storms past the moment.
|
||||||
|
// v15: frames also carry stateful volcano agents + their RNG.
|
||||||
auto wD = [&](const std::vector<double>& v){ uint64_t m = v.size(); os.write((char*)&m, 8); if (m) os.write((const char*)v.data(), (std::streamsize)(m * sizeof(double))); };
|
auto wD = [&](const std::vector<double>& v){ uint64_t m = v.size(); os.write((char*)&m, 8); if (m) os.write((const char*)v.data(), (std::streamsize)(m * sizeof(double))); };
|
||||||
|
auto wV = [&](const std::vector<Volcano>& v){ uint64_t m = v.size(); os.write((char*)&m, 8); if (m) os.write((const char*)v.data(), (std::streamsize)(m * sizeof(Volcano))); };
|
||||||
|
auto wS = [&](const std::string& s){ uint64_t m = s.size(); os.write((char*)&m, 8); if (m) os.write(s.data(), (std::streamsize)m); };
|
||||||
uint32_t hn = (uint32_t)std::min<size_t>(wxUndo.size(), (size_t)wxSaveMax);
|
uint32_t hn = (uint32_t)std::min<size_t>(wxUndo.size(), (size_t)wxSaveMax);
|
||||||
os.write((char*)&hn, 4);
|
os.write((char*)&hn, 4);
|
||||||
for (size_t i = wxUndo.size() - hn; i < wxUndo.size(); ++i) {
|
for (size_t i = wxUndo.size() - hn; i < wxUndo.size(); ++i) {
|
||||||
@ -276,6 +390,22 @@ void Viewer::saveGame(const char* path) {
|
|||||||
uint64_t sc = f.w.storms.size(); os.write((char*)&sc, 8);
|
uint64_t sc = f.w.storms.size(); os.write((char*)&sc, 8);
|
||||||
if (sc) os.write((const char*)f.w.storms.data(), (std::streamsize)(sc * sizeof(WeatherSystem)));
|
if (sc) os.write((const char*)f.w.storms.data(), (std::streamsize)(sc * sizeof(WeatherSystem)));
|
||||||
os.write((char*)&f.w.rng, 4); os.write((char*)&f.w.nextId, 4);
|
os.write((char*)&f.w.rng, 4); os.write((char*)&f.w.nextId, 4);
|
||||||
|
wV(f.w.volcanoes);
|
||||||
|
os.write((char*)&f.w.volRng, 4);
|
||||||
|
}
|
||||||
|
// v16: persistent world event journal, separate from step-back history.
|
||||||
|
uint32_t en = (uint32_t)std::min<size_t>(events.size(), (size_t)EVENT_LOG_MAX);
|
||||||
|
os.write((char*)&nextEventId, 4);
|
||||||
|
os.write((char*)&en, 4);
|
||||||
|
for (size_t i = events.size() - en; i < events.size(); ++i) {
|
||||||
|
const WorldEvent& e = events[i];
|
||||||
|
os.write((char*)&e.id, 4);
|
||||||
|
os.write((char*)&e.kind, 1);
|
||||||
|
os.write((char*)&e.severity, 1);
|
||||||
|
os.write((char*)&e.timeHours, 8);
|
||||||
|
os.write((char*)&e.cell, 4);
|
||||||
|
os.write((char*)&e.sourceId, 4);
|
||||||
|
wS(e.title); wS(e.detail);
|
||||||
}
|
}
|
||||||
setStatus(os ? std::string("Saved ") + path : "Save failed");
|
setStatus(os ? std::string("Saved ") + path : "Save failed");
|
||||||
}
|
}
|
||||||
@ -295,7 +425,7 @@ void Viewer::loadGame(const char* path) {
|
|||||||
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
||||||
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
||||||
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
||||||
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 volcanoes
|
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes
|
||||||
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
|
||||||
@ -308,7 +438,10 @@ void Viewer::loadGame(const char* path) {
|
|||||||
driftAccum = 0.0; formAccum = 0.0;
|
driftAccum = 0.0; formAccum = 0.0;
|
||||||
wxUndo.clear(); followId = 0; // drop stale step-back history / follow target
|
wxUndo.clear(); followId = 0; // drop stale step-back history / follow target
|
||||||
bool skippedHistory = false;
|
bool skippedHistory = false;
|
||||||
if (ver >= 12) { // v12: restore the saved step-back frames (rewind past load)
|
if (ver >= 12 && ver < 15) {
|
||||||
|
skippedHistory = true; // old frames lack stateful volcanoes; do not restore them
|
||||||
|
}
|
||||||
|
if (ver >= 15) { // v15: restore saved step-back frames, including volcano state
|
||||||
bool historyOk = true;
|
bool historyOk = true;
|
||||||
const uint64_t cellCount = planet.cells.size();
|
const uint64_t cellCount = planet.cells.size();
|
||||||
auto rD = [&](std::vector<double>& v){
|
auto rD = [&](std::vector<double>& v){
|
||||||
@ -318,6 +451,13 @@ void Viewer::loadGame(const char* path) {
|
|||||||
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(double)));
|
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(double)));
|
||||||
if (!is) historyOk = false;
|
if (!is) historyOk = false;
|
||||||
};
|
};
|
||||||
|
auto rV = [&](std::vector<Volcano>& v){
|
||||||
|
uint64_t m = 0; is.read((char*)&m, 8);
|
||||||
|
if (!is || m > 100000) { historyOk = false; v.clear(); return; }
|
||||||
|
v.resize((size_t)m);
|
||||||
|
if (m) is.read((char*)v.data(), (std::streamsize)(m * sizeof(Volcano)));
|
||||||
|
if (!is) historyOk = false;
|
||||||
|
};
|
||||||
uint32_t hn = 0; is.read((char*)&hn, 4);
|
uint32_t hn = 0; is.read((char*)&hn, 4);
|
||||||
if (!is || hn > (uint32_t)wxUndoMax) historyOk = false;
|
if (!is || hn > (uint32_t)wxUndoMax) historyOk = false;
|
||||||
for (uint32_t k = 0; k < hn && is; ++k) {
|
for (uint32_t k = 0; k < hn && is; ++k) {
|
||||||
@ -328,6 +468,8 @@ void Viewer::loadGame(const char* path) {
|
|||||||
f.w.storms.resize((size_t)sc);
|
f.w.storms.resize((size_t)sc);
|
||||||
if (sc) is.read((char*)f.w.storms.data(), (std::streamsize)(sc * sizeof(WeatherSystem)));
|
if (sc) is.read((char*)f.w.storms.data(), (std::streamsize)(sc * sizeof(WeatherSystem)));
|
||||||
is.read((char*)&f.w.rng, 4); is.read((char*)&f.w.nextId, 4);
|
is.read((char*)&f.w.rng, 4); is.read((char*)&f.w.nextId, 4);
|
||||||
|
rV(f.w.volcanoes);
|
||||||
|
is.read((char*)&f.w.volRng, 4);
|
||||||
auto sized = [&](const std::vector<double>& v) { return v.empty() || v.size() == planet.cells.size(); };
|
auto sized = [&](const std::vector<double>& v) { return v.empty() || v.size() == planet.cells.size(); };
|
||||||
if (!is || !sized(f.w.humidity) || !sized(f.w.cloud) || !sized(f.w.rain)
|
if (!is || !sized(f.w.humidity) || !sized(f.w.cloud) || !sized(f.w.rain)
|
||||||
|| f.w.humidity.size() != f.w.cloud.size() || f.w.humidity.size() != f.w.rain.size())
|
|| f.w.humidity.size() != f.w.cloud.size() || f.w.humidity.size() != f.w.rain.size())
|
||||||
@ -339,10 +481,45 @@ void Viewer::loadGame(const char* path) {
|
|||||||
|| !std::isfinite(ws.radius) || ws.radius <= 0.0
|
|| !std::isfinite(ws.radius) || ws.radius <= 0.0
|
||||||
|| !std::isfinite(ws.age) || !std::isfinite(ws.life)
|
|| !std::isfinite(ws.age) || !std::isfinite(ws.life)
|
||||||
|| !std::isfinite(ws.spin)) historyOk = false;
|
|| !std::isfinite(ws.spin)) historyOk = false;
|
||||||
|
for (const Volcano& v : f.w.volcanoes)
|
||||||
|
if (v.cell < 0 || v.cell >= (int)planet.cells.size() || v.phase > 1
|
||||||
|
|| !std::isfinite(v.activity) || !std::isfinite(v.baseElev)
|
||||||
|
|| !std::isfinite(v.built) || !std::isfinite(v.timer)
|
||||||
|
|| !std::isfinite(v.ashTimer) || !std::isfinite(v.ashCarry)) historyOk = false;
|
||||||
if (historyOk) wxUndo.push_back(std::move(f));
|
if (historyOk) wxUndo.push_back(std::move(f));
|
||||||
}
|
}
|
||||||
if (!historyOk) { wxUndo.clear(); skippedHistory = true; }
|
if (!historyOk) { wxUndo.clear(); skippedHistory = true; }
|
||||||
}
|
}
|
||||||
|
events.clear(); nextEventId = 1; liveInfoTab = 0; eventRowRects.clear(); eventRowIndices.clear();
|
||||||
|
if (ver >= 16) {
|
||||||
|
bool eventsOk = true;
|
||||||
|
auto rS = [&](std::string& s) {
|
||||||
|
uint64_t m = 0; is.read((char*)&m, 8);
|
||||||
|
if (!is || m > 4096) { eventsOk = false; s.clear(); return; }
|
||||||
|
s.assign((size_t)m, '\0');
|
||||||
|
if (m) is.read(&s[0], (std::streamsize)m);
|
||||||
|
if (!is) eventsOk = false;
|
||||||
|
};
|
||||||
|
uint32_t en = 0;
|
||||||
|
is.read((char*)&nextEventId, 4);
|
||||||
|
is.read((char*)&en, 4);
|
||||||
|
if (!is || en > (uint32_t)EVENT_LOG_MAX) eventsOk = false;
|
||||||
|
for (uint32_t k = 0; k < en && is; ++k) {
|
||||||
|
WorldEvent e;
|
||||||
|
is.read((char*)&e.id, 4);
|
||||||
|
is.read((char*)&e.kind, 1);
|
||||||
|
is.read((char*)&e.severity, 1);
|
||||||
|
is.read((char*)&e.timeHours, 8);
|
||||||
|
is.read((char*)&e.cell, 4);
|
||||||
|
is.read((char*)&e.sourceId, 4);
|
||||||
|
rS(e.title); rS(e.detail);
|
||||||
|
if (e.cell < 0 || e.cell >= (int)planet.cells.size()
|
||||||
|
|| !std::isfinite(e.timeHours) || e.kind == 0 || e.kind > 32 || e.severity > 3)
|
||||||
|
eventsOk = false;
|
||||||
|
if (eventsOk) events.push_back(std::move(e));
|
||||||
|
}
|
||||||
|
if (!eventsOk) { events.clear(); nextEventId = 1; skippedHistory = true; }
|
||||||
|
}
|
||||||
paused = true; selectedCell = -1; subgrids.clear();
|
paused = true; selectedCell = -1; subgrids.clear();
|
||||||
// Pre-v14 save already in Live World: it has no volcano block, so place a set now (v14+ saves
|
// Pre-v14 save already in Live World: it has no volcano block, so place a set now (v14+ saves
|
||||||
// restore their own). A non-live save places them when the user first presses W.
|
// restore their own). A non-live save places them when the user first presses W.
|
||||||
@ -418,11 +595,17 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
|||||||
moonDirs.push_back(Vector3{ (float)md.x, (float)md.y, (float)md.z });
|
moonDirs.push_back(Vector3{ (float)md.x, (float)md.y, (float)md.z });
|
||||||
moonNormals.push_back(Vector3{ (float)mn.x, (float)mn.y, (float)mn.z });
|
moonNormals.push_back(Vector3{ (float)mn.x, (float)mn.y, (float)mn.z });
|
||||||
}
|
}
|
||||||
|
std::vector<WeatherSystem> beforeStorms;
|
||||||
|
std::vector<Volcano> beforeVolcanoes;
|
||||||
|
if (dtWeather > 0.0) {
|
||||||
|
beforeStorms = planet.storms();
|
||||||
|
beforeVolcanoes = planet.volcanoes;
|
||||||
|
}
|
||||||
planet.stepWeather(dtWeather);
|
planet.stepWeather(dtWeather);
|
||||||
// Volcanoes: reassert vent elevations = base + built(liveTime) (pure function of the clock, so a
|
// Volcanoes are stateful lifecycle agents; step-back restores their snapshot, then dt=0 here
|
||||||
// backward step rewinds island growth), and inject ash cloud + local cooling. A grown/shrunk cone
|
// reasserts restored terrain/biome state without advancing the lifecycle.
|
||||||
// needs a recolor; a sea-level breach needs a biome reclassify (refreshView).
|
VolcanoUpdate vu = planet.stepVolcanoes(dtWeather);
|
||||||
VolcanoUpdate vu = planet.stepVolcanoes(dtWeather, liveTime);
|
if (dtWeather > 0.0) detectLiveEvents(beforeStorms, beforeVolcanoes);
|
||||||
if (vu.breach) refreshView();
|
if (vu.breach) refreshView();
|
||||||
else if (vu.recolor) recolor();
|
else if (vu.recolor) recolor();
|
||||||
rebuildLiveOverlay();
|
rebuildLiveOverlay();
|
||||||
|
|||||||
@ -15,8 +15,9 @@
|
|||||||
// 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 = 14; // v14: +volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
static constexpr uint32_t SAVE_VERSION = 16; // v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
||||||
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
||||||
|
static constexpr int EVENT_LOG_MAX = 200;
|
||||||
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)
|
||||||
@ -99,6 +100,23 @@ struct Viewer {
|
|||||||
bool showClouds = true; // Live World cloud/rain cover overlay (key K)
|
bool showClouds = true; // Live World cloud/rain cover overlay (key K)
|
||||||
bool showVolcanoes = true; // Live World volcano markers (cones + eruption glow, key V)
|
bool showVolcanoes = true; // Live World volcano markers (cones + eruption glow, key V)
|
||||||
|
|
||||||
|
// World event journal: currently Live World events, shaped to be reused by later phases.
|
||||||
|
struct WorldEvent {
|
||||||
|
uint32_t id = 0;
|
||||||
|
uint8_t kind = 0; // 1 weather, 2 volcano, later phases can append new kinds
|
||||||
|
uint8_t severity = 0; // 0 info, 1 notable, 2 severe
|
||||||
|
double timeHours = 0.0;
|
||||||
|
int cell = -1;
|
||||||
|
uint32_t sourceId = 0;
|
||||||
|
std::string title, detail;
|
||||||
|
};
|
||||||
|
std::vector<WorldEvent> events;
|
||||||
|
uint32_t nextEventId = 1;
|
||||||
|
int liveInfoTab = 0; // 0 Sky, 1 Tides, 2 Weather, 3 Events
|
||||||
|
std::vector<Rectangle> liveInfoTabRects;
|
||||||
|
std::vector<Rectangle> eventRowRects;
|
||||||
|
std::vector<int> eventRowIndices; // indices into events for visible event rows
|
||||||
|
|
||||||
// Selection + subgrid (phase 4/5 preview).
|
// Selection + subgrid (phase 4/5 preview).
|
||||||
int selectedCell = -1;
|
int selectedCell = -1;
|
||||||
double selectedThresh = 0.06;
|
double selectedThresh = 0.06;
|
||||||
@ -149,6 +167,11 @@ struct Viewer {
|
|||||||
void liveStepBack(); // step everything back one frame (restores weather/storms)
|
void liveStepBack(); // step everything back one frame (restores weather/storms)
|
||||||
void wxPushSnapshot(); // push the current weather state onto the step-back ring
|
void wxPushSnapshot(); // push the current weather state onto the step-back ring
|
||||||
Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect)
|
Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect)
|
||||||
|
void appendEvent(uint8_t kind, uint8_t severity, double timeHours, int cell, uint32_t sourceId,
|
||||||
|
const std::string& title, const std::string& detail);
|
||||||
|
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
||||||
|
const std::vector<Volcano>& beforeVolcanoes);
|
||||||
|
void focusCell(int idx, const std::string& status = "");
|
||||||
|
|
||||||
// ---- Input (ViewerInput.cpp) --------------------------------------------
|
// ---- Input (ViewerInput.cpp) --------------------------------------------
|
||||||
void handleInput();
|
void handleInput();
|
||||||
|
|||||||
@ -14,6 +14,7 @@ void Viewer::handleInput() {
|
|||||||
bool in3D = (mp.x < view3DW && mp.y < view3DH); // top-left quadrant
|
bool in3D = (mp.x < view3DW && mp.y < view3DH); // top-left quadrant
|
||||||
bool inMap = CheckCollisionPointRec(mp, mapRect);
|
bool inMap = CheckCollisionPointRec(mp, mapRect);
|
||||||
bool inPanel = (selectedCell >= 0) && CheckCollisionPointRec(mp, panelRect);
|
bool inPanel = (selectedCell >= 0) && CheckCollisionPointRec(mp, panelRect);
|
||||||
|
bool inLiveInfo = liveWorld && CheckCollisionPointRec(mp, liveInfoRect);
|
||||||
onPause = CheckCollisionPointRec(mp, pauseBtn);
|
onPause = CheckCollisionPointRec(mp, pauseBtn);
|
||||||
|
|
||||||
// --- Camera input (LMB drag orbits; tracks drag distance for clicks) --
|
// --- Camera input (LMB drag orbits; tracks drag distance for clicks) --
|
||||||
@ -30,8 +31,21 @@ void Viewer::handleInput() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dragDist = 0.0f;
|
dragDist = 0.0f;
|
||||||
pressInMap = inMap; // drag started on the map -> pan it
|
pressInMap = inMap && !inLiveInfo; // drag started on the map -> pan it
|
||||||
if (onPause) pauseAction(); // clickable pause / re-evolve button
|
if (onPause) pauseAction(); // clickable pause / re-evolve button
|
||||||
|
if (inLiveInfo) {
|
||||||
|
for (size_t i = 0; i < liveInfoTabRects.size(); ++i)
|
||||||
|
if (CheckCollisionPointRec(mp, liveInfoTabRects[i])) { liveInfoTab = (int)i; break; }
|
||||||
|
if (liveInfoTab == 3) {
|
||||||
|
for (size_t i = 0; i < eventRowRects.size() && i < eventRowIndices.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, eventRowRects[i])) continue;
|
||||||
|
int ei = eventRowIndices[i];
|
||||||
|
if (ei >= 0 && ei < (int)events.size())
|
||||||
|
focusCell(events[ei].cell, events[ei].title);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Live World: is the camera following a storm? (look up by stable id; release if dissipated)
|
// Live World: is the camera following a storm? (look up by stable id; release if dissipated)
|
||||||
@ -111,7 +125,7 @@ void Viewer::handleInput() {
|
|||||||
hitModel = rotateZ(hitUnit, -planet.cfg.axialTilt); // world -> model (undo tilt)
|
hitModel = rotateZ(hitUnit, -planet.cfg.axialTilt); // world -> model (undo tilt)
|
||||||
hovered = nearestCell(planet, hitModel);
|
hovered = nearestCell(planet, hitModel);
|
||||||
}
|
}
|
||||||
} else if (inMap) {
|
} else if (inMap && !inLiveInfo) {
|
||||||
Rectangle vr = mapViewRect(); // account for 2D zoom/pan
|
Rectangle vr = mapViewRect(); // account for 2D zoom/pan
|
||||||
double nx = (mp.x - vr.x) / vr.width, ny = (mp.y - vr.y) / vr.height;
|
double nx = (mp.x - vr.x) / vr.width, ny = (mp.y - vr.y) / vr.height;
|
||||||
double X = (nx * 2.0 - 1.0) * EqualEarth::halfWidth();
|
double X = (nx * 2.0 - 1.0) * EqualEarth::halfWidth();
|
||||||
@ -130,7 +144,7 @@ void Viewer::handleInput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Click = select a tile (ignored over panel/button / while dragging) -
|
// --- Click = select a tile (ignored over panel/button / while dragging) -
|
||||||
if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && dragDist < 6.0f && !inPanel && !onPause && !phase3Prompt && hovered >= 0)
|
if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && dragDist < 6.0f && !inPanel && !inLiveInfo && !onPause && !phase3Prompt && hovered >= 0)
|
||||||
selectCell(hovered);
|
selectCell(hovered);
|
||||||
|
|
||||||
// --- Keys -------------------------------------------------------------
|
// --- Keys -------------------------------------------------------------
|
||||||
|
|||||||
@ -159,9 +159,8 @@ void Viewer::renderGlobe3D() {
|
|||||||
if (hur) { Vec3 e = p * (double)SR; DrawSphere(Vector3{(float)e.x,(float)e.y,(float)e.z}, 0.02f, Color{255,240,200,255}); }
|
if (hur) { Vec3 e = p * (double)SR; DrawSphere(Vector3{(float)e.x,(float)e.y,(float)e.z}, 0.02f, Color{255,240,200,255}); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Live World volcano markers: a small cone at each vent (taller + redder as it builds), with an
|
// Live World volcano markers: growing vents glow red/orange, dormant vents go quiet/grey,
|
||||||
// orange eruption glow + radial ash-plume flare when erupting -- additive so it reads on the
|
// and post-explosion ash vents flare bright. Inside the tilted matrix, so it tracks the globe.
|
||||||
// night side too. Inside the tilted matrix, so it tracks the leaning globe.
|
|
||||||
if (liveWorld && showVolcanoes && !planet.volcanoes.empty()) {
|
if (liveWorld && showVolcanoes && !planet.volcanoes.empty()) {
|
||||||
const double maxH = std::max(1.0, planet.cfg.volcanoMaxHeight);
|
const double maxH = std::max(1.0, planet.cfg.volcanoMaxHeight);
|
||||||
for (const Volcano& vc : planet.volcanoes) {
|
for (const Volcano& vc : planet.volcanoes) {
|
||||||
@ -169,20 +168,23 @@ void Viewer::renderGlobe3D() {
|
|||||||
const Cell& c = planet.cells[vc.cell];
|
const Cell& c = planet.cells[vc.cell];
|
||||||
Vec3 u = c.unit;
|
Vec3 u = c.unit;
|
||||||
float r = visBase + (float)c.elevation * elevExagg;
|
float r = visBase + (float)c.elevation * elevExagg;
|
||||||
double bf = std::clamp(planet.volcanoBuilt(vc, liveTime) / maxH, 0.0, 1.0);
|
double bf = std::clamp(planet.volcanoBuilt(vc) / maxH, 0.0, 1.0);
|
||||||
double er = planet.volcanoErupting(vc, liveTime);
|
double er = planet.volcanoErupting(vc);
|
||||||
float coneH = 0.022f + 0.045f * (float)bf;
|
float coneH = 0.022f + 0.045f * (float)bf;
|
||||||
float coneR = 0.015f + 0.018f * (float)bf;
|
float coneR = 0.015f + 0.018f * (float)bf;
|
||||||
Vector3 b { (float)(u.x * r), (float)(u.y * r), (float)(u.z * r) };
|
Vector3 b { (float)(u.x * r), (float)(u.y * r), (float)(u.z * r) };
|
||||||
Vector3 apex{ (float)(u.x * (r + coneH)), (float)(u.y * (r + coneH)), (float)(u.z * (r + coneH)) };
|
Vector3 apex{ (float)(u.x * (r + coneH)), (float)(u.y * (r + coneH)), (float)(u.z * (r + coneH)) };
|
||||||
Color cone{ (unsigned char)(110 + 100 * er), (unsigned char)(70 - 20 * er), (unsigned char)(55 - 15 * er), 255 };
|
bool dormant = vc.phase == 1;
|
||||||
|
Color cone = dormant ? Color{105, 100, 95, 255}
|
||||||
|
: Color{ (unsigned char)(115 + 95 * er), (unsigned char)(65 + 20 * bf), 45, 255 };
|
||||||
DrawCylinderEx(b, apex, coneR, coneR * 0.25f, 8, cone);
|
DrawCylinderEx(b, apex, coneR, coneR * 0.25f, 8, cone);
|
||||||
if (er > 0.12) {
|
if (er > 0.12 && !dormant) {
|
||||||
unsigned char a = (unsigned char)std::clamp(60.0 + 195.0 * er, 0.0, 255.0);
|
unsigned char a = (unsigned char)std::clamp(60.0 + 195.0 * er, 0.0, 255.0);
|
||||||
DrawSphere(apex, 0.02f + 0.05f * (float)er, Color{255, 140, 40, a});
|
Color glow = vc.ashTimer > 0.0 ? Color{255, 210, 95, a} : Color{255, 140, 40, a};
|
||||||
float ph = coneH + 0.12f * (float)er;
|
DrawSphere(apex, 0.02f + 0.05f * (float)er, glow);
|
||||||
|
float ph = coneH + (vc.ashTimer > 0.0 ? 0.20f : 0.12f) * (float)er;
|
||||||
Vector3 top{ (float)(u.x * (r + ph)), (float)(u.y * (r + ph)), (float)(u.z * (r + ph)) };
|
Vector3 top{ (float)(u.x * (r + ph)), (float)(u.y * (r + ph)), (float)(u.z * (r + ph)) };
|
||||||
rlSetLineWidth(2.0f); rlBegin(RL_LINES); rlColor4ub(255, 170, 70, a);
|
rlSetLineWidth(2.0f); rlBegin(RL_LINES); rlColor4ub(255, 180, 80, a);
|
||||||
rlVertex3f(apex.x, apex.y, apex.z); rlVertex3f(top.x, top.y, top.z);
|
rlVertex3f(apex.x, apex.y, apex.z); rlVertex3f(top.x, top.y, top.z);
|
||||||
rlEnd(); rlSetLineWidth(1.0f);
|
rlEnd(); rlSetLineWidth(1.0f);
|
||||||
}
|
}
|
||||||
@ -297,13 +299,15 @@ void Viewer::renderMap2D() {
|
|||||||
if (liveWorld && showVolcanoes && !planet.volcanoes.empty()) {
|
if (liveWorld && showVolcanoes && !planet.volcanoes.empty()) {
|
||||||
for (const Volcano& vc : planet.volcanoes) {
|
for (const Volcano& vc : planet.volcanoes) {
|
||||||
if (vc.cell < 0 || vc.cell >= (int)planet.cells.size()) continue;
|
if (vc.cell < 0 || vc.cell >= (int)planet.cells.size()) continue;
|
||||||
double er = planet.volcanoErupting(vc, liveTime);
|
double er = planet.volcanoErupting(vc);
|
||||||
double lon, lat; dirToLonLat(planet.cells[vc.cell].unit, lon, lat);
|
double lon, lat; dirToLonLat(planet.cells[vc.cell].unit, lon, lat);
|
||||||
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
||||||
float s = (5.0f + 3.0f * (float)er) * (float)std::min(2.0, mapZoom);
|
float s = (5.0f + 3.0f * (float)er) * (float)std::min(2.0, mapZoom);
|
||||||
Color tri = er > 0.12 ? Color{235, 110, 40, 255} : Color{150, 75, 55, 255};
|
Color tri = vc.phase == 1 ? Color{125, 120, 115, 255}
|
||||||
|
: vc.ashTimer > 0.0 ? Color{245, 170, 55, 255}
|
||||||
|
: Color{170, 75, 50, 255};
|
||||||
DrawPoly(sp, 3, s, -90.0f, tri); // filled up-pointing triangle (cone)
|
DrawPoly(sp, 3, s, -90.0f, tri); // filled up-pointing triangle (cone)
|
||||||
if (er > 0.12)
|
if (er > 0.12 && vc.phase != 1)
|
||||||
DrawCircleLines((int)sp.x, (int)sp.y, s + 3.0f,
|
DrawCircleLines((int)sp.x, (int)sp.y, s + 3.0f,
|
||||||
Color{255, 170, 70, (unsigned char)std::clamp(90.0 + 150.0 * er, 0.0, 255.0)});
|
Color{255, 170, 70, (unsigned char)std::clamp(90.0 + 150.0 * er, 0.0, 255.0)});
|
||||||
}
|
}
|
||||||
@ -330,15 +334,29 @@ void Viewer::renderMap2D() {
|
|||||||
(int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255});
|
(int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live World "Sky & tides" panel in the freed space right of the (left-aligned) 2D map:
|
// Live World tabbed info panel in the freed space right of the (left-aligned) 2D map.
|
||||||
// the current phase of every moon, and the tidal phase of a selected coastal tile.
|
|
||||||
void Viewer::renderLiveInfo() {
|
void Viewer::renderLiveInfo() {
|
||||||
|
liveInfoTabRects.clear(); eventRowRects.clear(); eventRowIndices.clear();
|
||||||
if (!liveWorld) return;
|
if (!liveWorld) return;
|
||||||
Rectangle r = liveInfoRect;
|
Rectangle r = liveInfoRect;
|
||||||
DrawRectangleRec(r, Color{10, 12, 20, 235});
|
DrawRectangleRec(r, Color{10, 12, 20, 235});
|
||||||
DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255});
|
DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255});
|
||||||
int x = (int)r.x + 14, y = (int)r.y + 10;
|
int x = (int)r.x + 14, y = (int)r.y + 10;
|
||||||
DrawText("Sky & tides", x, y, 20, RAYWHITE); y += 30;
|
DrawText("Live info", x, y, 20, RAYWHITE);
|
||||||
|
const char* tabs[4] = { "Sky", "Tides", "Weather", "Events" };
|
||||||
|
float tx = r.x + 10.0f, ty = r.y + 38.0f;
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
float tw = (r.width - 20.0f) / 4.0f;
|
||||||
|
Rectangle tr{ tx + i * tw, ty, tw - 4.0f, 24.0f };
|
||||||
|
liveInfoTabRects.push_back(tr);
|
||||||
|
bool on = liveInfoTab == i;
|
||||||
|
DrawRectangleRec(tr, on ? Color{42, 48, 68, 255} : Color{18, 22, 34, 255});
|
||||||
|
DrawRectangleLinesEx(tr, 1, on ? Color{125, 145, 190, 255} : Color{65, 70, 90, 255});
|
||||||
|
int w = MeasureText(tabs[i], 14);
|
||||||
|
DrawText(tabs[i], (int)(tr.x + (tr.width - w) * 0.5f), (int)tr.y + 5, 14,
|
||||||
|
on ? RAYWHITE : Color{155, 165, 185, 255});
|
||||||
|
}
|
||||||
|
y = (int)r.y + 72;
|
||||||
|
|
||||||
// Sky geometry at the current clock (recomputed here so the panel is self-contained).
|
// Sky geometry at the current clock (recomputed here so the panel is self-contained).
|
||||||
const double dayH = planet.cfg.dayLengthHours, yrD = planet.cfg.yearLengthDays;
|
const double dayH = planet.cfg.dayLengthHours, yrD = planet.cfg.yearLengthDays;
|
||||||
@ -371,6 +389,7 @@ void Viewer::renderLiveInfo() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const auto& mns = planet.getMoons();
|
const auto& mns = planet.getMoons();
|
||||||
|
if (liveInfoTab == 0) {
|
||||||
for (size_t m = 0; m < mns.size(); ++m) {
|
for (size_t m = 0; m < mns.size(); ++m) {
|
||||||
Vec3 md = planet.moonDirection((int)m, tod, days);
|
Vec3 md = planet.moonDirection((int)m, tod, days);
|
||||||
Vec3 md2 = planet.moonDirection((int)m, tod2, days2);
|
Vec3 md2 = planet.moonDirection((int)m, tod2, days2);
|
||||||
@ -382,19 +401,15 @@ void Viewer::renderLiveInfo() {
|
|||||||
DrawText(TextFormat("%.0f%% lit period %.0f d", f * 100.0, mns[m].periodDays), x + 52, y + 27, 15, Color{150, 160, 175, 255});
|
DrawText(TextFormat("%.0f%% lit period %.0f d", f * 100.0, mns[m].periodDays), x + 52, y + 27, 15, Color{150, 160, 175, 255});
|
||||||
y += 50;
|
y += 50;
|
||||||
}
|
}
|
||||||
if (mns.empty()) { DrawText("(no moons)", x, y, 16, Color{150, 155, 170, 255}); y += 24; }
|
if (mns.empty()) DrawText("(no moons)", x, y, 16, Color{150, 155, 170, 255});
|
||||||
|
} else if (liveInfoTab == 1) {
|
||||||
// Tidal phase for a selected coastal tile (placeholder: high/low + rising/falling).
|
DrawText("Tidal phase", x, y, 18, Color{200, 205, 220, 255}); y += 28;
|
||||||
y += 8;
|
|
||||||
DrawText("Tidal phase", x, y, 18, Color{200, 205, 220, 255}); y += 26;
|
|
||||||
if (selectedCell >= 0 && selectedCell < (int)planet.cells.size()) {
|
if (selectedCell >= 0 && selectedCell < (int)planet.cells.size()) {
|
||||||
const Cell& c = planet.cells[selectedCell];
|
const Cell& c = planet.cells[selectedCell];
|
||||||
const double sea = planet.cfg.seaLevel;
|
const double sea = planet.cfg.seaLevel;
|
||||||
bool selLand = c.elevation > sea, coastal = false;
|
bool selLand = c.elevation > sea, coastal = false;
|
||||||
for (int nb : c.neighbors) if ((planet.cells[nb].elevation > sea) != selLand) { coastal = true; break; }
|
for (int nb : c.neighbors) if ((planet.cells[nb].elevation > sea) != selLand) { coastal = true; break; }
|
||||||
if (coastal) {
|
if (coastal) {
|
||||||
// Single-cell tide now vs a step ahead -> rising/falling (the field itself is the
|
|
||||||
// equilibrium tide; a richer coastal/resonant model is future work).
|
|
||||||
auto cellTide = [&](double dy, double td, double dd) {
|
auto cellTide = [&](double dy, double td, double dd) {
|
||||||
double h = 0.0;
|
double h = 0.0;
|
||||||
for (int mm = 0; mm < (int)mns.size(); ++mm) {
|
for (int mm = 0; mm < (int)mns.size(); ++mm) {
|
||||||
@ -405,38 +420,61 @@ void Viewer::renderLiveInfo() {
|
|||||||
h += planet.cfg.tideSunFactor * (cs * cs - 1.0 / 3.0);
|
h += planet.cfg.tideSunFactor * (cs * cs - 1.0 / 3.0);
|
||||||
return h * planet.cfg.tideAmplitude;
|
return h * planet.cfg.tideAmplitude;
|
||||||
};
|
};
|
||||||
bool rising = cellTide(doy2, tod2, days2) >= cellTide(doy, tod, days); // direction
|
bool rising = cellTide(doy2, tod2, days2) >= cellTide(doy, tod, days);
|
||||||
// Level from the actual tide field (so the enclosed-sea cap is reflected here too).
|
|
||||||
double lvl = ((int)planet.tide().size() == (int)planet.cells.size()) ? planet.tide()[selectedCell]
|
double lvl = ((int)planet.tide().size() == (int)planet.cells.size()) ? planet.tide()[selectedCell]
|
||||||
: cellTide(doy, tod, days);
|
: cellTide(doy, tod, days);
|
||||||
DrawText(TextFormat("coastal cell #%d", selectedCell), x, y, 15, Color{160, 170, 185, 255}); y += 21;
|
DrawText(TextFormat("coastal cell #%d", selectedCell), x, y, 15, Color{160, 170, 185, 255}); y += 22;
|
||||||
DrawText(TextFormat("%+.2f m %s, %s", lvl, lvl >= 0.0 ? "high" : "low", rising ? "rising" : "falling"),
|
DrawText(TextFormat("%+.2f m %s, %s", lvl, lvl >= 0.0 ? "high" : "low", rising ? "rising" : "falling"),
|
||||||
x, y, 17, tideColor(lvl, std::max(0.05, std::fabs(lvl)))); y += 24;
|
x, y, 17, tideColor(lvl, std::max(0.05, std::fabs(lvl)))); y += 25;
|
||||||
DrawText("(equilibrium model - placeholder)", x, y, 13, Color{120, 125, 140, 255});
|
DrawText("(equilibrium model - placeholder)", x, y, 13, Color{120, 125, 140, 255});
|
||||||
} else {
|
} else DrawText("selected tile is inland", x, y, 15, Color{150, 155, 170, 255});
|
||||||
DrawText("selected tile is inland", x, y, 15, Color{150, 155, 170, 255});
|
} else DrawText("click a coastal tile", x, y, 15, Color{150, 155, 170, 255});
|
||||||
}
|
} else if (liveInfoTab == 2) {
|
||||||
} else {
|
DrawText("Weather systems", x, y, 18, Color{200, 205, 220, 255}); y += 28;
|
||||||
DrawText("click a coastal tile", x, y, 15, Color{150, 155, 170, 255});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active weather systems (lows / tropical cyclones), named by basin.
|
|
||||||
y += 12;
|
|
||||||
DrawText("Weather systems", x, y, 18, Color{200, 205, 220, 255}); y += 26;
|
|
||||||
const auto& storms = planet.storms();
|
const auto& storms = planet.storms();
|
||||||
if (storms.empty()) DrawText("(calm — none active)", x, y, 15, Color{150, 155, 170, 255});
|
if (storms.empty()) DrawText("(calm - none active)", x, y, 15, Color{150, 155, 170, 255});
|
||||||
int shown = 0;
|
int shown = 0;
|
||||||
for (const auto& ws : storms) {
|
for (const auto& ws : storms) {
|
||||||
if (shown >= 6 || y > (int)(r.y + r.height) - 22) break;
|
if (shown >= 10 || y > (int)(r.y + r.height) - 22) break;
|
||||||
double lon, lat; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
double lon, lat; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
||||||
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
||||||
const char* kind = hur ? (lon > -0.5 && lon < 2.4 ? "Typhoon" : "Hurricane") // W Pacific vs rest
|
const char* kind = hur ? (lon > -0.5 && lon < 2.4 ? "Typhoon" : "Hurricane")
|
||||||
: ws.tropical ? "Tropical low" : "Low";
|
: ws.tropical ? "Tropical low" : "Low";
|
||||||
Color c = hur ? Color{240, 90, 80, 255} : Color{170, 200, 230, 255};
|
Color c = hur ? Color{240, 90, 80, 255} : Color{170, 200, 230, 255};
|
||||||
DrawText(TextFormat("%s %.0f%% @ %+.0f,%+.0f", kind, ws.strength * 100.0,
|
DrawText(TextFormat("%s %.0f%% @ %+.0f,%+.0f", kind, ws.strength * 100.0,
|
||||||
lat * 180.0 / M_PI, lon * 180.0 / M_PI), x, y, 15, c);
|
lat * 180.0 / M_PI, lon * 180.0 / M_PI), x, y, 15, c);
|
||||||
y += 21; ++shown;
|
y += 21; ++shown;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
DrawText("World events", x, y, 18, Color{200, 205, 220, 255});
|
||||||
|
DrawText(TextFormat("%d saved", (int)events.size()), (int)(r.x + r.width) - 74, y + 2, 13, Color{145, 155, 175, 255});
|
||||||
|
y += 28;
|
||||||
|
if (events.empty()) {
|
||||||
|
DrawText("(no events yet)", x, y, 15, Color{150, 155, 170, 255});
|
||||||
|
} else {
|
||||||
|
for (int ei = (int)events.size() - 1; ei >= 0; --ei) {
|
||||||
|
if (y > (int)(r.y + r.height) - 42) break;
|
||||||
|
const WorldEvent& e = events[ei];
|
||||||
|
Rectangle row{ r.x + 8.0f, (float)y - 3.0f, r.width - 16.0f, 40.0f };
|
||||||
|
eventRowRects.push_back(row); eventRowIndices.push_back(ei);
|
||||||
|
Color bg = e.severity >= 2 ? Color{58, 30, 34, 210}
|
||||||
|
: e.severity == 1 ? Color{42, 42, 34, 205}
|
||||||
|
: Color{18, 22, 34, 205};
|
||||||
|
Color fg = e.severity >= 2 ? Color{250, 130, 95, 255}
|
||||||
|
: e.severity == 1 ? Color{230, 190, 95, 255}
|
||||||
|
: Color{175, 205, 235, 255};
|
||||||
|
DrawRectangleRec(row, bg);
|
||||||
|
DrawRectangleLinesEx(row, 1, Color{70, 75, 92, 255});
|
||||||
|
const char* icon = e.kind == 2 ? "^" : "~";
|
||||||
|
DrawText(icon, (int)row.x + 7, (int)row.y + 6, 18, fg);
|
||||||
|
double d = e.timeHours / std::max(0.1, planet.cfg.dayLengthHours);
|
||||||
|
DrawText(TextFormat("D%.1f", d), (int)row.x + 24, (int)row.y + 5, 12, Color{145, 155, 175, 255});
|
||||||
|
DrawText(e.title.c_str(), (int)row.x + 68, (int)row.y + 4, 14, fg);
|
||||||
|
DrawText(e.detail.c_str(), (int)row.x + 68, (int)row.y + 21, 12, Color{165, 170, 185, 255});
|
||||||
|
y += 43;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public:
|
|||||||
std::vector<Cell> cells;
|
std::vector<Cell> cells;
|
||||||
std::vector<Plate> plates;
|
std::vector<Plate> plates;
|
||||||
std::vector<Moon> moons; // Live World: 1-3 natural satellites (generated + saved)
|
std::vector<Moon> moons; // Live World: 1-3 natural satellites (generated + saved)
|
||||||
std::vector<Volcano> volcanoes; // Live World: volcanoes placed on entry by tectonic context (saved v14)
|
std::vector<Volcano> volcanoes; // Live World: stateful lifecycle volcanoes (saved v15)
|
||||||
|
|
||||||
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
||||||
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
||||||
@ -108,15 +108,14 @@ public:
|
|||||||
|
|
||||||
// Volcanoes (Live World, PlanetVolcano.cpp). placeVolcanoes() runs once on entering Live World:
|
// Volcanoes (Live World, PlanetVolcano.cpp). placeVolcanoes() runs once on entering Live World:
|
||||||
// it seeds a set by tectonic context (high on young ridges, medium on borders, low elsewhere)
|
// it seeds a set by tectonic context (high on young ridges, medium on borders, low elsewhere)
|
||||||
// from a separate RNG (tectonic determinism intact), capturing each vent's baseElev. stepVolcanoes()
|
// from a separate RNG (tectonic determinism intact), capturing each vent's baseElev and initial
|
||||||
// runs each live frame: it reasserts each vent's cell elevation = baseElev + built(liveTime) (build
|
// built height. stepVolcanoes() integrates growth/dormancy/explosions forward, reasserts vent
|
||||||
// + eruption intensity are PURE FUNCTIONS of liveTime, so the stepper rewinds them), breaches
|
// elevations, breaches submarine vents into islands, and injects ash cloud + local cooling.
|
||||||
// submarine vents into islands, and injects ash cloud + local cooling into the weather/climate
|
// Saved (v15); step-back restores them via WeatherSnapshot.
|
||||||
// fields. Saved (v14). volcanoErupting(i)/volcanoBuilt(i) report a vent's current state for rendering.
|
|
||||||
void placeVolcanoes(double liveTime = 0.0);
|
void placeVolcanoes(double liveTime = 0.0);
|
||||||
VolcanoUpdate stepVolcanoes(double dtHours, double liveTime);
|
VolcanoUpdate stepVolcanoes(double dtHours);
|
||||||
double volcanoBuilt(const Volcano& v, double liveTime) const; // m built above baseElev at liveTime
|
double volcanoBuilt(const Volcano& v) const; // m built above baseElev
|
||||||
double volcanoErupting(const Volcano& v, double liveTime) const; // 0..1 current eruption intensity
|
double volcanoErupting(const Volcano& v) const; // 0..1 current visual eruption intensity
|
||||||
|
|
||||||
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
|
// Phase 3 (biomes): classify every cell into a Biome from elevation + the climate
|
||||||
// fields (temperature + normalized precipitation). Derived + written back into
|
// fields (temperature + normalized precipitation). Derived + written back into
|
||||||
@ -155,11 +154,11 @@ public:
|
|||||||
// hasMoons: whether the stream carries the moons block (save v9+); older saves
|
// hasMoons: whether the stream carries the moons block (save v9+); older saves
|
||||||
// synthesize moons from the seed instead. hasWeather: the weather block (save v10+);
|
// synthesize moons from the seed instead. hasWeather: the weather block (save v10+);
|
||||||
// older saves leave weather to spin up on entering Live World.
|
// older saves leave weather to spin up on entering Live World.
|
||||||
// hasVolcanoes: whether the stream carries the volcano block (save v14+); older saves load
|
// hasVolcanoes: whether the stream carries a volcano block (save v14+). hasStatefulVolcanoes
|
||||||
// with no volcanoes (they are placed on the next Live World entry).
|
// means v15+ lifecycle volcanoes; v14's old pure-function block is consumed and discarded.
|
||||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
||||||
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
|
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
|
||||||
bool hasVolcanoes = true);
|
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true);
|
||||||
|
|
||||||
// Helpers for rendering / info.
|
// Helpers for rendering / info.
|
||||||
double cellWidthMeters() const; // approx lateral cell spacing
|
double cellWidthMeters() const; // approx lateral cell spacing
|
||||||
@ -242,8 +241,8 @@ private:
|
|||||||
std::vector<WeatherSystem> sStorms;
|
std::vector<WeatherSystem> sStorms;
|
||||||
uint32_t sWeatherRng = 1;
|
uint32_t sWeatherRng = 1;
|
||||||
uint32_t sStormNextId = 1; // monotonic id for follow-cam tracking
|
uint32_t sStormNextId = 1; // monotonic id for follow-cam tracking
|
||||||
// Volcanoes (Live World; saved v14). Separate RNG (seeded from cfg.seed in placeVolcanoes)
|
// Volcanoes (Live World; saved v15). Separate RNG (seeded from cfg.seed in placeVolcanoes)
|
||||||
// keeps tectonic determinism intact; eruptions are pure functions of liveTime (no per-step RNG).
|
// keeps tectonic determinism intact while lifecycle rolls happen during Live World.
|
||||||
uint32_t sVolRng = 1;
|
uint32_t sVolRng = 1;
|
||||||
|
|
||||||
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||||
|
|||||||
@ -43,8 +43,11 @@
|
|||||||
D(weatherSpawnRate) D(weatherSystemSpeed) D(weatherTropicalSST) D(weatherSystemRadius) \
|
D(weatherSpawnRate) D(weatherSystemSpeed) D(weatherTropicalSST) D(weatherSystemRadius) \
|
||||||
D(weatherSystemCloud) D(weatherSystemRain) D(weatherHurricaneStr) \
|
D(weatherSystemCloud) D(weatherSystemRain) D(weatherHurricaneStr) \
|
||||||
D(volcanoProbRidge) D(volcanoProbBorder) D(volcanoProbInterior) \
|
D(volcanoProbRidge) D(volcanoProbBorder) D(volcanoProbInterior) \
|
||||||
D(volcanoBuildStep) D(volcanoMaxHeight) D(volcanoEruptFreq) \
|
D(volcanoBuildRate) D(volcanoFreeHeight) D(volcanoInitialBuildMax) D(volcanoMaxHeight) \
|
||||||
D(volcanoAshCloud) D(volcanoAshCooling) \
|
D(volcanoDormancyRate) D(volcanoDormantMinYears) D(volcanoDormantMaxYears) \
|
||||||
|
D(volcanoExplodeDropFrac) D(volcanoActivityDecay) D(volcanoDeadActivity) \
|
||||||
|
D(volcanoBlastRadius) D(volcanoBlastCloud) D(volcanoAshMinYears) D(volcanoAshMaxYears) \
|
||||||
|
D(volcanoAshPuffCellsPerWeek) D(volcanoAshCloud) D(volcanoAshCooling) \
|
||||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
||||||
@ -222,9 +225,21 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.volcanoProbRidge, 0.0, 1.0, "volcanoProbRidge"));
|
E(rng(cfg.volcanoProbRidge, 0.0, 1.0, "volcanoProbRidge"));
|
||||||
E(rng(cfg.volcanoProbBorder, 0.0, 1.0, "volcanoProbBorder"));
|
E(rng(cfg.volcanoProbBorder, 0.0, 1.0, "volcanoProbBorder"));
|
||||||
E(rng(cfg.volcanoProbInterior, 0.0, 1.0, "volcanoProbInterior"));
|
E(rng(cfg.volcanoProbInterior, 0.0, 1.0, "volcanoProbInterior"));
|
||||||
E(rng(cfg.volcanoBuildStep, 0.0, 5000.0, "volcanoBuildStep"));
|
E(rng(cfg.volcanoBuildRate, 0.0, 1000.0, "volcanoBuildRate"));
|
||||||
E(rng(cfg.volcanoMaxHeight, 0.0, 12000.0, "volcanoMaxHeight"));
|
E(rng(cfg.volcanoFreeHeight, -11000.0, 12000.0, "volcanoFreeHeight"));
|
||||||
E(rng(cfg.volcanoEruptFreq, 0.0, 100.0, "volcanoEruptFreq"));
|
E(rng(cfg.volcanoInitialBuildMax, 0.0, 12000.0, "volcanoInitialBuildMax"));
|
||||||
|
E(rng(cfg.volcanoMaxHeight, 1.0, 12000.0, "volcanoMaxHeight"));
|
||||||
|
E(rng(cfg.volcanoDormancyRate, 0.0, 100.0, "volcanoDormancyRate"));
|
||||||
|
E(rng(cfg.volcanoDormantMinYears, 0.0, 1.0e6, "volcanoDormantMinYears"));
|
||||||
|
E(rng(cfg.volcanoDormantMaxYears, 0.0, 1.0e6, "volcanoDormantMaxYears"));
|
||||||
|
E(rng(cfg.volcanoExplodeDropFrac, 0.0, 1.0, "volcanoExplodeDropFrac"));
|
||||||
|
E(rng(cfg.volcanoActivityDecay, 0.0, 1.0, "volcanoActivityDecay"));
|
||||||
|
E(rng(cfg.volcanoDeadActivity, 0.0, 1.0, "volcanoDeadActivity"));
|
||||||
|
E(rng(cfg.volcanoBlastRadius, 0.0, 3.2, "volcanoBlastRadius"));
|
||||||
|
E(rng(cfg.volcanoBlastCloud, 0.0, 10.0, "volcanoBlastCloud"));
|
||||||
|
E(rng(cfg.volcanoAshMinYears, 0.0, 1.0e6, "volcanoAshMinYears"));
|
||||||
|
E(rng(cfg.volcanoAshMaxYears, 0.0, 1.0e6, "volcanoAshMaxYears"));
|
||||||
|
E(rng(cfg.volcanoAshPuffCellsPerWeek, 0.0, 1000.0, "volcanoAshPuffCellsPerWeek"));
|
||||||
E(rng(cfg.volcanoAshCloud, 0.0, 10.0, "volcanoAshCloud"));
|
E(rng(cfg.volcanoAshCloud, 0.0, 10.0, "volcanoAshCloud"));
|
||||||
E(rng(cfg.volcanoAshCooling, 0.0, 40.0, "volcanoAshCooling"));
|
E(rng(cfg.volcanoAshCooling, 0.0, 40.0, "volcanoAshCooling"));
|
||||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||||
@ -253,6 +268,10 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
||||||
if (cfg.peakSoftCapStart >= cfg.peakSoftCapEnd)
|
if (cfg.peakSoftCapStart >= cfg.peakSoftCapEnd)
|
||||||
bad.push_back("peakSoftCapStart >= peakSoftCapEnd (grow probability must span a band)");
|
bad.push_back("peakSoftCapStart >= peakSoftCapEnd (grow probability must span a band)");
|
||||||
|
if (cfg.volcanoDormantMinYears > cfg.volcanoDormantMaxYears)
|
||||||
|
bad.push_back("volcanoDormantMinYears > volcanoDormantMaxYears");
|
||||||
|
if (cfg.volcanoAshMinYears > cfg.volcanoAshMaxYears)
|
||||||
|
bad.push_back("volcanoAshMinYears > volcanoAshMaxYears");
|
||||||
|
|
||||||
if (bad.empty()) return {};
|
if (bad.empty()) return {};
|
||||||
std::string msg = "Bad config:";
|
std::string msg = "Bad config:";
|
||||||
@ -261,6 +280,16 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
struct LegacyVolcano {
|
||||||
|
uint32_t id = 0;
|
||||||
|
int cell = -1;
|
||||||
|
uint8_t kind = 2;
|
||||||
|
uint8_t submarine = 0;
|
||||||
|
double activity = 0.5;
|
||||||
|
double baseElev = 0.0;
|
||||||
|
double tStart = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
template <class T> void writePod(std::ostream& os, const T& v) {
|
template <class T> void writePod(std::ostream& os, const T& v) {
|
||||||
static_assert(std::is_trivially_copyable<T>::value, "writePod needs a POD type");
|
static_assert(std::is_trivially_copyable<T>::value, "writePod needs a POD type");
|
||||||
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
||||||
@ -334,15 +363,14 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
writeVec(os, sHumidity); writeVec(os, sCloud); writeVec(os, sRain);
|
writeVec(os, sHumidity); writeVec(os, sCloud); writeVec(os, sRain);
|
||||||
writeVec(os, sStorms); writePod(os, sWeatherRng); writePod(os, sStormNextId); // v11
|
writeVec(os, sStorms); writePod(os, sWeatherRng); writePod(os, sStormNextId); // v11
|
||||||
}
|
}
|
||||||
// v14: Live World volcanoes (placed by tectonic context on entry; eruption state is a pure
|
// v15: Live World volcanoes are stateful lifecycle agents. Always written from v15;
|
||||||
// function of liveTime, so only the placed set + its RNG need saving). Always written from v14;
|
|
||||||
// older readers stop before this block.
|
// older readers stop before this block.
|
||||||
writeVec(os, volcanoes);
|
writeVec(os, volcanoes);
|
||||||
writePod(os, sVolRng);
|
writePod(os, sVolRng);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
||||||
bool hasWeather, bool hasStorms, bool hasVolcanoes) {
|
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes) {
|
||||||
// Save blocks are append-only by version. If a caller asks for an older prefix,
|
// Save blocks are append-only by version. If a caller asks for an older prefix,
|
||||||
// later blocks cannot exist in that stream even if the default arguments say otherwise.
|
// later blocks cannot exist in that stream even if the default arguments say otherwise.
|
||||||
if (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; hasVolcanoes = false; }
|
if (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; hasVolcanoes = false; }
|
||||||
@ -450,16 +478,29 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// v14: Live World volcanoes. Older saves load with none (placed on next Live World entry).
|
// v14 had pure-function volcanoes with the old struct layout; v15 has stateful lifecycle
|
||||||
|
// volcanoes. Consume the old block so the stream stays aligned, but discard it.
|
||||||
volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
||||||
if (hasVolcanoes) {
|
if (hasVolcanoes) {
|
||||||
|
if (!hasStatefulVolcanoes) {
|
||||||
|
std::vector<LegacyVolcano> legacy;
|
||||||
|
if (!readVec(is, legacy, 100000)) return false;
|
||||||
|
readPod(is, sVolRng);
|
||||||
|
sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
||||||
|
volcanoes.clear();
|
||||||
|
if (!is) return false;
|
||||||
|
computeBiotaDensity();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!readVec(is, volcanoes, 100000)) return false;
|
if (!readVec(is, volcanoes, 100000)) return false;
|
||||||
readPod(is, sVolRng);
|
readPod(is, sVolRng);
|
||||||
if (!is) return false;
|
if (!is) return false;
|
||||||
const int nc2 = (int)cells.size();
|
const int nc2 = (int)cells.size();
|
||||||
for (const Volcano& v : volcanoes)
|
for (const Volcano& v : volcanoes)
|
||||||
if (v.cell < 0 || v.cell >= nc2 || !std::isfinite(v.activity)
|
if (v.cell < 0 || v.cell >= nc2 || !std::isfinite(v.activity)
|
||||||
|| !std::isfinite(v.baseElev) || !std::isfinite(v.tStart)) return false;
|
|| !std::isfinite(v.baseElev) || !std::isfinite(v.built)
|
||||||
|
|| !std::isfinite(v.timer) || !std::isfinite(v.ashTimer)
|
||||||
|
|| !std::isfinite(v.ashCarry) || v.phase > 1) return false;
|
||||||
}
|
}
|
||||||
computeBiotaDensity(); // derived density scalars for the colour views
|
computeBiotaDensity(); // derived density scalars for the colour views
|
||||||
return (bool)is;
|
return (bool)is;
|
||||||
|
|||||||
@ -66,17 +66,20 @@ struct Moon {
|
|||||||
|
|
||||||
// A volcano (Live World): a fixed point on the grid (one cell) placed by tectonic context when the
|
// A volcano (Live World): a fixed point on the grid (one cell) placed by tectonic context when the
|
||||||
// world enters Live World -- high probability on young spreading ridges ("new plate"), medium on
|
// world enters Live World -- high probability on young spreading ridges ("new plate"), medium on
|
||||||
// plate borders, low elsewhere (hotspots). Over the live clock it erupts; submarine ones build their
|
// plate borders, low elsewhere (hotspots). It is a small stateful lifecycle agent: it grows, can
|
||||||
// cell up into new islands. Eruption state is a PURE FUNCTION of liveTime (built height + intensity
|
// go dormant, explodes after dormancy, then regrows weaker. Saved (v15).
|
||||||
// recomputed each frame, never integrated) so the live stepper rewinds it for free. Saved (v14).
|
|
||||||
struct Volcano {
|
struct Volcano {
|
||||||
uint32_t id = 0; // stable id (markers / cell-info)
|
uint32_t id = 0; // stable id (markers / cell-info)
|
||||||
int cell = -1; // the grid cell it sits on (fixed geometry)
|
int cell = -1; // the grid cell it sits on (fixed geometry)
|
||||||
uint8_t kind = 2; // 0 = ridge (new plate), 1 = plate border, 2 = hotspot/interior
|
uint8_t kind = 2; // 0 = ridge (new plate), 1 = plate border, 2 = hotspot/interior
|
||||||
uint8_t submarine = 0; // 1 if its baseElev is below sea level (can build an island)
|
uint8_t submarine = 0; // 1 if its baseElev is below sea level (can build an island)
|
||||||
|
uint8_t phase = 0; // 0 = growing, 1 = dormant
|
||||||
double activity = 0.5; // 0..1 eruption vigour (drives cadence + build rate)
|
double activity = 0.5; // 0..1 eruption vigour (drives cadence + build rate)
|
||||||
double baseElev = 0.0; // m: cell elevation captured at placement (build adds on top)
|
double baseElev = 0.0; // m: cell elevation captured at placement (build adds on top)
|
||||||
double tStart = 0.0; // liveTime (h) at placement; built height is f(liveTime - tStart)
|
double built = 0.0; // m: current height built above baseElev
|
||||||
|
double timer = 0.0; // h: dormancy countdown
|
||||||
|
double ashTimer = 0.0; // h: sustained ash emission after an explosion
|
||||||
|
double ashCarry = 0.0; // fractional ash-puff accumulator
|
||||||
};
|
};
|
||||||
|
|
||||||
// Result of one stepVolcanoes() call, telling the viewer how much of the view to rebuild:
|
// Result of one stepVolcanoes() call, telling the viewer how much of the view to rebuild:
|
||||||
@ -89,7 +92,9 @@ struct VolcanoUpdate { bool recolor = false; bool breach = false; };
|
|||||||
struct WeatherSnapshot {
|
struct WeatherSnapshot {
|
||||||
std::vector<double> humidity, cloud, rain;
|
std::vector<double> humidity, cloud, rain;
|
||||||
std::vector<WeatherSystem> storms;
|
std::vector<WeatherSystem> storms;
|
||||||
|
std::vector<Volcano> volcanoes;
|
||||||
uint32_t rng = 0, nextId = 0;
|
uint32_t rng = 0, nextId = 0;
|
||||||
|
uint32_t volRng = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Plate {
|
struct Plate {
|
||||||
@ -338,9 +343,21 @@ struct PlanetConfig {
|
|||||||
double volcanoProbBorder = 0.06; // per-cell placement prob on a normal plate-border cell
|
double volcanoProbBorder = 0.06; // per-cell placement prob on a normal plate-border cell
|
||||||
double volcanoProbInterior = 0.003; // per-cell placement prob elsewhere (intraplate hotspots)
|
double volcanoProbInterior = 0.003; // per-cell placement prob elsewhere (intraplate hotspots)
|
||||||
int volcanoMaxCount = 60; // global cap on placed volcanoes
|
int volcanoMaxCount = 60; // global cap on placed volcanoes
|
||||||
double volcanoBuildStep = 130.0; // m of cone/island growth per eruption pulse
|
double volcanoBuildRate = 0.02; // m/h at activity=1 while growing
|
||||||
double volcanoMaxHeight = 3200.0;// m: max height a volcano builds above its baseElev
|
double volcanoFreeHeight = 1000.0;// m absolute elevation below which vents cannot go dormant
|
||||||
double volcanoEruptFreq = 0.05; // eruption pulses per (hour * activity) -- cadence
|
double volcanoInitialBuildMax = 2500.0; // m: max pre-built height on Live World entry
|
||||||
|
double volcanoMaxHeight = 3200.0;// m built height where dormancy becomes certain
|
||||||
|
double volcanoDormancyRate = 1.0; // /year hazard scale once above volcanoFreeHeight
|
||||||
|
double volcanoDormantMinYears = 120.0; // min dormancy before explosion
|
||||||
|
double volcanoDormantMaxYears = 1200.0; // max dormancy before explosion
|
||||||
|
double volcanoExplodeDropFrac = 0.20; // fraction of built height shaved by an explosion
|
||||||
|
double volcanoActivityDecay = 0.70; // activity multiplier after each explosion
|
||||||
|
double volcanoDeadActivity = 0.05; // activity floor below which growth stops
|
||||||
|
double volcanoBlastRadius = 0.09; // rad: wide ash blast radius around the vent
|
||||||
|
double volcanoBlastCloud = 1.5; // cloud added inside the explosion blast
|
||||||
|
double volcanoAshMinYears = 0.5; // min sustained ash emission after explosion
|
||||||
|
double volcanoAshMaxYears = 3.0; // max sustained ash emission after explosion
|
||||||
|
double volcanoAshPuffCellsPerWeek = 2.0;// average local cells puffed per week while ashTimer runs
|
||||||
double volcanoAshCloud = 0.9; // cloud cover injected at the vent per erupting hour (ash plume)
|
double volcanoAshCloud = 0.9; // cloud cover injected at the vent per erupting hour (ash plume)
|
||||||
double volcanoAshCooling = 6.0; // C: peak local cooling under an active ash plume
|
double volcanoAshCooling = 6.0; // C: peak local cooling under an active ash plume
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,49 +1,53 @@
|
|||||||
#include "Planet.hpp"
|
#include "Planet.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// --- Volcanoes (Live World) --------------------------------------------------
|
// --- Volcanoes (Live World) --------------------------------------------------
|
||||||
// A volcano is a fixed point on the grid (one cell), placed ONCE when the world
|
// Volcanoes are fixed vents placed once by tectonic context when the world enters
|
||||||
// enters Live World by tectonic context: very high probability on young spreading
|
// Live World. They are stateful agents: growing vents build height, tall vents can
|
||||||
// ridges ("new plate" / baby plates), medium on normal plate borders, low elsewhere
|
// go dormant, dormant vents eventually explode, shave their peak, emit ash, and
|
||||||
// (intraplate hotspots). Over the live clock it erupts; submarine vents build their
|
// regrow weaker. The state is saved and included in the Live World step-back
|
||||||
// cell up until it breaches sea level into a new volcanic island.
|
// snapshot, while all random rolls use sVolRng so tectonic determinism is untouched.
|
||||||
//
|
|
||||||
// Determinism note: placement draws a SEPARATE RNG (sVolRng, seeded from cfg.seed)
|
|
||||||
// so it never perturbs the tectonic stream. The eruption state -- built height and
|
|
||||||
// eruption intensity -- is a PURE FUNCTION of liveTime (no integration, no per-step
|
|
||||||
// RNG), exactly like insolation/tides/seasons (PlanetLive.cpp). That is what lets the
|
|
||||||
// live stepper rewind volcanoes for free: a smaller liveTime recomputes a smaller
|
|
||||||
// island and un-does eruptions; the ash already mixed into the (integrated) weather
|
|
||||||
// field reverts via the existing weather snapshot.
|
|
||||||
|
|
||||||
// Built height (m above baseElev) at a clock time. Discrete eruption "pulses" step the
|
namespace {
|
||||||
// cone up, capped at volcanoMaxHeight -- monotonic and a pure function of liveTime.
|
constexpr double YEAR_HOURS = 24.0 * 365.25;
|
||||||
double Planet::volcanoBuilt(const Volcano& v, double liveTime) const {
|
|
||||||
double age = liveTime - v.tStart;
|
uint32_t volNext(uint32_t& rng) {
|
||||||
if (age <= 0.0) return 0.0;
|
rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; return rng;
|
||||||
double pulses = std::floor(age * cfg.volcanoEruptFreq * std::max(0.0, v.activity));
|
}
|
||||||
return std::min(cfg.volcanoMaxHeight, pulses * cfg.volcanoBuildStep);
|
double volRf(uint32_t& rng) {
|
||||||
|
return (volNext(rng) & 0xFFFFFFu) / double(0x1000000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eruption intensity (0..1) at a clock time: a flare right after each pulse boundary,
|
double Planet::volcanoBuilt(const Volcano& v) const {
|
||||||
// decaying through the cycle -- so a vent mostly smoulders and briefly erupts. Pure
|
return std::max(0.0, v.built);
|
||||||
// function of liveTime.
|
}
|
||||||
double Planet::volcanoErupting(const Volcano& v, double liveTime) const {
|
|
||||||
double age = liveTime - v.tStart;
|
double Planet::volcanoErupting(const Volcano& v) const {
|
||||||
if (age < 0.0) return 0.0;
|
if (v.ashTimer > 0.0) return 1.0;
|
||||||
double prog = age * cfg.volcanoEruptFreq * std::max(0.0, v.activity);
|
if (v.phase == 1) return 0.0;
|
||||||
double frac = prog - std::floor(prog); // 0 just after a pulse .. 1 just before the next
|
if (v.activity <= cfg.volcanoDeadActivity) return 0.05;
|
||||||
return std::exp(-frac * 4.0); // ~1 at frac 0, ~0.13 at frac 0.5
|
double vigor = std::clamp(v.activity, 0.0, 1.0);
|
||||||
|
double heightGlow = std::clamp(v.built / std::max(1.0, cfg.volcanoMaxHeight), 0.0, 1.0);
|
||||||
|
return std::clamp(0.12 + 0.35 * vigor + 0.15 * heightGlow, 0.0, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place the volcano set by tectonic context. Reservoir-sampled to volcanoMaxCount so the
|
// Place the volcano set by tectonic context. Reservoir-sampled to volcanoMaxCount so the
|
||||||
// kept set is an unbiased random subset of all cells that pass their context probability.
|
// kept set is an unbiased random subset of all cells that pass their context probability.
|
||||||
void Planet::placeVolcanoes(double liveTime) {
|
void Planet::placeVolcanoes(double liveTime) {
|
||||||
|
(void)liveTime; // Lifecycle volcanoes carry state directly; the clock no longer defines height.
|
||||||
|
for (const Volcano& v : volcanoes) {
|
||||||
|
if (v.cell < 0 || v.cell >= (int)cells.size()) continue;
|
||||||
|
cells[v.cell].elevation = v.baseElev;
|
||||||
|
if (v.submarine) {
|
||||||
|
cells[v.cell].oceanic = true;
|
||||||
|
cells[v.cell].biome = Biome::Ocean;
|
||||||
|
}
|
||||||
|
}
|
||||||
volcanoes.clear();
|
volcanoes.clear();
|
||||||
sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
||||||
auto next = [&]() { sVolRng ^= sVolRng << 13; sVolRng ^= sVolRng >> 17; sVolRng ^= sVolRng << 5; return sVolRng; };
|
|
||||||
auto rf = [&]() { return (next() & 0xFFFFFFu) / double(0x1000000); };
|
|
||||||
const int n = (int)cells.size();
|
const int n = (int)cells.size();
|
||||||
const double sea = cfg.seaLevel;
|
const double sea = cfg.seaLevel;
|
||||||
const int cap = std::max(0, cfg.volcanoMaxCount);
|
const int cap = std::max(0, cfg.volcanoMaxCount);
|
||||||
@ -61,38 +65,53 @@ void Planet::placeVolcanoes(double liveTime) {
|
|||||||
if (ridge) { kind = 0; prob = cfg.volcanoProbRidge; }
|
if (ridge) { kind = 0; prob = cfg.volcanoProbRidge; }
|
||||||
else if (border) { kind = 1; prob = cfg.volcanoProbBorder; }
|
else if (border) { kind = 1; prob = cfg.volcanoProbBorder; }
|
||||||
else { kind = 2; prob = cfg.volcanoProbInterior; }
|
else { kind = 2; prob = cfg.volcanoProbInterior; }
|
||||||
if (rf() >= prob) continue;
|
if (volRf(sVolRng) >= prob) continue;
|
||||||
Volcano v;
|
Volcano v;
|
||||||
v.cell = i;
|
v.cell = i;
|
||||||
v.kind = (uint8_t)kind;
|
v.kind = (uint8_t)kind;
|
||||||
v.submarine = (cells[i].elevation <= sea) ? 1 : 0;
|
v.submarine = (cells[i].elevation <= sea) ? 1 : 0;
|
||||||
|
v.phase = 0;
|
||||||
v.baseElev = cells[i].elevation;
|
v.baseElev = cells[i].elevation;
|
||||||
v.tStart = liveTime;
|
|
||||||
double base = (kind == 0) ? 0.70 : (kind == 1) ? 0.50 : 0.35; // ridges more vigorous
|
double base = (kind == 0) ? 0.70 : (kind == 1) ? 0.50 : 0.35; // ridges more vigorous
|
||||||
v.activity = std::clamp(base + (rf() - 0.5) * 0.4, 0.05, 1.0);
|
v.activity = std::clamp(base + (volRf(sVolRng) - 0.5) * 0.4, 0.05, 1.0);
|
||||||
|
double skew = std::pow(volRf(sVolRng), 1.7); // mostly young, some pre-built
|
||||||
|
v.built = std::max(0.0, cfg.volcanoInitialBuildMax) * skew;
|
||||||
|
v.timer = v.ashTimer = v.ashCarry = 0.0;
|
||||||
++passed;
|
++passed;
|
||||||
if ((int)volcanoes.size() < cap) volcanoes.push_back(v);
|
if ((int)volcanoes.size() < cap) volcanoes.push_back(v);
|
||||||
else if (cap > 0) { uint32_t r = next() % (uint32_t)passed; if ((int)r < cap) volcanoes[r] = v; }
|
else if (cap > 0) { uint32_t r = volNext(sVolRng) % (uint32_t)passed; if ((int)r < cap) volcanoes[r] = v; }
|
||||||
}
|
}
|
||||||
for (int k = 0; k < (int)volcanoes.size(); ++k) volcanoes[k].id = (uint32_t)(k + 1);
|
for (int k = 0; k < (int)volcanoes.size(); ++k) volcanoes[k].id = (uint32_t)(k + 1);
|
||||||
|
stepVolcanoes(0.0); // show pre-built islands immediately on Live World entry.
|
||||||
}
|
}
|
||||||
|
|
||||||
// One live-frame volcano update. Reasserts each vent's cell elevation = baseElev +
|
VolcanoUpdate Planet::stepVolcanoes(double dtHours) {
|
||||||
// built(liveTime) (in Live World nothing else moves elevation, so this is safe &
|
|
||||||
// complete), breaches submarine vents into islands (and un-breaches them on a step
|
|
||||||
// back), and injects ash cloud + local cooling into the weather/climate fields.
|
|
||||||
VolcanoUpdate Planet::stepVolcanoes(double dtHours, double liveTime) {
|
|
||||||
VolcanoUpdate up;
|
VolcanoUpdate up;
|
||||||
if (volcanoes.empty()) return up;
|
if (volcanoes.empty()) return up;
|
||||||
const int n = (int)cells.size();
|
const int n = (int)cells.size();
|
||||||
const double sea = cfg.seaLevel;
|
const double sea = cfg.seaLevel;
|
||||||
for (Volcano& v : volcanoes) {
|
const double minDormYears = std::min(cfg.volcanoDormantMinYears, cfg.volcanoDormantMaxYears);
|
||||||
if (v.cell < 0 || v.cell >= n) continue;
|
const double maxDormYears = std::max(cfg.volcanoDormantMinYears, cfg.volcanoDormantMaxYears);
|
||||||
double newElev = v.baseElev + volcanoBuilt(v, liveTime);
|
const double minAshYears = std::min(cfg.volcanoAshMinYears, cfg.volcanoAshMaxYears);
|
||||||
|
const double maxAshYears = std::max(cfg.volcanoAshMinYears, cfg.volcanoAshMaxYears);
|
||||||
|
|
||||||
|
auto addAshCell = [&](int c, double cloud, double cooling) {
|
||||||
|
if (c < 0 || c >= n) return;
|
||||||
|
if (!sCloud.empty()) {
|
||||||
|
sCloud[c] = std::min(2.0, sCloud[c] + cloud);
|
||||||
|
if (!sHumidity.empty())
|
||||||
|
sHumidity[c] = std::min(2.0, sHumidity[c] + 0.3 * cloud);
|
||||||
|
}
|
||||||
|
if (!sLiveTemp.empty())
|
||||||
|
sLiveTemp[c] -= cooling;
|
||||||
|
};
|
||||||
|
auto reassertVent = [&](Volcano& v) {
|
||||||
|
if (v.cell < 0 || v.cell >= n) return;
|
||||||
|
v.built = std::max(0.0, v.built);
|
||||||
|
double newElev = v.baseElev + v.built;
|
||||||
double& e = cells[v.cell].elevation;
|
double& e = cells[v.cell].elevation;
|
||||||
if (std::fabs(newElev - e) > 0.5) up.recolor = true;
|
if (std::fabs(newElev - e) > 0.5) up.recolor = true;
|
||||||
e = newElev;
|
e = newElev;
|
||||||
// Submarine vent crossing sea level -> a new island (or, on a step back, re-submerged).
|
|
||||||
if (v.submarine) {
|
if (v.submarine) {
|
||||||
bool land = newElev > sea;
|
bool land = newElev > sea;
|
||||||
if (land && cells[v.cell].oceanic) {
|
if (land && cells[v.cell].oceanic) {
|
||||||
@ -101,21 +120,70 @@ VolcanoUpdate Planet::stepVolcanoes(double dtHours, double liveTime) {
|
|||||||
cells[v.cell].oceanic = true; cells[v.cell].biome = Biome::Ocean; up.breach = true;
|
cells[v.cell].oceanic = true; cells[v.cell].biome = Biome::Ocean; up.breach = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
double intensity = volcanoErupting(v, liveTime);
|
};
|
||||||
if (intensity > 0.05) {
|
auto blastAsh = [&](const Volcano& v) {
|
||||||
// Ash plume -> the integrated weather field (advects downwind, reverts on step-back via
|
if (v.cell < 0 || v.cell >= n) return;
|
||||||
// the weather snapshot). Only on a forward step (dtHours > 0).
|
double cosR = std::cos(std::max(0.0, cfg.volcanoBlastRadius));
|
||||||
if (dtHours > 0.0 && !sCloud.empty()) {
|
Vec3 center = cells[v.cell].unit;
|
||||||
int c = v.cell;
|
for (int i = 0; i < n; ++i) {
|
||||||
sCloud[c] = std::min(2.0, sCloud[c] + cfg.volcanoAshCloud * intensity * dtHours);
|
double d = center.dot(cells[i].unit);
|
||||||
if (!sHumidity.empty())
|
if (d < cosR) continue;
|
||||||
sHumidity[c] = std::min(2.0, sHumidity[c] + 0.3 * cfg.volcanoAshCloud * intensity * dtHours);
|
double t = (1.0 - cosR > 1e-9) ? std::clamp((d - cosR) / (1.0 - cosR), 0.0, 1.0) : 1.0;
|
||||||
|
addAshCell(i, cfg.volcanoBlastCloud * (0.35 + 0.65 * t), cfg.volcanoAshCooling * t);
|
||||||
}
|
}
|
||||||
// Local cooling under the plume: sLiveTemp is re-derived each frame, so subtracting here
|
};
|
||||||
// is itself a pure function of liveTime (consistent forward and backward).
|
auto sustainedAsh = [&](Volcano& v) {
|
||||||
if (!sLiveTemp.empty())
|
if (v.cell < 0 || v.cell >= n) return;
|
||||||
sLiveTemp[v.cell] -= cfg.volcanoAshCooling * intensity;
|
addAshCell(v.cell, cfg.volcanoAshCloud * dtHours, cfg.volcanoAshCooling);
|
||||||
|
v.ashCarry += cfg.volcanoAshPuffCellsPerWeek * dtHours / (24.0 * 7.0);
|
||||||
|
std::vector<int> candidates;
|
||||||
|
candidates.push_back(v.cell);
|
||||||
|
for (int j : cells[v.cell].neighbors) candidates.push_back(j);
|
||||||
|
int puffs = (int)std::floor(v.ashCarry);
|
||||||
|
v.ashCarry -= puffs;
|
||||||
|
if (volRf(sVolRng) < v.ashCarry) { ++puffs; v.ashCarry = 0.0; }
|
||||||
|
for (int k = 0; k < puffs && !candidates.empty(); ++k) {
|
||||||
|
int c = candidates[volNext(sVolRng) % (uint32_t)candidates.size()];
|
||||||
|
addAshCell(c, cfg.volcanoAshCloud, cfg.volcanoAshCooling * 0.5);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (Volcano& v : volcanoes) {
|
||||||
|
if (v.cell < 0 || v.cell >= n) continue;
|
||||||
|
if (dtHours > 0.0) {
|
||||||
|
if (v.ashTimer > 0.0) {
|
||||||
|
sustainedAsh(v);
|
||||||
|
v.ashTimer = std::max(0.0, v.ashTimer - dtHours);
|
||||||
|
}
|
||||||
|
if (v.phase == 1) {
|
||||||
|
v.timer -= dtHours;
|
||||||
|
if (v.timer <= 0.0) {
|
||||||
|
v.built *= std::clamp(1.0 - cfg.volcanoExplodeDropFrac, 0.0, 1.0);
|
||||||
|
double ashYears = minAshYears + (maxAshYears - minAshYears) * volRf(sVolRng);
|
||||||
|
v.ashTimer = ashYears * YEAR_HOURS;
|
||||||
|
v.ashCarry = 0.0;
|
||||||
|
blastAsh(v);
|
||||||
|
v.activity = std::clamp(v.activity * cfg.volcanoActivityDecay, 0.0, 1.0);
|
||||||
|
v.phase = 0;
|
||||||
|
v.timer = 0.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (v.activity > cfg.volcanoDeadActivity)
|
||||||
|
v.built += std::max(0.0, cfg.volcanoBuildRate) * v.activity * dtHours;
|
||||||
|
double absElev = v.baseElev + v.built;
|
||||||
|
if (absElev > cfg.volcanoFreeHeight) {
|
||||||
|
double heightP = std::clamp(v.built / std::max(1.0, cfg.volcanoMaxHeight), 0.01, 1.0);
|
||||||
|
double hazard = heightP * std::max(0.0, cfg.volcanoDormancyRate) * dtHours / YEAR_HOURS;
|
||||||
|
double pStep = 1.0 - std::exp(-hazard);
|
||||||
|
if (volRf(sVolRng) < pStep) {
|
||||||
|
double dormYears = minDormYears + (maxDormYears - minDormYears) * volRf(sVolRng);
|
||||||
|
v.phase = 1;
|
||||||
|
v.timer = dormYears * YEAR_HOURS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reassertVent(v);
|
||||||
}
|
}
|
||||||
return up;
|
return up;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,12 +30,14 @@ WeatherSnapshot Planet::captureWeather() const {
|
|||||||
WeatherSnapshot s;
|
WeatherSnapshot s;
|
||||||
s.humidity = sHumidity; s.cloud = sCloud; s.rain = sRain;
|
s.humidity = sHumidity; s.cloud = sCloud; s.rain = sRain;
|
||||||
s.storms = sStorms; s.rng = sWeatherRng; s.nextId = sStormNextId;
|
s.storms = sStorms; s.rng = sWeatherRng; s.nextId = sStormNextId;
|
||||||
|
s.volcanoes = volcanoes; s.volRng = sVolRng;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Planet::restoreWeather(const WeatherSnapshot& s) {
|
void Planet::restoreWeather(const WeatherSnapshot& s) {
|
||||||
sHumidity = s.humidity; sCloud = s.cloud; sRain = s.rain;
|
sHumidity = s.humidity; sCloud = s.cloud; sRain = s.rain;
|
||||||
sStorms = s.storms; sWeatherRng = s.rng; sStormNextId = s.nextId;
|
sStorms = s.storms; sWeatherRng = s.rng; sStormNextId = s.nextId;
|
||||||
|
volcanoes = s.volcanoes; sVolRng = s.volRng;
|
||||||
sHasWeather = !sHumidity.empty();
|
sHasWeather = !sHumidity.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
50
test_events.cpp
Normal file
50
test_events.cpp
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// Headless checks for the viewer event journal (no window needed).
|
||||||
|
|
||||||
|
#include "Viewer.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
static void check(bool cond, const char* what) {
|
||||||
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||||
|
if (!cond) ++failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::printf("Events: cap and save/load\n");
|
||||||
|
Viewer v;
|
||||||
|
PlanetConfig cfg; cfg.subdivisions = 2; cfg.seed = 4242;
|
||||||
|
v.cfg = cfg;
|
||||||
|
v.planet.generate(cfg);
|
||||||
|
for (int i = 0; i < 205; ++i) {
|
||||||
|
int cell = i % (int)v.planet.cells.size();
|
||||||
|
v.appendEvent(1, (uint8_t)(i % 3), (double)i, cell, (uint32_t)i,
|
||||||
|
"event " + std::to_string(i), "detail " + std::to_string(i));
|
||||||
|
}
|
||||||
|
check((int)v.events.size() == Viewer::EVENT_LOG_MAX, "event journal keeps the newest 200 entries");
|
||||||
|
check(v.events.front().title == "event 5", "oldest entries are trimmed first");
|
||||||
|
check(v.events.back().title == "event 204", "newest event is retained");
|
||||||
|
|
||||||
|
const char* path = "/tmp/fanworgen_event_test.save";
|
||||||
|
v.saveGame(path);
|
||||||
|
Viewer r;
|
||||||
|
r.loadGame(path);
|
||||||
|
check((int)r.events.size() == Viewer::EVENT_LOG_MAX, "v16 save/load restores event count");
|
||||||
|
check(!r.events.empty() && r.events.front().title == "event 5" && r.events.back().title == "event 204",
|
||||||
|
"v16 save/load restores event contents");
|
||||||
|
check(r.nextEventId == v.nextEventId, "v16 save/load restores next event id");
|
||||||
|
|
||||||
|
{
|
||||||
|
std::fstream fs(path, std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
uint32_t oldVer = 15;
|
||||||
|
fs.seekp(4);
|
||||||
|
fs.write((char*)&oldVer, 4);
|
||||||
|
}
|
||||||
|
Viewer old;
|
||||||
|
old.loadGame(path);
|
||||||
|
check(old.events.empty(), "pre-v16 saves load with an empty event journal");
|
||||||
|
|
||||||
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL EVENT CHECKS PASSED\n", failures);
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
171
test_volcano.cpp
171
test_volcano.cpp
@ -1,5 +1,5 @@
|
|||||||
// Headless test for Live World volcanoes (placement by tectonic context + eruption / island
|
// Headless test for Live World volcanoes (stateful growth, dormancy, explosions, ash,
|
||||||
// building). No display needed.
|
// rewind snapshots and save/load). No display needed.
|
||||||
//
|
//
|
||||||
// g++ -std=c++17 -O2 -Isrc/sim test_volcano.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \
|
// g++ -std=c++17 -O2 -Isrc/sim test_volcano.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 \
|
||||||
@ -7,11 +7,6 @@
|
|||||||
// src/sim/PlanetLive.cpp src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp \
|
// src/sim/PlanetLive.cpp src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp \
|
||||||
// src/sim/PlanetVolcano.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
// src/sim/PlanetVolcano.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
|
||||||
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tv && /tmp/tv
|
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tv && /tmp/tv
|
||||||
//
|
|
||||||
// Verifies: placement is deterministic + isolated from the tectonic RNG; the context classification
|
|
||||||
// (ridge / border / interior) drives where vents land and respects the probabilities; a submarine
|
|
||||||
// vent's built height is a monotonic PURE FUNCTION of liveTime that breaches sea level into an island
|
|
||||||
// and recedes when the clock steps back; and an eruption injects ash cloud at the vent.
|
|
||||||
|
|
||||||
#include "Planet.hpp"
|
#include "Planet.hpp"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@ -21,6 +16,8 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
static int failures = 0;
|
static int failures = 0;
|
||||||
|
static constexpr double YEAR_HOURS = 24.0 * 365.25;
|
||||||
|
|
||||||
static void check(bool cond, const char* what) {
|
static void check(bool cond, const char* what) {
|
||||||
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||||
if (!cond) ++failures;
|
if (!cond) ++failures;
|
||||||
@ -49,21 +46,32 @@ static int classify(const Planet& p, int i) {
|
|||||||
for (int j : p.cells[i].neighbors) { int pj = p.cells[j].plateId; if (pj != pid) border = true; if (isBaby(pj)) ridge = true; }
|
for (int j : p.cells[i].neighbors) { int pj = p.cells[j].plateId; if (pj != pid) border = true; if (isBaby(pj)) ridge = true; }
|
||||||
return ridge ? 0 : (border ? 1 : 2);
|
return ridge ? 0 : (border ? 1 : 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool sameVolcanoes(const std::vector<Volcano>& a, const std::vector<Volcano>& b) {
|
static bool sameVolcanoes(const std::vector<Volcano>& a, const std::vector<Volcano>& b) {
|
||||||
if (a.size() != b.size()) return false;
|
if (a.size() != b.size()) return false;
|
||||||
for (size_t i = 0; i < a.size(); ++i)
|
for (size_t i = 0; i < a.size(); ++i)
|
||||||
if (a[i].id != b[i].id || a[i].cell != b[i].cell || a[i].kind != b[i].kind
|
if (a[i].id != b[i].id || a[i].cell != b[i].cell || a[i].kind != b[i].kind
|
||||||
|| a[i].submarine != b[i].submarine || a[i].activity != b[i].activity
|
|| a[i].submarine != b[i].submarine || a[i].phase != b[i].phase
|
||||||
|| a[i].baseElev != b[i].baseElev || a[i].tStart != b[i].tStart) return false;
|
|| a[i].activity != b[i].activity || a[i].baseElev != b[i].baseElev
|
||||||
|
|| a[i].built != b[i].built || a[i].timer != b[i].timer
|
||||||
|
|| a[i].ashTimer != b[i].ashTimer || a[i].ashCarry != b[i].ashCarry) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int cloudRaisedCells(const Planet& p, const std::vector<double>& before, double eps = 1e-9) {
|
||||||
|
int raised = 0;
|
||||||
|
const auto& cloud = p.cloud();
|
||||||
|
for (size_t i = 0; i < cloud.size() && i < before.size(); ++i)
|
||||||
|
if (cloud[i] > before[i] + eps) ++raised;
|
||||||
|
return raised;
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 9090;
|
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 9090;
|
||||||
Planet p; p.generate(cfg); settle(p); drift(p, 120);
|
Planet p; p.generate(cfg); settle(p); drift(p, 120);
|
||||||
const int n = (int)p.cells.size();
|
const int n = (int)p.cells.size();
|
||||||
|
|
||||||
std::printf("Volcanoes: determinism\n");
|
std::printf("Volcanoes: deterministic placement\n");
|
||||||
p.placeVolcanoes(0.0); std::vector<Volcano> first = p.volcanoes;
|
p.placeVolcanoes(0.0); std::vector<Volcano> first = p.volcanoes;
|
||||||
p.placeVolcanoes(0.0);
|
p.placeVolcanoes(0.0);
|
||||||
check(!first.empty(), "placeVolcanoes places a non-empty set");
|
check(!first.empty(), "placeVolcanoes places a non-empty set");
|
||||||
@ -72,6 +80,7 @@ int main() {
|
|||||||
std::printf("Volcanoes: RNG isolation from tectonics\n");
|
std::printf("Volcanoes: RNG isolation from tectonics\n");
|
||||||
Planet a; a.generate(cfg); settle(a);
|
Planet a; a.generate(cfg); settle(a);
|
||||||
Planet b; b.generate(cfg); settle(b);
|
Planet b; b.generate(cfg); settle(b);
|
||||||
|
b.cfg.volcanoMaxCount = 0; // isolate RNG stream without intentionally changing terrain
|
||||||
for (int k = 0; k < 40; ++k) {
|
for (int k = 0; k < 40; ++k) {
|
||||||
double dta = a.cflDtMy(); a.advect(dta); a.step(); a.erode(dta);
|
double dta = a.cflDtMy(); a.advect(dta); a.step(); a.erode(dta);
|
||||||
double dtb = b.cflDtMy(); b.advect(dtb); b.step(); b.erode(dtb);
|
double dtb = b.cflDtMy(); b.advect(dtb); b.step(); b.erode(dtb);
|
||||||
@ -85,9 +94,8 @@ int main() {
|
|||||||
int eligRidge = 0, eligBorder = 0, eligInterior = 0;
|
int eligRidge = 0, eligBorder = 0, eligInterior = 0;
|
||||||
for (int i = 0; i < n; ++i) { int k = classify(p, i); if (k == 0) ++eligRidge; else if (k == 1) ++eligBorder; else ++eligInterior; }
|
for (int i = 0; i < n; ++i) { int k = classify(p, i); if (k == 0) ++eligRidge; else if (k == 1) ++eligBorder; else ++eligInterior; }
|
||||||
std::printf(" eligible cells: ridge %d, border %d, interior %d\n", eligRidge, eligBorder, eligInterior);
|
std::printf(" eligible cells: ridge %d, border %d, interior %d\n", eligRidge, eligBorder, eligInterior);
|
||||||
// probs ridge=border=1, interior=0, no cap -> exactly the ridge+border cells, none interior.
|
|
||||||
p.cfg.volcanoProbRidge = 1.0; p.cfg.volcanoProbBorder = 1.0; p.cfg.volcanoProbInterior = 0.0;
|
p.cfg.volcanoProbRidge = 1.0; p.cfg.volcanoProbBorder = 1.0; p.cfg.volcanoProbInterior = 0.0;
|
||||||
p.cfg.volcanoMaxCount = 1000000;
|
p.cfg.volcanoMaxCount = 1000000; p.cfg.volcanoInitialBuildMax = 0.0;
|
||||||
p.placeVolcanoes(0.0);
|
p.placeVolcanoes(0.0);
|
||||||
bool noInterior = true; for (const Volcano& v : p.volcanoes) if (v.kind == 2) noInterior = false;
|
bool noInterior = true; for (const Volcano& v : p.volcanoes) if (v.kind == 2) noInterior = false;
|
||||||
check(noInterior, "interior prob 0 places no interior vents");
|
check(noInterior, "interior prob 0 places no interior vents");
|
||||||
@ -108,54 +116,111 @@ int main() {
|
|||||||
check(rR >= rB, "young-ridge cells are the likeliest of all");
|
check(rR >= rB, "young-ridge cells are the likeliest of all");
|
||||||
} else std::printf(" (no young-ridge cells this seed -- ridge rate not asserted)\n");
|
} else std::printf(" (no young-ridge cells this seed -- ridge rate not asserted)\n");
|
||||||
|
|
||||||
std::printf("Volcanoes: build is a pure function of liveTime; submarine vent breaches into an island\n");
|
std::printf("Volcanoes: initial built height can make islands immediately\n");
|
||||||
Planet q; q.generate(cfg); settle(q); drift(q, 120);
|
Planet pre; pre.generate(cfg); settle(pre); drift(pre, 120);
|
||||||
q.cfg.volcanoMaxHeight = 9000.0; q.cfg.volcanoBuildStep = 130.0; q.cfg.volcanoEruptFreq = 0.05;
|
pre.cfg.volcanoProbRidge = pre.cfg.volcanoProbBorder = pre.cfg.volcanoProbInterior = 1.0;
|
||||||
q.placeVolcanoes(0.0);
|
pre.cfg.volcanoMaxCount = 1000000;
|
||||||
int vi = -1; double best = -1e18;
|
pre.cfg.volcanoInitialBuildMax = 10000.0;
|
||||||
for (size_t k = 0; k < q.volcanoes.size(); ++k)
|
pre.placeVolcanoes(0.0);
|
||||||
if (q.volcanoes[k].submarine && q.volcanoes[k].baseElev > best) { best = q.volcanoes[k].baseElev; vi = (int)k; }
|
bool someBuilt = false, instantIsland = false;
|
||||||
check(vi >= 0, "at least one submarine volcano was placed");
|
for (const Volcano& v : pre.volcanoes) {
|
||||||
if (vi >= 0) {
|
if (v.built > 0.0) someBuilt = true;
|
||||||
const Volcano v = q.volcanoes[vi];
|
if (v.submarine && pre.cells[v.cell].elevation > pre.cfg.seaLevel && !pre.cells[v.cell].oceanic)
|
||||||
double b0 = q.volcanoBuilt(v, 0.0), b1 = q.volcanoBuilt(v, 5000.0),
|
instantIsland = true;
|
||||||
b2 = q.volcanoBuilt(v, 50000.0), b3 = q.volcanoBuilt(v, 500000.0);
|
}
|
||||||
check(b0 <= b1 && b1 <= b2 && b2 <= b3, "built height is monotonic in liveTime");
|
check(someBuilt, "placement assigns nonzero pre-built height");
|
||||||
check(b3 > b0, "a submarine vent builds up over time");
|
check(instantIsland, "a pre-built submarine vent can breach into an island on entry");
|
||||||
check(q.volcanoBuilt(v, 5000.0) == b1, "volcanoBuilt is deterministic (pure function of t)");
|
|
||||||
q.stepVolcanoes(1.0, 500000.0);
|
std::printf("Volcanoes: forward stepping grows statefully\n");
|
||||||
check(q.cells[v.cell].elevation > q.cfg.seaLevel, "submarine volcano breaches sea level into an island");
|
Planet g; g.generate(cfg); settle(g); drift(g, 80);
|
||||||
check(!q.cells[v.cell].oceanic, "the breached island is land crust");
|
g.cfg.volcanoProbRidge = g.cfg.volcanoProbBorder = g.cfg.volcanoProbInterior = 1.0;
|
||||||
// Step the clock back to the start: the island must recede (pure function of liveTime).
|
g.cfg.volcanoMaxCount = 1; g.cfg.volcanoInitialBuildMax = 0.0;
|
||||||
q.stepVolcanoes(0.0, 0.0);
|
g.cfg.volcanoBuildRate = 10.0; g.cfg.volcanoFreeHeight = 1e9; g.cfg.volcanoDeadActivity = 0.0;
|
||||||
check(q.cells[v.cell].elevation <= q.cfg.seaLevel + 1e-6, "stepping the clock back recedes the island");
|
g.placeVolcanoes(0.0);
|
||||||
check(std::fabs(q.cells[v.cell].elevation - (v.baseElev + q.volcanoBuilt(v, 0.0))) < 1e-6,
|
check(!g.volcanoes.empty(), "one growth-test volcano placed");
|
||||||
"vent elevation = baseElev + built(liveTime)");
|
if (!g.volcanoes.empty()) {
|
||||||
|
g.volcanoes[0].activity = 1.0;
|
||||||
|
double b0 = g.volcanoes[0].built;
|
||||||
|
g.stepVolcanoes(2.0);
|
||||||
|
check(g.volcanoes[0].built > b0 + 19.9, "growing vent integrates built height forward");
|
||||||
|
check(std::fabs(g.cells[g.volcanoes[0].cell].elevation - (g.volcanoes[0].baseElev + g.volcanoes[0].built)) < 1e-6,
|
||||||
|
"vent elevation is reasserted from baseElev + built");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::printf("Volcanoes: an eruption injects ash cloud\n");
|
std::printf("Volcanoes: forced dormancy, explosion, ash blast and activity decay\n");
|
||||||
Planet w; w.generate(cfg); settle(w);
|
Planet x; x.generate(cfg); settle(x); drift(x, 80);
|
||||||
w.initWeather();
|
x.initWeather();
|
||||||
w.computeInsolation(0.25, 0.3);
|
x.computeInsolation(0.25, 0.3);
|
||||||
w.placeVolcanoes(0.0); // tStart = 0 -> at liveTime 0 every vent is at peak eruption intensity
|
x.computeLiveSeason(0.25);
|
||||||
check(!w.volcanoes.empty(), "volcanoes placed for the ash test");
|
x.cfg.volcanoProbRidge = x.cfg.volcanoProbBorder = x.cfg.volcanoProbInterior = 1.0;
|
||||||
if (!w.volcanoes.empty()) {
|
x.cfg.volcanoMaxCount = 1; x.cfg.volcanoInitialBuildMax = 0.0;
|
||||||
std::vector<double> before = w.cloud();
|
x.cfg.volcanoFreeHeight = -1e9; x.cfg.volcanoMaxHeight = 1.0;
|
||||||
w.stepVolcanoes(1.0, 0.0); // dtHours > 0 -> inject ash
|
x.cfg.volcanoDormancyRate = YEAR_HOURS * 1000.0;
|
||||||
bool rose = false;
|
x.cfg.volcanoDormantMinYears = x.cfg.volcanoDormantMaxYears = 0.0;
|
||||||
for (const Volcano& vv : w.volcanoes)
|
x.cfg.volcanoExplodeDropFrac = 0.20; x.cfg.volcanoActivityDecay = 0.70;
|
||||||
if (w.cloud()[vv.cell] > before[vv.cell] + 1e-9) rose = true;
|
x.cfg.volcanoAshMinYears = x.cfg.volcanoAshMaxYears = 0.01;
|
||||||
check(rose, "an erupting vent thickens the cloud at its cell");
|
x.cfg.volcanoBlastRadius = 0.09; x.cfg.volcanoBlastCloud = 1.5;
|
||||||
|
x.cfg.volcanoAshPuffCellsPerWeek = 100.0;
|
||||||
|
x.placeVolcanoes(0.0);
|
||||||
|
check(!x.volcanoes.empty(), "one lifecycle-test volcano placed");
|
||||||
|
if (!x.volcanoes.empty()) {
|
||||||
|
x.volcanoes[0].built = 2000.0;
|
||||||
|
x.volcanoes[0].activity = 1.0;
|
||||||
|
x.volcanoes[0].phase = 0;
|
||||||
|
x.stepVolcanoes(0.0);
|
||||||
|
x.stepVolcanoes(1.0);
|
||||||
|
check(x.volcanoes[0].phase == 1, "tall growing vent can go dormant");
|
||||||
|
double beforeBuilt = x.volcanoes[0].built;
|
||||||
|
double beforeActivity = x.volcanoes[0].activity;
|
||||||
|
std::vector<double> beforeCloud = x.cloud();
|
||||||
|
x.stepVolcanoes(1.0);
|
||||||
|
check(x.volcanoes[0].phase == 0, "dormant vent explodes and returns to growing");
|
||||||
|
check(x.volcanoes[0].built < beforeBuilt * 0.81, "explosion shaves the peak");
|
||||||
|
check(x.volcanoes[0].ashTimer > 0.0, "explosion starts sustained ash emission");
|
||||||
|
check(x.volcanoes[0].activity < beforeActivity, "explosion decays activity");
|
||||||
|
check(cloudRaisedCells(x, beforeCloud) >= 20, "explosion blasts ash over a wide cell radius");
|
||||||
|
beforeCloud = x.cloud();
|
||||||
|
x.stepVolcanoes(24.0 * 7.0);
|
||||||
|
check(cloudRaisedCells(x, beforeCloud) > 0, "post-explosion ashTimer keeps puffing ash");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::printf("Volcanoes: save v14 round-trip\n");
|
std::printf("Volcanoes: snapshot restore reverses lifecycle state\n");
|
||||||
|
if (!x.volcanoes.empty()) {
|
||||||
|
WeatherSnapshot snap = x.captureWeather();
|
||||||
|
std::vector<Volcano> saved = x.volcanoes;
|
||||||
|
x.volcanoes[0].built += 500.0;
|
||||||
|
x.volcanoes[0].phase = 1;
|
||||||
|
x.volcanoes[0].timer = 123.0;
|
||||||
|
x.stepVolcanoes(0.0);
|
||||||
|
x.restoreWeather(snap);
|
||||||
|
x.stepVolcanoes(0.0);
|
||||||
|
check(sameVolcanoes(saved, x.volcanoes), "captureWeather/restoreWeather round-trips volcano state");
|
||||||
|
check(std::fabs(x.cells[x.volcanoes[0].cell].elevation - (x.volcanoes[0].baseElev + x.volcanoes[0].built)) < 1e-6,
|
||||||
|
"restored volcano state reasserts terrain");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Volcanoes: save v15 round-trip and v14 discard path\n");
|
||||||
{
|
{
|
||||||
|
x.cfg.volcanoDormancyRate = 1.0; // keep saved config inside normal validation bounds
|
||||||
|
x.cfg.volcanoFreeHeight = 1000.0;
|
||||||
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
q.writeState(ss);
|
x.writeState(ss);
|
||||||
|
ss.seekg(0);
|
||||||
Planet r;
|
Planet r;
|
||||||
bool ok = r.readState(ss, true, true, true, true, true, true);
|
bool ok = r.readState(ss, true, true, true, true, true, true, true);
|
||||||
check(ok, "readState accepts a v14 stream");
|
check(ok, "readState accepts a v15 stream");
|
||||||
check(sameVolcanoes(q.volcanoes, r.volcanoes), "volcano set round-trips through save");
|
check(sameVolcanoes(x.volcanoes, r.volcanoes), "stateful volcanoes round-trip through save");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Planet old; old.generate(cfg); settle(old);
|
||||||
|
old.volcanoes.clear(); // empty old block is layout-compatible and still exercises discard.
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
old.writeState(ss);
|
||||||
|
ss.seekg(0);
|
||||||
|
Planet r;
|
||||||
|
bool ok = r.readState(ss, true, true, true, true, true, true, false);
|
||||||
|
check(ok, "readState consumes a v14 volcano block");
|
||||||
|
check(r.volcanoes.empty(), "v14 volcanoes are discarded for lifecycle reseeding");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL VOLCANO CHECKS PASSED\n", failures);
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL VOLCANO CHECKS PASSED\n", failures);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user