Add edit mode: manually edit every cell/settlement property (save v24)
F3 opens a tabbed panel (Geology/Climate/Biota/Settlement) for the selected tile. Every field writes straight into the field the sim already reads next tick (Planet::setXxx(), PlanetEdit.cpp), so an unlocked edit is a one-off nudge the simulation keeps evolving afterward; a per-field lock button sets a new per-cell EditLock bitmask that exempts it from automatic recompute (tectonics/erosion/ hydrology/drift/biomes skip a locked cell's write; climate/biota- density/habitability, which have no other persistent storage, pin a value in a small sparse map instead). Settlement population/ allegiance/culture locks are keyed by the settlement's home cell. Adds headless test_edit.cpp (nudge vs. lock for every field, save round-trip, corrupt-stream rejection, pre-v24 compatibility) and fixes a real bug found while testing: raylib's default ESC-exits-app behavior collided with edit mode's Escape-to-cancel, so SetExitKey is now disabled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
This commit is contained in:
parent
ab2c4024f8
commit
8fec3d82ad
54
CLAUDE.md
54
CLAUDE.md
@ -288,6 +288,28 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived
|
|||||||
shows a schism child's founding year. Knobs `cult*`.
|
shows a schism child's founding year. Knobs `cult*`.
|
||||||
*Next steps (not yet built): tribute / vassalage treaties; an accumulated treasury (funding armies /
|
*Next steps (not yet built): tribute / vassalage treaties; an accumulated treasury (funding armies /
|
||||||
buying peace).*
|
buying peace).*
|
||||||
|
- **Edit mode (manual world editing)** *(done — see `PlanetEdit.cpp`, save v24)* — the "between phases
|
||||||
|
the user can edit the world" hook from the project's roadmap. Key **`F3`** (a settled world; auto-pauses)
|
||||||
|
toggles it; click a tile to open an **Edit Mode** panel (replaces the detail panel) tabbed **Geology**
|
||||||
|
(elevation, plate, crust type, geological age, biome) / **Climate** (temperature, moisture) / **Biota**
|
||||||
|
(flora/fauna/funga density + the discrete population list, add/remove organisms) / **Settlement**
|
||||||
|
(population, allegiance, culture — only shown when the tile hosts one). Each row is a `Planet::setXxx()`
|
||||||
|
call (`PlanetEdit.cpp`) that writes straight into the field the sim already reads next tick — click a
|
||||||
|
numeric value to type a new one (Enter commits, Escape cancels) or scroll the wheel over it to nudge;
|
||||||
|
click a discrete field (plate/biome/crust/allegiance/culture) to cycle or pick from a list; click **+
|
||||||
|
add** to insert a biota organism from the archetype table. Every field also has a **lock** button —
|
||||||
|
unlocked, an edit is a one-off "nudge" the simulation keeps evolving afterward (like a meteor impact);
|
||||||
|
locked, a new per-cell `EditLock` bitmask (`PlanetTypes.hpp`) exempts that field from its normal per-tick
|
||||||
|
recompute (`PlanetTectonics`/`Erosion`/`Hydrology`/`Drift`/`Biomes.cpp` skip a locked cell's write;
|
||||||
|
`PlanetClimate`/`FloraGen`/`FaunaGen`/`FungiGen`/`Civ.cpp` re-apply a pinned value from a small sparse
|
||||||
|
map after their normal recompute, since those fields have no other persistent storage of their own) until
|
||||||
|
unlocked again. Settlement locks are keyed by the settlement's home cell and gate `stepCivilization()` /
|
||||||
|
`stepConflict()` / `stepCulture()` for it. Elevation/plate/crust/geoAge/biome locks are a bare bit (no
|
||||||
|
extra storage — the Cell field itself is already the save-format source of truth); a plate **fission/
|
||||||
|
merge** event (whole-plate cell reassignment) is not individually lock-checked. **Save v24** appends the
|
||||||
|
per-cell `editLock` bitmask + the sparse locked-value maps (climate/biota-density/habitability); older
|
||||||
|
saves load with no locks. Headless `test_edit.cpp`: nudge-vs-lock behaviour for every field, organism
|
||||||
|
add/remove, save v24 round-trip, corrupt-stream rejection, pre-v24 compatibility.
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
@ -701,13 +723,14 @@ src/
|
|||||||
PlanetCulture.* computeCultures (derived refresh + one-time seeding; civ Step 4) + stepCulture (assimilation/conversion/schism; civ Step 8, save v23)
|
PlanetCulture.* computeCultures (derived refresh + one-time seeding; civ Step 4) + stepCulture (assimilation/conversion/schism; civ Step 8, save v23)
|
||||||
PlanetConflict.* stepConflict (wars/conquest/revolts + diplomacy/alliances/coalitions; civ Steps 5-6, save v21/v22)
|
PlanetConflict.* stepConflict (wars/conquest/revolts + diplomacy/alliances/coalitions; civ Steps 5-6, save v21/v22)
|
||||||
PlanetTrade.* computeTrade (trade routes + prosperity/wealth feeding growth; civ Step 7, derived)
|
PlanetTrade.* computeTrade (trade routes + prosperity/wealth feeding growth; civ Step 7, derived)
|
||||||
|
PlanetEdit.cpp manual cell/settlement edits + the EditLock lock mechanism (Edit Mode, save v24)
|
||||||
PlanetIO.cpp config file (text) + binary save/load
|
PlanetIO.cpp config file (text) + binary save/load
|
||||||
render/ (raylib viewer)
|
render/ (raylib viewer)
|
||||||
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
||||||
Map2D.* Equal Earth 2D map: positions + projection/draw helpers
|
Map2D.* Equal Earth 2D map: positions + projection/draw helpers
|
||||||
Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids
|
Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids
|
||||||
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
|
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
|
||||||
Panels.* right-column UI: detail panel, hover info, world stats
|
Panels.* right-column UI: detail panel, hover info, world stats, edit-mode panel
|
||||||
Viewer.{hpp,cpp} Viewer struct: all state + setup + sim orchestration
|
Viewer.{hpp,cpp} Viewer struct: all state + setup + sim orchestration
|
||||||
ViewerInput.cpp handleInput(): camera, hover picking, click, keys
|
ViewerInput.cpp handleInput(): camera, hover picking, click, keys
|
||||||
ViewerRender.cpp renderGlobe3D / renderMap2D / renderPanels / renderHUD / renderPrompt
|
ViewerRender.cpp renderGlobe3D / renderMap2D / renderPanels / renderHUD / renderPrompt
|
||||||
@ -765,12 +788,13 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
|||||||
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
|
||||||
src/sim/NameGen.cpp src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp \
|
src/sim/NameGen.cpp src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp \
|
||||||
src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp src/sim/PlanetTrade.cpp \
|
src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp src/sim/PlanetTrade.cpp \
|
||||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
src/sim/PlanetEdit.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
```
|
```
|
||||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
||||||
`test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp`, `test_civ.cpp`,
|
`test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp`, `test_civ.cpp`,
|
||||||
`test_nation.cpp`, `test_culture.cpp`, `test_conflict.cpp`, `test_diplomacy.cpp`, `test_trade.cpp`, `test_colony.cpp` or `test_cultevo.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
`test_nation.cpp`, `test_culture.cpp`, `test_conflict.cpp`, `test_diplomacy.cpp`, `test_trade.cpp`, `test_colony.cpp`,
|
||||||
Civilization / Nation / Culture / Conflict / Diplomacy / Trade / Colony / Cultural-evolution suites — same source list. CMake also builds `test_events` for the
|
`test_cultevo.cpp` or `test_edit.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
||||||
|
Civilization / Nation / Culture / Conflict / Diplomacy / Trade / Colony / Cultural-evolution / Edit-mode suites — same source list. CMake also builds `test_events` for the
|
||||||
viewer event journal.)
|
viewer event journal.)
|
||||||
|
|
||||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||||
@ -819,6 +843,8 @@ zooms toward the cursor (drag pans when zoomed) · `F` fast-forward Phase-1 form
|
|||||||
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
||||||
on a settled world; re-press regenerates) · `W` enter/leave **Live World** (settled world) ·
|
on a settled world; re-press regenerates) · `W` enter/leave **Live World** (settled world) ·
|
||||||
`R` reseed ·
|
`R` reseed ·
|
||||||
|
`F3` **edit mode** (a settled world; click a tile to edit every property — geology/climate/biota/
|
||||||
|
settlement — with per-field lock against automatic recompute; see below) ·
|
||||||
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
||||||
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
||||||
|
|
||||||
@ -850,6 +876,17 @@ cuts the routes between belligerents, and the **Civ** tab shows each settlement'
|
|||||||
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**
|
||||||
weather/storms/volcano lifecycle **and wars/conquests and cultural shifts** via snapshots). Mouse-wheel over the 2D map zooms (drag pans).
|
weather/storms/volcano lifecycle **and wars/conquests and cultural shifts** via snapshots). Mouse-wheel over the 2D map zooms (drag pans).
|
||||||
|
|
||||||
|
Edit mode (`F3`, a settled world; auto-pauses): click a tile to open an **Edit Mode** panel in place
|
||||||
|
of the detail panel, tabbed **Geology** / **Climate** / **Biota** / **Settlement** (the last only
|
||||||
|
when the tile hosts one). Click a numeric value to type a new one (**Enter** commits, **Escape**
|
||||||
|
cancels) or scroll the mouse wheel over it to nudge; click a discrete field (plate/biome/crust type/
|
||||||
|
allegiance/culture) to pick from a list; click **+ add** to place a biota organism, **×** to remove
|
||||||
|
one. Every field has its own **lock** button — unlocked, an edit is a one-off nudge the simulation
|
||||||
|
keeps evolving afterward (like a meteor impact); locked, that field is exempted from its normal
|
||||||
|
automatic recompute until unlocked again (elevation resists erosion/relaxation, biome/climate/biota-
|
||||||
|
density/habitability stay pinned, a settlement's population/allegiance/culture stop changing on
|
||||||
|
their own). Saved (v24).
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
@ -859,7 +896,7 @@ PlanetConfig param, auto-created on first run, reload with `F2`) and
|
|||||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
||||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
||||||
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
||||||
save header is versioned (currently **23**; v2 adds the `[`/`]` drift rate, v3 a
|
save header is versioned (currently **24**; v2 adds the `[`/`]` drift rate, v3 a
|
||||||
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
||||||
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
||||||
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
||||||
@ -877,7 +914,10 @@ to the step-back frames so a load can rewind conquests, v22 the **diplomacy** bl
|
|||||||
**relations** (alliances / rivalries / truces), likewise per-frame, and v23 the **culture** block —
|
**relations** (alliances / rivalries / truces), likewise per-frame, and v23 the **culture** block —
|
||||||
stateful culture identities (append-only; schism children) + the per-settlement culture + a next-id
|
stateful culture identities (append-only; schism children) + the per-settlement culture + a next-id
|
||||||
counter (cultural evolution), likewise per-frame (per-settlement vector + the list *length*; a rewind
|
counter (cultural evolution), likewise per-frame (per-settlement vector + the list *length*; a rewind
|
||||||
truncates, a replay re-creates);
|
truncates, a replay re-creates), and v24 the **edit mode** block — a per-cell `editLock` bitmask
|
||||||
|
(manual-edit locks, `F3`) as a trailing vector plus sparse locked-value maps for the climate/biota-
|
||||||
|
density/habitability fields (which have no other persistent storage — the map only holds an entry
|
||||||
|
while its lock bit is set);
|
||||||
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
|
||||||
@ -890,7 +930,7 @@ demand via `M`); pre-v19 saves load with no ecoregions (regenerated via `E`); pr
|
|||||||
no settlements (re-seeded via `U`); pre-v21 saves load with no wars (everyone independent; wars begin
|
no settlements (re-seeded via `U`); pre-v21 saves load with no wars (everyone independent; wars begin
|
||||||
again as the clock runs); pre-v22 saves load with no diplomacy (relations re-form as the clock runs);
|
again as the clock runs); pre-v22 saves load with no diplomacy (relations re-form as the clock runs);
|
||||||
pre-v23 saves load with no culture state (re-seeded one-per-continent on the next refresh — the same
|
pre-v23 saves load with no culture state (re-seeded one-per-continent on the next refresh — the same
|
||||||
peoples as before; evolution starts from there).
|
peoples as before; evolution starts from there); pre-v24 saves load with no manual edits/locks.
|
||||||
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
||||||
saved one.
|
saved one.
|
||||||
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
||||||
|
|||||||
@ -40,6 +40,7 @@ set(SIM_SOURCES
|
|||||||
src/sim/PlanetCulture.cpp
|
src/sim/PlanetCulture.cpp
|
||||||
src/sim/PlanetConflict.cpp
|
src/sim/PlanetConflict.cpp
|
||||||
src/sim/PlanetTrade.cpp
|
src/sim/PlanetTrade.cpp
|
||||||
|
src/sim/PlanetEdit.cpp
|
||||||
src/sim/PlanetIO.cpp
|
src/sim/PlanetIO.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ if(UNIX AND NOT APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture conflict diplomacy trade colony cultevo)
|
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture conflict diplomacy trade colony cultevo edit)
|
||||||
add_executable(test_${test_name} test_${test_name}.cpp)
|
add_executable(test_${test_name} test_${test_name}.cpp)
|
||||||
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
||||||
add_test(NAME ${test_name} COMMAND test_${test_name})
|
add_test(NAME ${test_name} COMMAND test_${test_name})
|
||||||
|
|||||||
@ -298,6 +298,186 @@ void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw one editable row: a label + a value box (click to type / toggle / open a picker) + its
|
||||||
|
// lock-toggle button. Rows sharing a lock bit (e.g. Crust + GeoAge -> LockCrust) each draw their
|
||||||
|
// own button wired to the same id -- redundant but simplest, and both stay visually in sync since
|
||||||
|
// the lock state is re-read from the cell every frame. Advances and returns `y`.
|
||||||
|
static int editRow(EditPanelState& ed, EditField valueId, const std::string& label, const std::string& value,
|
||||||
|
EditField lockId, bool locked, int x, int y, int w) {
|
||||||
|
Rectangle r{ (float)x, (float)y, (float)(w - 72), 22.0f };
|
||||||
|
bool active = (ed.editingField == valueId);
|
||||||
|
DrawRectangleRec(r, active ? Color{40, 46, 64, 255} : Color{22, 24, 34, 255});
|
||||||
|
DrawRectangleLinesEx(r, 1, Color{70, 74, 90, 255});
|
||||||
|
DrawText(label.c_str(), (int)r.x + 4, (int)r.y + 3, 14, Color{190, 195, 210, 255});
|
||||||
|
std::string val = active ? (ed.editBuffer + "_") : value;
|
||||||
|
int vw = MeasureText(val.c_str(), 14);
|
||||||
|
DrawText(val.c_str(), (int)(r.x + r.width) - vw - 6, (int)r.y + 3, 14, active ? Color{255, 230, 140, 255} : RAYWHITE);
|
||||||
|
ed.fieldRects.push_back(r); ed.fieldIds.push_back(valueId);
|
||||||
|
|
||||||
|
Rectangle lr{ r.x + r.width + 4, r.y, 64.0f, 22.0f };
|
||||||
|
DrawRectangleRec(lr, locked ? Color{120, 60, 50, 255} : Color{34, 50, 40, 255});
|
||||||
|
DrawRectangleLinesEx(lr, 1, Color{100, 104, 120, 255});
|
||||||
|
const char* t = locked ? "LOCKED" : "lock";
|
||||||
|
int tw = MeasureText(t, 12);
|
||||||
|
DrawText(t, (int)(lr.x + lr.width / 2 - tw / 2), (int)lr.y + 5, 12, locked ? Color{255, 210, 200, 255} : Color{190, 220, 195, 255});
|
||||||
|
ed.lockRects.push_back(lr); ed.lockIds.push_back(lockId);
|
||||||
|
return y + 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawEditPanel(const Planet& p, int cellIdx, Rectangle panel, EditPanelState& ed) {
|
||||||
|
ed.tabRects.clear(); ed.fieldRects.clear(); ed.fieldIds.clear();
|
||||||
|
ed.lockRects.clear(); ed.lockIds.clear();
|
||||||
|
ed.pickerRects.clear(); ed.pickerValues.clear();
|
||||||
|
ed.removeRects.clear(); ed.removeKind.clear(); ed.removeIndex.clear();
|
||||||
|
if (cellIdx < 0 || cellIdx >= (int)p.cells.size()) return;
|
||||||
|
|
||||||
|
DrawRectangleRec(panel, Color{12, 14, 22, 235});
|
||||||
|
DrawRectangleLinesEx(panel, 2, Color{210, 150, 70, 255}); // orange border marks edit mode
|
||||||
|
int x = (int)panel.x + 10, y = (int)panel.y + 8;
|
||||||
|
int w = (int)panel.width - 20;
|
||||||
|
int maxY = (int)(panel.y + panel.height) - 8;
|
||||||
|
DrawText(TextFormat("EDIT MODE - Tile #%d", cellIdx), x, y, 18, Color{255, 200, 120, 255});
|
||||||
|
const char* hint = "F3: exit";
|
||||||
|
DrawText(hint, (int)(panel.x + panel.width) - MeasureText(hint, 13) - 10, y + 3, 13, Color{170, 170, 185, 255});
|
||||||
|
y += 26;
|
||||||
|
|
||||||
|
const Cell& c = p.cells[cellIdx];
|
||||||
|
bool hasSettle = p.settlementsPlaced() && cellIdx < (int)p.cellSettlement().size() && p.cellSettlement()[cellIdx] >= 0;
|
||||||
|
|
||||||
|
const char* tabNames[4] = { "Geology", "Climate", "Biota", "Settlement" };
|
||||||
|
int nTabs = hasSettle ? 4 : 3;
|
||||||
|
if (ed.tab >= nTabs) ed.tab = 0;
|
||||||
|
int tabW = w / nTabs;
|
||||||
|
for (int i = 0; i < nTabs; ++i) {
|
||||||
|
Rectangle tr{ (float)(x + i * tabW), (float)y, (float)(tabW - 2), 24.0f };
|
||||||
|
bool on = (ed.tab == i);
|
||||||
|
DrawRectangleRec(tr, on ? Color{60, 66, 92, 255} : Color{28, 30, 42, 255});
|
||||||
|
DrawRectangleLinesEx(tr, 1, Color{90, 94, 115, 255});
|
||||||
|
int tw = MeasureText(tabNames[i], 14);
|
||||||
|
DrawText(tabNames[i], (int)(tr.x + tr.width / 2 - tw / 2), (int)tr.y + 5, 14, on ? RAYWHITE : Color{170, 175, 190, 255});
|
||||||
|
ed.tabRects.push_back(tr);
|
||||||
|
}
|
||||||
|
y += 30;
|
||||||
|
|
||||||
|
// An open picker list replaces the row list for this tab until a row (or the tab strip) is clicked.
|
||||||
|
if (ed.pickerField != EditField::None) {
|
||||||
|
DrawText("Select a value (click a tab to cancel):", x, y, 13, Color{190, 195, 210, 255}); y += 20;
|
||||||
|
auto pickRow = [&](const std::string& label, int value) {
|
||||||
|
if (y + 22 > maxY) return;
|
||||||
|
Rectangle r{ (float)x, (float)y, (float)w, 22.0f };
|
||||||
|
DrawRectangleLinesEx(r, 1, Color{70, 74, 90, 255});
|
||||||
|
DrawText(label.c_str(), x + 4, y + 3, 14, RAYWHITE);
|
||||||
|
ed.pickerRects.push_back(r); ed.pickerValues.push_back(value);
|
||||||
|
y += 24;
|
||||||
|
};
|
||||||
|
switch (ed.pickerField) {
|
||||||
|
case EditField::PlateId:
|
||||||
|
for (int pi = 0; pi < (int)p.plates.size(); ++pi) {
|
||||||
|
if (p.plates[pi].baby) continue;
|
||||||
|
pickRow(TextFormat("Plate %d (%s)", pi, p.plates[pi].type == PlateType::Oceanic ? "Oceanic" : "Continental"), pi);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EditField::Biome:
|
||||||
|
for (int b = 0; b <= (int)Biome::Mountains; ++b) pickRow(biomeName((Biome)b), b);
|
||||||
|
break;
|
||||||
|
case EditField::Allegiance:
|
||||||
|
pickRow("Independent", -1);
|
||||||
|
for (size_t k = 0; k < p.settlements.size(); ++k)
|
||||||
|
if (p.settlements[k].population >= p.cfg.civAbandonPop) pickRow(p.settlements[k].name, (int)k);
|
||||||
|
break;
|
||||||
|
case EditField::Culture:
|
||||||
|
pickRow("None", -1);
|
||||||
|
for (size_t k = 0; k < p.cultureList().size(); ++k) pickRow(p.cultureList()[k].name, (int)k);
|
||||||
|
break;
|
||||||
|
case EditField::AddFlora: case EditField::AddFauna: case EditField::AddFunga: {
|
||||||
|
BiotaKind kind = ed.pickerField == EditField::AddFlora ? BiotaKind::Flora
|
||||||
|
: ed.pickerField == EditField::AddFauna ? BiotaKind::Fauna : BiotaKind::Funga;
|
||||||
|
const auto& AR = biotaArchetypes();
|
||||||
|
for (int a = 0; a < (int)AR.size(); ++a) if (AR[a].kind == kind) pickRow(AR[a].name, a);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t lock = c.editLock;
|
||||||
|
if (ed.tab == 0) { // Geology
|
||||||
|
static const Plate kUnknown{};
|
||||||
|
const Plate& pl = (c.plateId >= 0 && c.plateId < (int)p.plates.size()) ? p.plates[c.plateId] : kUnknown;
|
||||||
|
y = editRow(ed, EditField::Elevation, "Elevation", TextFormat("%.0f m", c.elevation),
|
||||||
|
EditField::LockElevation, (lock & LockElevation) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::PlateId, "Plate", TextFormat("P%d (%s)", c.plateId, pl.type == PlateType::Oceanic ? "Oceanic" : "Continental"),
|
||||||
|
EditField::LockPlate, (lock & LockPlate) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::Crust, "Crust type", c.oceanic ? "Oceanic" : "Continental",
|
||||||
|
EditField::LockCrust, (lock & LockCrust) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::GeoAge, "Geo age", TextFormat("%.0f My", c.geoAge),
|
||||||
|
EditField::LockCrust, (lock & LockCrust) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::Biome, "Biome", biomeName(c.biome),
|
||||||
|
EditField::LockBiome, (lock & LockBiome) != 0, x, y, w);
|
||||||
|
} else if (ed.tab == 1) { // Climate
|
||||||
|
double temp = (cellIdx < (int)p.temperature().size()) ? p.temperature()[cellIdx] : 0.0;
|
||||||
|
double moist = (cellIdx < (int)p.moisture().size()) ? p.moisture()[cellIdx] : 0.0;
|
||||||
|
y = editRow(ed, EditField::Temperature, "Temperature", TextFormat("%.1f C", temp),
|
||||||
|
EditField::LockClimate, (lock & LockClimate) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::Moisture, "Moisture", TextFormat("%.0f%%", moist * 100.0),
|
||||||
|
EditField::LockClimate, (lock & LockClimate) != 0, x, y, w);
|
||||||
|
} else if (ed.tab == 2) { // Biota
|
||||||
|
double fl = (cellIdx < (int)p.floraDensity().size()) ? p.floraDensity()[cellIdx] : 0.0;
|
||||||
|
double fa = (cellIdx < (int)p.faunaDensity().size()) ? p.faunaDensity()[cellIdx] : 0.0;
|
||||||
|
double fu = (cellIdx < (int)p.fungaDensity().size()) ? p.fungaDensity()[cellIdx] : 0.0;
|
||||||
|
double hab = (cellIdx < (int)p.habitability().size()) ? p.habitability()[cellIdx] : 0.0;
|
||||||
|
y = editRow(ed, EditField::FloraDensity, "Flora density", TextFormat("%.0f%%", fl * 100.0),
|
||||||
|
EditField::LockBiota, (lock & LockBiota) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::FaunaDensity, "Fauna density", TextFormat("%.0f%%", fa * 100.0),
|
||||||
|
EditField::LockBiota, (lock & LockBiota) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::FungaDensity, "Funga density", TextFormat("%.0f%%", fu * 100.0),
|
||||||
|
EditField::LockBiota, (lock & LockBiota) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::Habitability, "Habitability", TextFormat("%.0f%%", hab * 100.0),
|
||||||
|
EditField::LockHabitability, (lock & LockHabitability) != 0, x, y, w);
|
||||||
|
y += 8;
|
||||||
|
auto listOrganisms = [&](const char* tag, const std::vector<Organism>& v, BiotaKind kind, EditField addId) {
|
||||||
|
if (y + 18 > maxY) return;
|
||||||
|
DrawText(tag, x, y, 13, Color{170, 175, 190, 255});
|
||||||
|
Rectangle ar{ (float)(x + w - 48), (float)y - 2, 48.0f, 18.0f };
|
||||||
|
DrawRectangleRec(ar, Color{34, 50, 40, 255}); DrawRectangleLinesEx(ar, 1, Color{100, 104, 120, 255});
|
||||||
|
DrawText("+ add", (int)ar.x + 3, (int)ar.y + 3, 11, Color{190, 220, 195, 255});
|
||||||
|
ed.fieldRects.push_back(ar); ed.fieldIds.push_back(addId);
|
||||||
|
y += 18;
|
||||||
|
for (size_t oi = 0; oi < v.size() && y + 16 <= maxY; ++oi) {
|
||||||
|
std::string nm = organismName(v[oi]);
|
||||||
|
DrawText(nm.c_str(), x + 10, y, 12, Color{200, 205, 215, 255});
|
||||||
|
Rectangle xr{ (float)(x + w - 20), (float)y - 1, 18.0f, 16.0f };
|
||||||
|
DrawRectangleRec(xr, Color{60, 30, 30, 255}); DrawRectangleLinesEx(xr, 1, Color{100, 60, 60, 255});
|
||||||
|
DrawText("x", (int)xr.x + 6, (int)xr.y + 1, 12, Color{230, 180, 180, 255});
|
||||||
|
ed.removeRects.push_back(xr); ed.removeKind.push_back((int)kind); ed.removeIndex.push_back((int)oi);
|
||||||
|
y += 16;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (p.biotaPopulated() && cellIdx < (int)p.biota().size()) {
|
||||||
|
const CellBiota& cb = p.biota()[cellIdx];
|
||||||
|
listOrganisms("Flora population", cb.flora, BiotaKind::Flora, EditField::AddFlora);
|
||||||
|
listOrganisms("Fauna population", cb.fauna, BiotaKind::Fauna, EditField::AddFauna);
|
||||||
|
listOrganisms("Funga population", cb.funga, BiotaKind::Funga, EditField::AddFunga);
|
||||||
|
} else {
|
||||||
|
DrawText("(press L to generate a biota population first)", x, y, 13, Color{150, 150, 165, 255});
|
||||||
|
}
|
||||||
|
} else if (ed.tab == 3 && hasSettle) { // Settlement
|
||||||
|
int si = p.cellSettlement()[cellIdx];
|
||||||
|
const Settlement& s = p.settlements[si];
|
||||||
|
int ov = (si < (int)p.settleAllegiance().size()) ? p.settleAllegiance()[si] : -1;
|
||||||
|
std::string ovName = (ov >= 0 && ov < (int)p.settlements.size()) ? p.settlements[ov].name : "Independent";
|
||||||
|
int ci = (si < (int)p.settleCulture().size()) ? p.settleCulture()[si] : -1;
|
||||||
|
std::string ciName = (ci >= 0 && ci < (int)p.cultureList().size()) ? p.cultureList()[ci].name : "None";
|
||||||
|
y = editRow(ed, EditField::Population, "Population", TextFormat("%.0f", s.population),
|
||||||
|
EditField::LockPopulation, (lock & LockPopulation) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::Allegiance, "Allegiance", ovName,
|
||||||
|
EditField::LockAllegiance, (lock & LockAllegiance) != 0, x, y, w);
|
||||||
|
y = editRow(ed, EditField::Culture, "Culture", ciName,
|
||||||
|
EditField::LockCulture, (lock & LockCulture) != 0, x, y, w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
||||||
bool live, double liveHours) {
|
bool live, double liveHours) {
|
||||||
DrawRectangleRec(r, Color{12, 14, 22, 235});
|
DrawRectangleRec(r, Color{12, 14, 22, 235});
|
||||||
|
|||||||
@ -2,9 +2,46 @@
|
|||||||
#include "raylib.h"
|
#include "raylib.h"
|
||||||
#include "Planet.hpp"
|
#include "Planet.hpp"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// Right-column UI panels: the tile detail panel (subtiles grid), the hover/
|
// Right-column UI panels: the tile detail panel (subtiles grid), the hover/
|
||||||
// selection info box, and the world-statistics panel.
|
// selection info box, the world-statistics panel, and the edit-mode panel.
|
||||||
|
|
||||||
|
// One editable row/group/button in the edit-mode panel (F3). Groups sharing a lock
|
||||||
|
// button mirror the EditLock bit groupings in PlanetTypes.hpp (e.g. Crust+GeoAge
|
||||||
|
// share LockCrust). Add* open a biota-archetype picker for that kind.
|
||||||
|
enum class EditField {
|
||||||
|
None = 0,
|
||||||
|
Elevation, PlateId, Crust, GeoAge, Biome,
|
||||||
|
Temperature, Moisture,
|
||||||
|
FloraDensity, FaunaDensity, FungaDensity, Habitability,
|
||||||
|
Population, Allegiance, Culture,
|
||||||
|
LockElevation, LockPlate, LockCrust, LockBiome, LockClimate, LockBiota,
|
||||||
|
LockHabitability, LockPopulation, LockAllegiance, LockCulture,
|
||||||
|
AddFlora, AddFauna, AddFunga,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transient edit-panel UI state, owned by the Viewer, rebuilt each frame by
|
||||||
|
// drawEditPanel() (render side) and consumed by the click/keyboard handlers
|
||||||
|
// (input side) -- mirrors the liveInfoTabRects/eventRowRects pattern.
|
||||||
|
struct EditPanelState {
|
||||||
|
int tab = 0; // 0 Geology, 1 Climate, 2 Biota, 3 Settlement
|
||||||
|
EditField editingField = EditField::None; // focused for typed numeric entry
|
||||||
|
std::string editBuffer;
|
||||||
|
EditField pickerField = EditField::None; // an open picker list is for this field (None = closed)
|
||||||
|
|
||||||
|
std::vector<Rectangle> tabRects;
|
||||||
|
std::vector<Rectangle> fieldRects; // value-click / toggle rects
|
||||||
|
std::vector<EditField> fieldIds; // parallel to fieldRects
|
||||||
|
std::vector<Rectangle> lockRects; // lock-toggle button rects
|
||||||
|
std::vector<EditField> lockIds; // parallel to lockRects (the Lock* field)
|
||||||
|
std::vector<Rectangle> pickerRects; // rows of an open picker list
|
||||||
|
std::vector<int> pickerValues; // parallel: plate id / biome / settlement / culture / archetype
|
||||||
|
std::vector<Rectangle> removeRects; // biota organism "remove" buttons
|
||||||
|
std::vector<int> removeKind; // parallel: BiotaKind as int
|
||||||
|
std::vector<int> removeIndex; // parallel: index within that kind's list
|
||||||
|
};
|
||||||
|
|
||||||
// Tile detail panel: tile info header + the selected cell's subgrid drawn as a
|
// Tile detail panel: tile info header + the selected cell's subgrid drawn as a
|
||||||
// flat, hoverable grid of subtiles (neighbor-owned subtiles dimmed). hoveredSub
|
// flat, hoverable grid of subtiles (neighbor-owned subtiles dimmed). hoveredSub
|
||||||
@ -19,3 +56,8 @@ void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected);
|
|||||||
// World statistics panel (shown in the subareas quadrant when no tile selected).
|
// World statistics panel (shown in the subareas quadrant when no tile selected).
|
||||||
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
||||||
bool live = false, double liveHours = 0.0);
|
bool live = false, double liveHours = 0.0);
|
||||||
|
|
||||||
|
// Edit mode (key F3): every property of the selected cell, editable. Rebuilds `ed`'s hit-rects
|
||||||
|
// each call (render populates, the input handler consumes -- see EditPanelState above); reads
|
||||||
|
// live values from `p` but never mutates it (all mutation happens in the click/keyboard handlers).
|
||||||
|
void drawEditPanel(const Planet& p, int cellIdx, Rectangle panel, EditPanelState& ed);
|
||||||
|
|||||||
@ -76,6 +76,7 @@ bool Viewer::init(int argc, char** argv) {
|
|||||||
SetConfigFlags(FLAG_MSAA_4X_HINT);
|
SetConfigFlags(FLAG_MSAA_4X_HINT);
|
||||||
InitWindow(screenW, screenH, "Planet Sim - Phase 1: Tectonics");
|
InitWindow(screenW, screenH, "Planet Sim - Phase 1: Tectonics");
|
||||||
SetTargetFPS(60);
|
SetTargetFPS(60);
|
||||||
|
SetExitKey(KEY_NULL); // raylib's default exit-on-ESC would fight edit mode's Escape-to-cancel
|
||||||
|
|
||||||
// Layout: left column 70% wide (3D globe 60% h on top, 2D map 40% h below);
|
// Layout: left column 70% wide (3D globe 60% h on top, 2D map 40% h below);
|
||||||
// right column 30% wide (cell info 50% h on top, subareas 50% below).
|
// right column 30% wide (cell info 50% h on top, subareas 50% below).
|
||||||
@ -152,7 +153,12 @@ void Viewer::selectCell(int idx) {
|
|||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
if (idx == selectedCell) { selectedCell = -1; subgrids.clear(); return; }
|
if (idx == selectedCell) { selectedCell = -1; subgrids.clear(); return; }
|
||||||
selectedCell = idx; rebuildSub();
|
selectedCell = idx; rebuildSub();
|
||||||
|
if (editMode) { // switching tiles mid-edit cancels any open picker / typed entry (keeps the tab)
|
||||||
|
editPanel.editingField = EditField::None; editPanel.editBuffer.clear(); editPanel.pickerField = EditField::None;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewer::exitEditMode() { editPanel = EditPanelState{}; }
|
||||||
|
|
||||||
// Recolor the mesh + refresh the elevation range, read straight from cells.
|
// Recolor the mesh + refresh the elevation range, read straight from cells.
|
||||||
void Viewer::recolor() {
|
void Viewer::recolor() {
|
||||||
@ -329,6 +335,7 @@ void Viewer::regenWorld() { // after generate(): geometry change
|
|||||||
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
||||||
buildMap2D(planet, mapRect, map2D);
|
buildMap2D(planet, mapRect, map2D);
|
||||||
selectedCell = -1; subgrids.clear();
|
selectedCell = -1; subgrids.clear();
|
||||||
|
editPanel.editingField = EditField::None; editPanel.editBuffer.clear(); editPanel.pickerField = EditField::None;
|
||||||
mapZoom = 1.0; mapPanX = 0.0; mapPanY = 0.0; // drop any 2D-map zoom/pan from the old world
|
mapZoom = 1.0; mapPanX = 0.0; mapPanY = 0.0; // drop any 2D-map zoom/pan from the old world
|
||||||
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(); events.clear(); nextEventId = 1; // reseed/regen drops back to World Creation
|
liveWorld = false; followId = 0; wxUndo.clear(); events.clear(); nextEventId = 1; // reseed/regen drops back to World Creation
|
||||||
@ -618,7 +625,7 @@ void Viewer::loadGame(const char* path) {
|
|||||||
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
||||||
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
||||||
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
||||||
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15, ver >= 17, ver >= 18, ver >= 19, ver >= 20, ver >= 21, ver >= 22, ver >= 23)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes, v17 geography, v18 geography salt, v19 ecoregions, v20 settlements, v21 conflict, v22 diplomacy, v23 cultures
|
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15, ver >= 17, ver >= 18, ver >= 19, ver >= 20, ver >= 21, ver >= 22, ver >= 23, ver >= 24)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes, v17 geography, v18 geography salt, v19 ecoregions, v20 settlements, v21 conflict, v22 diplomacy, v23 cultures, v24 edit mode
|
||||||
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
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "Colors.hpp" // ColorMode
|
#include "Colors.hpp" // ColorMode
|
||||||
#include "Overlays.hpp" // PlateLabel
|
#include "Overlays.hpp" // PlateLabel
|
||||||
#include "Map2D.hpp" // Map2D
|
#include "Map2D.hpp" // Map2D
|
||||||
|
#include "Panels.hpp" // EditField, EditPanelState
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@ -15,7 +16,7 @@
|
|||||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||||
struct Viewer {
|
struct Viewer {
|
||||||
// ---- Files / save format ------------------------------------------------
|
// ---- Files / save format ------------------------------------------------
|
||||||
static constexpr uint32_t SAVE_VERSION = 23; // v23: civ cultural evolution; v22: civ diplomacy; v21: civ conflict/wars; v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
static constexpr uint32_t SAVE_VERSION = 24; // v24: edit mode (per-cell lock bits + locked-value maps); v23: civ cultural evolution; v22: civ diplomacy; v21: civ conflict/wars; v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
||||||
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
||||||
static constexpr int EVENT_LOG_MAX = 200;
|
static constexpr int EVENT_LOG_MAX = 200;
|
||||||
const char* CONFIG_PATH = "planet.cfg";
|
const char* CONFIG_PATH = "planet.cfg";
|
||||||
@ -137,6 +138,11 @@ struct Viewer {
|
|||||||
double selectedThresh = 0.06;
|
double selectedThresh = 0.06;
|
||||||
std::vector<std::shared_ptr<SubGrid>> subgrids;
|
std::vector<std::shared_ptr<SubGrid>> subgrids;
|
||||||
|
|
||||||
|
// Edit mode (key F3, on a settled world; save v24): manually edit every property of the
|
||||||
|
// selected cell, with locking against automatic per-tick recomputation. Reuses selectedCell.
|
||||||
|
bool editMode = false;
|
||||||
|
EditPanelState editPanel;
|
||||||
|
|
||||||
// Transient on-screen status line.
|
// Transient on-screen status line.
|
||||||
std::string statusMsg; double statusUntil = 0.0;
|
std::string statusMsg; double statusUntil = 0.0;
|
||||||
|
|
||||||
@ -194,6 +200,10 @@ struct Viewer {
|
|||||||
|
|
||||||
// ---- Input (ViewerInput.cpp) --------------------------------------------
|
// ---- Input (ViewerInput.cpp) --------------------------------------------
|
||||||
void handleInput();
|
void handleInput();
|
||||||
|
void handleEditClick(); // dispatch a click inside the edit panel (tabs/fields/locks/picker/remove)
|
||||||
|
bool nudgeEditField(float wheel);// mouse-wheel adjusts the hovered numeric field; false if none hovered
|
||||||
|
void commitEditField(); // parse editPanel.editBuffer and apply it to editPanel.editingField
|
||||||
|
void exitEditMode(); // clear edit-mode UI state (on F3 off / reseed / deselect)
|
||||||
|
|
||||||
// ---- Render (ViewerRender.cpp) ------------------------------------------
|
// ---- Render (ViewerRender.cpp) ------------------------------------------
|
||||||
void renderFrame();
|
void renderFrame();
|
||||||
|
|||||||
@ -52,6 +52,7 @@ void Viewer::handleInput() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (editMode && inPanel) handleEditClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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)
|
||||||
@ -94,6 +95,8 @@ void Viewer::handleInput() {
|
|||||||
mapPanY = std::clamp(mp.y - v * h - mapRect.y - (mapRect.height - h) * 0.5, -(h - mapRect.height) * 0.5, (h - mapRect.height) * 0.5);
|
mapPanY = std::clamp(mp.y - v * h - mapRect.y - (mapRect.height - h) * 0.5, -(h - mapRect.height) * 0.5, (h - mapRect.height) * 0.5);
|
||||||
mapZoom = nz;
|
mapZoom = nz;
|
||||||
}
|
}
|
||||||
|
} else if (editMode && inPanel && wheel != 0.0f && nudgeEditField(wheel)) {
|
||||||
|
// consumed by the edit panel (a numeric field under the cursor was nudged)
|
||||||
} else {
|
} else {
|
||||||
camDist -= wheel * 0.4f;
|
camDist -= wheel * 0.4f;
|
||||||
camDist = std::clamp(camDist, 2.6f, 14.0f);
|
camDist = std::clamp(camDist, 2.6f, 14.0f);
|
||||||
@ -115,7 +118,7 @@ void Viewer::handleInput() {
|
|||||||
hovered = -1; hasHoverSub = false; hoveredSubIdx = -1;
|
hovered = -1; hasHoverSub = false; hoveredSubIdx = -1;
|
||||||
bool have3DHit = false; Vec3 hitUnit, hitModel;
|
bool have3DHit = false; Vec3 hitUnit, hitModel;
|
||||||
if (inPanel) {
|
if (inPanel) {
|
||||||
if (!subgrids.empty() && CheckCollisionPointRec(mp, gridRect)) {
|
if (!editMode && !subgrids.empty() && CheckCollisionPointRec(mp, gridRect)) {
|
||||||
const auto& sg = subgrids[0]; int R = sg->res;
|
const auto& sg = subgrids[0]; int R = sg->res;
|
||||||
int i = std::clamp((int)((mp.x - gridRect.x) / (gridRect.width / R)), 0, R - 1);
|
int i = std::clamp((int)((mp.x - gridRect.x) / (gridRect.width / R)), 0, R - 1);
|
||||||
int j = std::clamp((int)((mp.y - gridRect.y) / (gridRect.height / R)), 0, R - 1);
|
int j = std::clamp((int)((mp.y - gridRect.y) / (gridRect.height / R)), 0, R - 1);
|
||||||
@ -154,6 +157,18 @@ void Viewer::handleInput() {
|
|||||||
selectCell(hovered);
|
selectCell(hovered);
|
||||||
|
|
||||||
// --- Keys -------------------------------------------------------------
|
// --- Keys -------------------------------------------------------------
|
||||||
|
// Edit-mode typed numeric entry swallows all other keys while a field is focused.
|
||||||
|
if (editMode && editPanel.editingField != EditField::None) {
|
||||||
|
int ch = GetCharPressed();
|
||||||
|
while (ch > 0) {
|
||||||
|
if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-') editPanel.editBuffer += (char)ch;
|
||||||
|
ch = GetCharPressed();
|
||||||
|
}
|
||||||
|
if (IsKeyPressed(KEY_BACKSPACE) && !editPanel.editBuffer.empty()) editPanel.editBuffer.pop_back();
|
||||||
|
if (IsKeyPressed(KEY_ENTER)) commitEditField();
|
||||||
|
if (IsKeyPressed(KEY_ESCAPE)) { editPanel.editingField = EditField::None; editPanel.editBuffer.clear(); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (IsKeyPressed(KEY_SPACE) && !phase3Prompt) pauseAction();
|
if (IsKeyPressed(KEY_SPACE) && !phase3Prompt) pauseAction();
|
||||||
if (IsKeyPressed(KEY_ONE)) { mode = ColorMode::Elevation; recolor(); }
|
if (IsKeyPressed(KEY_ONE)) { mode = ColorMode::Elevation; recolor(); }
|
||||||
if (IsKeyPressed(KEY_TWO)) { mode = ColorMode::Plate; recolor(); }
|
if (IsKeyPressed(KEY_TWO)) { mode = ColorMode::Plate; recolor(); }
|
||||||
@ -292,7 +307,15 @@ void Viewer::handleInput() {
|
|||||||
setStatus(showNames ? "Place names on" : "Place names off");
|
setStatus(showNames ? "Place names on" : "Place names off");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
|
if (IsKeyPressed(KEY_F3) && settled) { // toggle edit mode (manual cell/settlement edits)
|
||||||
|
editMode = !editMode;
|
||||||
|
if (editMode) { paused = true; setStatus("Edit mode on - click a tile to edit (F3 to exit)"); }
|
||||||
|
else { exitEditMode(); setStatus("Edit mode off"); }
|
||||||
|
}
|
||||||
|
if (IsKeyPressed(KEY_C)) {
|
||||||
|
selectedCell = -1; subgrids.clear();
|
||||||
|
editPanel.editingField = EditField::None; editPanel.editBuffer.clear(); editPanel.pickerField = EditField::None;
|
||||||
|
}
|
||||||
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
||||||
if (IsKeyPressed(KEY_S)) { // one step
|
if (IsKeyPressed(KEY_S)) { // one step
|
||||||
if (liveWorld) liveStepForward(); // Live World: step the clock forward
|
if (liveWorld) liveStepForward(); // Live World: step the clock forward
|
||||||
@ -332,3 +355,170 @@ void Viewer::handleInput() {
|
|||||||
else driftRate = std::max(driftRate / 1.5, 0.5);
|
else driftRate = std::max(driftRate / 1.5, 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dispatch a click that landed inside the edit panel (editMode && inPanel). drawEditPanel()
|
||||||
|
// rebuilt editPanel's hit-rects this same frame; this just resolves which one was hit and calls
|
||||||
|
// the matching Planet::setXxx()/setXxxLock() (PlanetEdit.cpp). Order mirrors visual stacking:
|
||||||
|
// tabs first, then an open picker (which otherwise swallows the whole panel), then locks/fields/
|
||||||
|
// remove buttons.
|
||||||
|
void Viewer::handleEditClick() {
|
||||||
|
if (selectedCell < 0) return;
|
||||||
|
const int cellIdx = selectedCell;
|
||||||
|
const int si = (cellIdx < (int)planet.cellSettlement().size()) ? planet.cellSettlement()[cellIdx] : -1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < editPanel.tabRects.size(); ++i)
|
||||||
|
if (CheckCollisionPointRec(mp, editPanel.tabRects[i])) {
|
||||||
|
editPanel.tab = (int)i;
|
||||||
|
editPanel.pickerField = EditField::None;
|
||||||
|
editPanel.editingField = EditField::None; editPanel.editBuffer.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editPanel.pickerField != EditField::None) {
|
||||||
|
for (size_t i = 0; i < editPanel.pickerRects.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, editPanel.pickerRects[i])) continue;
|
||||||
|
int v = editPanel.pickerValues[i];
|
||||||
|
switch (editPanel.pickerField) {
|
||||||
|
case EditField::PlateId: planet.setPlateId(cellIdx, v); break;
|
||||||
|
case EditField::Biome: planet.setBiome(cellIdx, (Biome)v); break;
|
||||||
|
case EditField::Allegiance: if (si >= 0) planet.setSettlementAllegiance(si, v); break;
|
||||||
|
case EditField::Culture: if (si >= 0) planet.setSettlementCulture(si, v); break;
|
||||||
|
case EditField::AddFlora: planet.addOrganism(cellIdx, BiotaKind::Flora, v); break;
|
||||||
|
case EditField::AddFauna: planet.addOrganism(cellIdx, BiotaKind::Fauna, v); break;
|
||||||
|
case EditField::AddFunga: planet.addOrganism(cellIdx, BiotaKind::Funga, v); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
editPanel.pickerField = EditField::None;
|
||||||
|
refreshView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return; // inside the panel but not on a picker row: swallow the click, stay open
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < editPanel.lockRects.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, editPanel.lockRects[i])) continue;
|
||||||
|
EditField f = editPanel.lockIds[i];
|
||||||
|
uint16_t bit = 0;
|
||||||
|
switch (f) {
|
||||||
|
case EditField::LockElevation: bit = LockElevation; break;
|
||||||
|
case EditField::LockPlate: bit = LockPlate; break;
|
||||||
|
case EditField::LockCrust: bit = LockCrust; break;
|
||||||
|
case EditField::LockBiome: bit = LockBiome; break;
|
||||||
|
case EditField::LockClimate: bit = LockClimate; break;
|
||||||
|
case EditField::LockBiota: bit = LockBiota; break;
|
||||||
|
case EditField::LockHabitability: bit = LockHabitability; break;
|
||||||
|
case EditField::LockPopulation: bit = LockPopulation; break;
|
||||||
|
case EditField::LockAllegiance: bit = LockAllegiance; break;
|
||||||
|
case EditField::LockCulture: bit = LockCulture; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
bool want = (planet.editLockAt(cellIdx) & bit) == 0; // toggle
|
||||||
|
switch (f) {
|
||||||
|
case EditField::LockElevation: planet.setElevationLock(cellIdx, want); break;
|
||||||
|
case EditField::LockPlate: planet.setPlateLock(cellIdx, want); break;
|
||||||
|
case EditField::LockCrust: planet.setCrustLock(cellIdx, want); break;
|
||||||
|
case EditField::LockBiome: planet.setBiomeLock(cellIdx, want); break;
|
||||||
|
case EditField::LockClimate: planet.setClimateLock(cellIdx, want); break;
|
||||||
|
case EditField::LockBiota: planet.setBiotaDensityLock(cellIdx, want); break;
|
||||||
|
case EditField::LockHabitability: planet.setHabitabilityLock(cellIdx, want); break;
|
||||||
|
case EditField::LockPopulation: if (si >= 0) planet.setPopulationLock(si, want); break;
|
||||||
|
case EditField::LockAllegiance: if (si >= 0) planet.setAllegianceLock(si, want); break;
|
||||||
|
case EditField::LockCulture: if (si >= 0) planet.setCultureLock(si, want); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < editPanel.removeRects.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, editPanel.removeRects[i])) continue;
|
||||||
|
planet.removeOrganism(cellIdx, (BiotaKind)editPanel.removeKind[i], editPanel.removeIndex[i]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < editPanel.fieldRects.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, editPanel.fieldRects[i])) continue;
|
||||||
|
EditField f = editPanel.fieldIds[i];
|
||||||
|
if (f == EditField::Crust) {
|
||||||
|
planet.setCrust(cellIdx, !planet.cells[cellIdx].oceanic);
|
||||||
|
refreshView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (f == EditField::PlateId || f == EditField::Biome || f == EditField::Allegiance || f == EditField::Culture
|
||||||
|
|| f == EditField::AddFlora || f == EditField::AddFauna || f == EditField::AddFunga) {
|
||||||
|
editPanel.pickerField = f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Numeric: focus it for typed entry, seeded with the current value.
|
||||||
|
editPanel.editingField = f;
|
||||||
|
const Cell& c = planet.cells[cellIdx];
|
||||||
|
switch (f) {
|
||||||
|
case EditField::Elevation: editPanel.editBuffer = TextFormat("%.0f", c.elevation); break;
|
||||||
|
case EditField::GeoAge: editPanel.editBuffer = TextFormat("%.0f", c.geoAge); break;
|
||||||
|
case EditField::Temperature: editPanel.editBuffer = TextFormat("%.1f", cellIdx < (int)planet.temperature().size() ? planet.temperature()[cellIdx] : 0.0); break;
|
||||||
|
case EditField::Moisture: editPanel.editBuffer = TextFormat("%.0f", cellIdx < (int)planet.moisture().size() ? planet.moisture()[cellIdx] * 100.0 : 0.0); break;
|
||||||
|
case EditField::FloraDensity: editPanel.editBuffer = TextFormat("%.0f", cellIdx < (int)planet.floraDensity().size() ? planet.floraDensity()[cellIdx] * 100.0 : 0.0); break;
|
||||||
|
case EditField::FaunaDensity: editPanel.editBuffer = TextFormat("%.0f", cellIdx < (int)planet.faunaDensity().size() ? planet.faunaDensity()[cellIdx] * 100.0 : 0.0); break;
|
||||||
|
case EditField::FungaDensity: editPanel.editBuffer = TextFormat("%.0f", cellIdx < (int)planet.fungaDensity().size() ? planet.fungaDensity()[cellIdx] * 100.0 : 0.0); break;
|
||||||
|
case EditField::Habitability: editPanel.editBuffer = TextFormat("%.0f", cellIdx < (int)planet.habitability().size() ? planet.habitability()[cellIdx] * 100.0 : 0.0); break;
|
||||||
|
case EditField::Population: if (si >= 0) editPanel.editBuffer = TextFormat("%.0f", planet.settlements[si].population); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse wheel over a hovered numeric field row nudges it by a fixed step, without needing focus.
|
||||||
|
// Returns false (and leaves the wheel free for camera zoom) if nothing nudge-able is under the cursor.
|
||||||
|
bool Viewer::nudgeEditField(float wheel) {
|
||||||
|
if (selectedCell < 0 || editPanel.pickerField != EditField::None) return false;
|
||||||
|
const int cellIdx = selectedCell;
|
||||||
|
for (size_t i = 0; i < editPanel.fieldRects.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, editPanel.fieldRects[i])) continue;
|
||||||
|
EditField f = editPanel.fieldIds[i];
|
||||||
|
const int si = (cellIdx < (int)planet.cellSettlement().size()) ? planet.cellSettlement()[cellIdx] : -1;
|
||||||
|
double dir = (wheel > 0.0f) ? 1.0 : -1.0;
|
||||||
|
switch (f) {
|
||||||
|
case EditField::Elevation: planet.setElevation(cellIdx, planet.cells[cellIdx].elevation + dir * 100.0); break;
|
||||||
|
case EditField::GeoAge: planet.setGeoAge(cellIdx, planet.cells[cellIdx].geoAge + dir * 10.0); break;
|
||||||
|
case EditField::Temperature: planet.setTemperature(cellIdx, (cellIdx < (int)planet.temperature().size() ? planet.temperature()[cellIdx] : 0.0) + dir * 1.0); break;
|
||||||
|
case EditField::Moisture: planet.setMoisture(cellIdx, (cellIdx < (int)planet.moisture().size() ? planet.moisture()[cellIdx] : 0.0) + dir * 0.05); break;
|
||||||
|
case EditField::FloraDensity: planet.setFloraDensity(cellIdx, (cellIdx < (int)planet.floraDensity().size() ? planet.floraDensity()[cellIdx] : 0.0) + dir * 0.05); break;
|
||||||
|
case EditField::FaunaDensity: planet.setFaunaDensity(cellIdx, (cellIdx < (int)planet.faunaDensity().size() ? planet.faunaDensity()[cellIdx] : 0.0) + dir * 0.05); break;
|
||||||
|
case EditField::FungaDensity: planet.setFungaDensity(cellIdx, (cellIdx < (int)planet.fungaDensity().size() ? planet.fungaDensity()[cellIdx] : 0.0) + dir * 0.05); break;
|
||||||
|
case EditField::Habitability: planet.setHabitability(cellIdx, (cellIdx < (int)planet.habitability().size() ? planet.habitability()[cellIdx] : 0.0) + dir * 0.05); break;
|
||||||
|
case EditField::Population: if (si >= 0) planet.setSettlementPopulation(si, planet.settlements[si].population * (1.0 + dir * 0.1)); break;
|
||||||
|
default: return false; // not a nudge-able field (Crust/PlateId/Biome/Allegiance/Culture/Add*)
|
||||||
|
}
|
||||||
|
refreshView();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse editPanel.editBuffer and apply it to the currently focused field (Enter, or a field
|
||||||
|
// switch). Silently discards on a bad/empty number, matching a text-field's usual "just stop
|
||||||
|
// editing" behaviour rather than raising an error for a hand-rolled widget.
|
||||||
|
void Viewer::commitEditField() {
|
||||||
|
if (selectedCell < 0 || editPanel.editingField == EditField::None) {
|
||||||
|
editPanel.editingField = EditField::None; editPanel.editBuffer.clear(); return;
|
||||||
|
}
|
||||||
|
double v = 0.0;
|
||||||
|
try { v = std::stod(editPanel.editBuffer); }
|
||||||
|
catch (...) { editPanel.editingField = EditField::None; editPanel.editBuffer.clear(); return; }
|
||||||
|
const int cellIdx = selectedCell;
|
||||||
|
const int si = (cellIdx < (int)planet.cellSettlement().size()) ? planet.cellSettlement()[cellIdx] : -1;
|
||||||
|
switch (editPanel.editingField) {
|
||||||
|
case EditField::Elevation: planet.setElevation(cellIdx, v); break;
|
||||||
|
case EditField::GeoAge: planet.setGeoAge(cellIdx, v); break;
|
||||||
|
case EditField::Temperature: planet.setTemperature(cellIdx, v); break;
|
||||||
|
case EditField::Moisture: planet.setMoisture(cellIdx, v / 100.0); break;
|
||||||
|
case EditField::FloraDensity: planet.setFloraDensity(cellIdx, v / 100.0); break;
|
||||||
|
case EditField::FaunaDensity: planet.setFaunaDensity(cellIdx, v / 100.0); break;
|
||||||
|
case EditField::FungaDensity: planet.setFungaDensity(cellIdx, v / 100.0); break;
|
||||||
|
case EditField::Habitability: planet.setHabitability(cellIdx, v / 100.0); break;
|
||||||
|
case EditField::Population: if (si >= 0) planet.setSettlementPopulation(si, v); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
editPanel.editingField = EditField::None; editPanel.editBuffer.clear();
|
||||||
|
refreshView();
|
||||||
|
}
|
||||||
|
|||||||
@ -822,13 +822,24 @@ void Viewer::renderLiveInfo() {
|
|||||||
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
// Right column: hover/selection info (top) + detail panel or world stats (bottom).
|
||||||
void Viewer::renderPanels() {
|
void Viewer::renderPanels() {
|
||||||
drawHoverPanel(planet, hoverRect, hovered, selectedCell);
|
drawHoverPanel(planet, hoverRect, hovered, selectedCell);
|
||||||
if (selectedCell >= 0 && !subgrids.empty())
|
if (editMode) {
|
||||||
|
if (selectedCell >= 0) {
|
||||||
|
drawEditPanel(planet, selectedCell, panelRect, editPanel);
|
||||||
|
} else {
|
||||||
|
DrawRectangleRec(panelRect, Color{12, 14, 22, 235});
|
||||||
|
DrawRectangleLinesEx(panelRect, 2, Color{210, 150, 70, 255});
|
||||||
|
DrawText("EDIT MODE", (int)panelRect.x + 10, (int)panelRect.y + 8, 18, Color{255, 200, 120, 255});
|
||||||
|
DrawText("click a tile to edit its properties", (int)panelRect.x + 10, (int)panelRect.y + 40, 16, Color{190, 195, 210, 255});
|
||||||
|
DrawText("F3: exit", (int)panelRect.x + 10, (int)panelRect.y + 64, 14, Color{170, 170, 185, 255});
|
||||||
|
}
|
||||||
|
} else if (selectedCell >= 0 && !subgrids.empty()) {
|
||||||
drawDetailPanel(planet, subgrids[0], selectedCell,
|
drawDetailPanel(planet, subgrids[0], selectedCell,
|
||||||
planet.cells[selectedCell].elevation, planet.cells[selectedCell].geoAge,
|
planet.cells[selectedCell].elevation, planet.cells[selectedCell].geoAge,
|
||||||
panelRect, gridRect, hoveredSubIdx);
|
panelRect, gridRect, hoveredSubIdx);
|
||||||
else
|
} else {
|
||||||
drawStats(planet, panelRect, elapsedMy, settled, liveWorld, liveTime);
|
drawStats(planet, panelRect, elapsedMy, settled, liveWorld, liveTime);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Top-left HUD text + the clickable pause button.
|
// Top-left HUD text + the clickable pause button.
|
||||||
void Viewer::renderHUD() {
|
void Viewer::renderHUD() {
|
||||||
@ -838,6 +849,11 @@ void Viewer::renderHUD() {
|
|||||||
int vw = MeasureText(vm, 22);
|
int vw = MeasureText(vm, 22);
|
||||||
DrawText(vm, view3DW / 2 - vw / 2, 10, 22, Color{235, 225, 140, 255});
|
DrawText(vm, view3DW / 2 - vw / 2, 10, 22, Color{235, 225, 140, 255});
|
||||||
}
|
}
|
||||||
|
if (editMode) {
|
||||||
|
const char* eb = "EDIT MODE";
|
||||||
|
int ew = MeasureText(eb, 18);
|
||||||
|
DrawText(eb, view3DW / 2 - ew / 2, 36, 18, Color{255, 170, 90, 255});
|
||||||
|
}
|
||||||
|
|
||||||
int y = 10;
|
int y = 10;
|
||||||
auto line = [&](const std::string& s){ DrawText(s.c_str(), 12, y, 18, RAYWHITE); y += 22; };
|
auto line = [&](const std::string& s){ DrawText(s.c_str(), 12, y, 18, RAYWHITE); y += 22; };
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <iosfwd>
|
#include <iosfwd>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
class Planet {
|
class Planet {
|
||||||
public:
|
public:
|
||||||
@ -253,6 +254,46 @@ public:
|
|||||||
const std::vector<double>& settlementDrought() const { return sCivDrought; }
|
const std::vector<double>& settlementDrought() const { return sCivDrought; }
|
||||||
const std::vector<double>& settlementPlague() const { return sCivPlague; } // active epidemic loss (/yr, 0 = none)
|
const std::vector<double>& settlementPlague() const { return sCivPlague; } // active epidemic loss (/yr, 0 = none)
|
||||||
|
|
||||||
|
// --- Edit mode (manual world editing, PlanetEdit.cpp; save v24) --------------
|
||||||
|
// Every setXxx() writes directly into the field the simulation already reads each
|
||||||
|
// tick, so an edit "takes" immediately (Eulerian: properties flow over the fixed
|
||||||
|
// grid). Without locking it's a one-off nudge -- the ongoing sim keeps evolving it,
|
||||||
|
// like a meteor impact. setXxxLock(i, true) additionally exempts that cell's field
|
||||||
|
// from automatic per-tick recomputation (see EditLock in PlanetTypes.hpp for which
|
||||||
|
// sim pass reads which bit) until unlocked. Climate/biota-density/habitability have
|
||||||
|
// no persistent backing of their own, so locking them also pins the frozen value.
|
||||||
|
void setElevation(int i, double meters);
|
||||||
|
void setElevationLock(int i, bool locked);
|
||||||
|
void setPlateId(int i, int plateId);
|
||||||
|
void setPlateLock(int i, bool locked);
|
||||||
|
void setCrust(int i, bool oceanic);
|
||||||
|
void setGeoAge(int i, double my);
|
||||||
|
void setCrustLock(int i, bool locked); // covers oceanic + geoAge together
|
||||||
|
void setBiome(int i, Biome b);
|
||||||
|
void setBiomeLock(int i, bool locked);
|
||||||
|
void setTemperature(int i, double celsius);
|
||||||
|
void setMoisture(int i, double normalized01);
|
||||||
|
void setClimateLock(int i, bool locked); // covers temperature + moisture together
|
||||||
|
void setFloraDensity(int i, double d01);
|
||||||
|
void setFaunaDensity(int i, double d01);
|
||||||
|
void setFungaDensity(int i, double d01);
|
||||||
|
void setBiotaDensityLock(int i, bool locked); // covers flora/fauna/funga density together
|
||||||
|
void setHabitability(int i, double h01);
|
||||||
|
void setHabitabilityLock(int i, bool locked);
|
||||||
|
// Biota population: directly add/remove an Organism from a cell's discrete list (already
|
||||||
|
// stateful/saved -- no lock concept needed, mirrors what generateBiota() itself writes).
|
||||||
|
void addOrganism(int i, BiotaKind kind, int archetype);
|
||||||
|
void removeOrganism(int i, BiotaKind kind, int index);
|
||||||
|
// Settlement-linked edits (keyed by settlement index; the lock lives on the settlement's
|
||||||
|
// home cell, gating stepCivilization()/stepConflict()/stepCulture() for it).
|
||||||
|
void setSettlementPopulation(int settleIdx, double population);
|
||||||
|
void setPopulationLock(int settleIdx, bool locked);
|
||||||
|
void setSettlementAllegiance(int settleIdx, int overlordSettleIdx); // -1 = independent
|
||||||
|
void setAllegianceLock(int settleIdx, bool locked);
|
||||||
|
void setSettlementCulture(int settleIdx, int cultureIdx); // -1 = none
|
||||||
|
void setCultureLock(int settleIdx, bool locked);
|
||||||
|
uint16_t editLockAt(int i) const; // current EditLock bitmask for a cell (0 if out of range)
|
||||||
|
|
||||||
// Build a fine-resolution subgrid patch for one macro cell (phase 4/5 hook).
|
// Build a fine-resolution subgrid patch for one macro cell (phase 4/5 hook).
|
||||||
std::shared_ptr<SubGrid> makeSubGrid(int cellIndex, int res) const;
|
std::shared_ptr<SubGrid> makeSubGrid(int cellIndex, int res) const;
|
||||||
|
|
||||||
@ -274,12 +315,15 @@ public:
|
|||||||
// hasSettlements: whether the stream carries the civilization settlements block (save v20+); older
|
// hasSettlements: whether the stream carries the civilization settlements block (save v20+); older
|
||||||
// saves load with none (re-seeded on demand via the civ key). hasCultures: whether the stream
|
// saves load with none (re-seeded on demand via the civ key). hasCultures: whether the stream
|
||||||
// carries the stateful culture block (save v23+); older saves re-seed one culture per continent
|
// carries the stateful culture block (save v23+); older saves re-seed one culture per continent
|
||||||
// on the next computeCultures().
|
// on the next computeCultures(). hasEditState: whether the stream carries the edit-mode block
|
||||||
|
// (save v24+) -- per-cell editLock bits + the sparse locked-value maps; older saves load with no
|
||||||
|
// manual edits/locks.
|
||||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
||||||
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
|
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
|
||||||
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true,
|
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true,
|
||||||
bool hasGeoSalt = true, bool hasEcoregions = true, bool hasSettlements = true,
|
bool hasGeoSalt = true, bool hasEcoregions = true, bool hasSettlements = true,
|
||||||
bool hasConflict = true, bool hasDiplo = true, bool hasCultures = true);
|
bool hasConflict = true, bool hasDiplo = true, bool hasCultures = true,
|
||||||
|
bool hasEditState = true);
|
||||||
|
|
||||||
// Helpers for rendering / info.
|
// Helpers for rendering / info.
|
||||||
double cellWidthMeters() const; // approx lateral cell spacing
|
double cellWidthMeters() const; // approx lateral cell spacing
|
||||||
@ -299,7 +343,7 @@ private:
|
|||||||
bool hasMoons, bool hasWeather, bool hasStorms,
|
bool hasMoons, bool hasWeather, bool hasStorms,
|
||||||
bool hasVolcanoes, bool hasStatefulVolcanoes, bool hasGeography,
|
bool hasVolcanoes, bool hasStatefulVolcanoes, bool hasGeography,
|
||||||
bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
||||||
bool hasConflict, bool hasDiplo, bool hasCultures);
|
bool hasConflict, bool hasDiplo, bool hasCultures, bool hasEditState);
|
||||||
void clearDerivedState(); // clear geometry-dependent scratch/derived fields
|
void clearDerivedState(); // clear geometry-dependent scratch/derived fields
|
||||||
void assignPlates();
|
void assignPlates();
|
||||||
void seedInitialRelief();
|
void seedInitialRelief();
|
||||||
@ -413,6 +457,14 @@ private:
|
|||||||
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
||||||
std::vector<CellBiota> sBiota;
|
std::vector<CellBiota> sBiota;
|
||||||
bool sHasBiota = false;
|
bool sHasBiota = false;
|
||||||
|
|
||||||
|
// Edit mode (save v24; PlanetEdit.cpp): sparse locked values for the derived fields that have
|
||||||
|
// no other persistent backing (fully recomputed from scratch each tick, so LOCKING pins the
|
||||||
|
// VALUE, not just a write). Keyed by cell index; an entry exists only while the matching
|
||||||
|
// EditLock bit is set on that cell. Naturally sparse -- most worlds lock few or no cells.
|
||||||
|
std::unordered_map<int, double> sLockTemp, sLockMoist;
|
||||||
|
std::unordered_map<int, double> sLockFlora, sLockFauna, sLockFunga;
|
||||||
|
std::unordered_map<int, double> sLockHab;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Human-editable config file (key = value text). All PlanetConfig input
|
// Human-editable config file (key = value text). All PlanetConfig input
|
||||||
|
|||||||
@ -57,6 +57,6 @@ void Planet::classifyBiomes() {
|
|||||||
else if (moist < GRASS_MOIST) b = (temp > SAVANNA_TEMP) ? Biome::Savanna : Biome::Grassland;
|
else if (moist < GRASS_MOIST) b = (temp > SAVANNA_TEMP) ? Biome::Savanna : Biome::Grassland;
|
||||||
else b = Biome::Forest;
|
else b = Biome::Forest;
|
||||||
}
|
}
|
||||||
cells[i].biome = b;
|
if (!(c.editLock & LockBiome)) cells[i].biome = b; // edit mode: biome pinned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,9 @@ void Planet::computeHabitability() {
|
|||||||
double hab = (wW * water + fW * food + tW * tComfort) * coldGate * elevF;
|
double hab = (wW * water + fW * food + tW * tComfort) * coldGate * elevF;
|
||||||
sHabitability[i] = std::clamp(hab, 0.0, 1.0);
|
sHabitability[i] = std::clamp(hab, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
// Edit mode: re-apply any locked habitability (see setHabitabilityLock/setHabitability in
|
||||||
|
// PlanetEdit.cpp) -- the array is fully recomputed above each call, so the pin lives in sLockHab.
|
||||||
|
for (const auto& kv : sLockHab) if (kv.first >= 0 && kv.first < n) sHabitability[kv.first] = kv.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-time placement: habitability-WEIGHTED RANDOM sampling with soft local suppression, so settlements
|
// One-time placement: habitability-WEIGHTED RANDOM sampling with soft local suppression, so settlements
|
||||||
@ -259,7 +262,8 @@ CivUpdate Planet::stepCivilization(double dtHours, double liveTime) {
|
|||||||
stormLoss += cfg.civStormDeathRate * ws.strength * overlap * (hur ? cfg.civHurricaneDeathMult : 1.0);
|
stormLoss += cfg.civStormDeathRate * ws.strength * overlap * (hur ? cfg.civHurricaneDeathMult : 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dtYears > 0.0) {
|
bool popLocked = (c >= 0 && c < n) && (cells[c].editLock & LockPopulation) != 0;
|
||||||
|
if (dtYears > 0.0 && !popLocked) { // edit mode: population growth pinned
|
||||||
double P = st.population;
|
double P = st.population;
|
||||||
double r = cfg.civGrowthRate * (cfg.civGrowthMin + (1.0 - cfg.civGrowthMin) * hab); // env-driven rate
|
double r = cfg.civGrowthRate * (cfg.civGrowthMin + (1.0 - cfg.civGrowthMin) * hab); // env-driven rate
|
||||||
P += r * P * (1.0 - P / std::max(1.0, K)) * dtYears; // logistic toward K
|
P += r * P * (1.0 - P / std::max(1.0, K)) * dtYears; // logistic toward K
|
||||||
|
|||||||
@ -94,6 +94,9 @@ void Planet::computeClimate() {
|
|||||||
}
|
}
|
||||||
for (int i = 0; i < n; ++i) sTemp[i] += dT[i];
|
for (int i = 0; i < n; ++i) sTemp[i] += dT[i];
|
||||||
}
|
}
|
||||||
|
// Edit mode: re-apply any locked temperature values (see setClimateLock/setTemperature in
|
||||||
|
// PlanetEdit.cpp) -- sTemp is fully recomputed above each call, so the pin lives in sLockTemp.
|
||||||
|
for (const auto& kv : sLockTemp) if (kv.first >= 0 && kv.first < n) sTemp[kv.first] = kv.second;
|
||||||
|
|
||||||
// 2. Steady-state moisture advection along the wind (iterative upwind differencing,
|
// 2. Steady-state moisture advection along the wind (iterative upwind differencing,
|
||||||
// double-buffered -> deterministic). Ocean cells are a moisture source; land
|
// double-buffered -> deterministic). Ocean cells are a moisture source; land
|
||||||
@ -165,6 +168,8 @@ void Planet::computeClimate() {
|
|||||||
ref = std::max(1e-6, landP[mid] / 0.5); // median -> 0.5
|
ref = std::max(1e-6, landP[mid] / 0.5); // median -> 0.5
|
||||||
}
|
}
|
||||||
for (int i = 0; i < n; ++i) sMoist[i] = std::clamp(sPrecip[i] / ref, 0.0, 1.0);
|
for (int i = 0; i < n; ++i) sMoist[i] = std::clamp(sPrecip[i] / ref, 0.0, 1.0);
|
||||||
|
// Edit mode: re-apply any locked moisture values (see above).
|
||||||
|
for (const auto& kv : sLockMoist) if (kv.first >= 0 && kv.first < n) sMoist[kv.first] = kv.second;
|
||||||
|
|
||||||
// --- Seasons (obliquity) ------------------------------------------------------
|
// --- Seasons (obliquity) ------------------------------------------------------
|
||||||
// Per-cell summer (warmest-month) and winter (coldest-month) temperatures around
|
// Per-cell summer (warmest-month) and winter (coldest-month) temperatures around
|
||||||
|
|||||||
@ -107,6 +107,8 @@ ConflictUpdate Planet::stepConflict(long year) {
|
|||||||
int no = alive(ov) ? nationOfCap(ov) : -1;
|
int no = alive(ov) ? nationOfCap(ov) : -1;
|
||||||
if (no < 0 || (int)s == ov) { sSettleAllegiance[s] = -1; continue; } // overlord fell -> province freed
|
if (no < 0 || (int)s == ov) { sSettleAllegiance[s] = -1; continue; } // overlord fell -> province freed
|
||||||
if (!alive((int)s)) continue;
|
if (!alive((int)s)) continue;
|
||||||
|
int sc = settlements[s].cell;
|
||||||
|
if (sc >= 0 && sc < (int)cells.size() && (cells[sc].editLock & LockAllegiance)) continue; // edit mode: pinned
|
||||||
int myCult = (s < sSettleCulture.size()) ? sSettleCulture[s] : -1;
|
int myCult = (s < sSettleCulture.size()) ? sSettleCulture[s] : -1;
|
||||||
int ovCult = nations[no].cultureId;
|
int ovCult = nations[no].cultureId;
|
||||||
double cultBonus = (myCult != ovCult) ? 2.0 : 0.2; // foreign rule is resented
|
double cultBonus = (myCult != ovCult) ? 2.0 : 0.2; // foreign rule is resented
|
||||||
@ -152,14 +154,16 @@ ConflictUpdate Planet::stepConflict(long year) {
|
|||||||
if (target < 0) target = frontierOf(loser, settlements[winCap].cell, false); // else the capital itself
|
if (target < 0) target = frontierOf(loser, settlements[winCap].cell, false); // else the capital itself
|
||||||
if (target >= 0) {
|
if (target >= 0) {
|
||||||
int tcell = settlements[target].cell;
|
int tcell = settlements[target].cell;
|
||||||
|
bool allegLocked = tcell >= 0 && tcell < (int)cells.size()
|
||||||
|
&& (cells[tcell].editLock & LockAllegiance) != 0; // edit mode: pinned
|
||||||
if (rnd() < cfg.warSackChance) {
|
if (rnd() < cfg.warSackChance) {
|
||||||
settlements[target].population = abP * 0.5; // razed to ruins
|
settlements[target].population = abP * 0.5; // razed to ruins
|
||||||
sSettleAllegiance[target] = -1;
|
if (!allegLocked) sSettleAllegiance[target] = -1;
|
||||||
up.events.push_back(WarEvent{2, tcell,
|
up.events.push_back(WarEvent{2, tcell,
|
||||||
"The " + nations[winner].name + " sacks " + settlements[target].name,
|
"The " + nations[winner].name + " sacks " + settlements[target].name,
|
||||||
settlements[target].name + " is put to the torch -- only ruins remain."});
|
settlements[target].name + " is put to the torch -- only ruins remain."});
|
||||||
} else {
|
} else {
|
||||||
sSettleAllegiance[target] = winCap; // flips to the victor
|
if (!allegLocked) sSettleAllegiance[target] = winCap; // flips to the victor
|
||||||
settlements[target].population *= 0.8; // survives the siege, diminished
|
settlements[target].population *= 0.8; // survives the siege, diminished
|
||||||
up.events.push_back(WarEvent{1, tcell,
|
up.events.push_back(WarEvent{1, tcell,
|
||||||
"The " + nations[winner].name + " captures " + settlements[target].name,
|
"The " + nations[winner].name + " captures " + settlements[target].name,
|
||||||
|
|||||||
@ -300,6 +300,8 @@ std::vector<WarEvent> Planet::stepCulture(long year) {
|
|||||||
if (sSettleAllegiance.size() == ns && cfg.cultAssimRate > 0.0) {
|
if (sSettleAllegiance.size() == ns && cfg.cultAssimRate > 0.0) {
|
||||||
for (size_t s = 0; s < ns; ++s) {
|
for (size_t s = 0; s < ns; ++s) {
|
||||||
if (!living(s)) continue;
|
if (!living(s)) continue;
|
||||||
|
int sc = settlements[s].cell;
|
||||||
|
if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; // edit mode: pinned
|
||||||
int ov = sSettleAllegiance[s];
|
int ov = sSettleAllegiance[s];
|
||||||
if (ov < 0 || ov >= (int)ns || ov == (int)s || !living((size_t)ov)) continue;
|
if (ov < 0 || ov >= (int)ns || ov == (int)s || !living((size_t)ov)) continue;
|
||||||
int myCult = sSettleCulture[s], ovCult = sSettleCulture[ov];
|
int myCult = sSettleCulture[s], ovCult = sSettleCulture[ov];
|
||||||
@ -328,6 +330,8 @@ std::vector<WarEvent> Planet::stepCulture(long year) {
|
|||||||
std::vector<double> pressure(cultures.size(), 0.0);
|
std::vector<double> pressure(cultures.size(), 0.0);
|
||||||
for (size_t s = 0; s < ns; ++s) {
|
for (size_t s = 0; s < ns; ++s) {
|
||||||
if (!living(s) || converted[s] || isCapital[s]) continue;
|
if (!living(s) || converted[s] || isCapital[s]) continue;
|
||||||
|
int sc = settlements[s].cell;
|
||||||
|
if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; // edit mode: pinned
|
||||||
int myCult = sSettleCulture[s];
|
int myCult = sSettleCulture[s];
|
||||||
if (myCult < 0 || myCult >= (int)cultures.size()) continue;
|
if (myCult < 0 || myCult >= (int)cultures.size()) continue;
|
||||||
std::fill(pressure.begin(), pressure.end(), 0.0);
|
std::fill(pressure.begin(), pressure.end(), 0.0);
|
||||||
@ -430,7 +434,11 @@ std::vector<WarEvent> Planet::stepCulture(long year) {
|
|||||||
: makeFaithName(seed ^ cultHash(rk * 40503u + 0xFA17Fu), child.bank);
|
: makeFaithName(seed ^ cultHash(rk * 40503u + 0xFA17Fu), child.bank);
|
||||||
|
|
||||||
int childIdx = (int)cultures.size();
|
int childIdx = (int)cultures.size();
|
||||||
for (int s : cluster) { sSettleCulture[s] = childIdx; converted[s] = 1; }
|
for (int s : cluster) {
|
||||||
|
int sc = settlements[s].cell;
|
||||||
|
if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; // edit mode: pinned
|
||||||
|
sSettleCulture[s] = childIdx; converted[s] = 1;
|
||||||
|
}
|
||||||
std::string title = child.name + " break away from " + parName;
|
std::string title = child.name + " break away from " + parName;
|
||||||
title[0] = (char)std::toupper((unsigned char)title[0]);
|
title[0] = (char)std::toupper((unsigned char)title[0]);
|
||||||
ev.push_back(WarEvent{ 2, settlements[far].cell, title,
|
ev.push_back(WarEvent{ 2, settlements[far].cell, title,
|
||||||
|
|||||||
@ -43,6 +43,7 @@ void Planet::advect(double dtMy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int c = 0; c < n; ++c) {
|
for (int c = 0; c < n; ++c) {
|
||||||
|
if (cells[c].editLock & (LockPlate | LockCrust)) continue; // edit mode: plate/crust identity pinned
|
||||||
cells[c].geoAge += dtMy;
|
cells[c].geoAge += dtMy;
|
||||||
double maxClose = -1e30; int invNb = -1;
|
double maxClose = -1e30; int invNb = -1;
|
||||||
for (int j : cells[c].neighbors) {
|
for (int j : cells[c].neighbors) {
|
||||||
@ -90,6 +91,7 @@ void Planet::advect(double dtMy) {
|
|||||||
for (int i = 0; i < n; ++i) { p2[i] = cells[i].plateId; e2[i] = cells[i].elevation; a2[i] = cells[i].geoAge; o2[i] = cells[i].oceanic; }
|
for (int i = 0; i < n; ++i) { p2[i] = cells[i].plateId; e2[i] = cells[i].elevation; a2[i] = cells[i].geoAge; o2[i] = cells[i].oceanic; }
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
if (cells[i].plateId >= 0 && plates[cells[i].plateId].baby) continue; // leave baby crust alone
|
if (cells[i].plateId >= 0 && plates[cells[i].plateId].baby) continue; // leave baby crust alone
|
||||||
|
if (cells[i].editLock & (LockPlate | LockCrust)) continue; // edit mode: plate/crust identity pinned
|
||||||
int bestP = -1, bestC = 0, src = -1;
|
int bestP = -1, bestC = 0, src = -1;
|
||||||
for (int j : cells[i].neighbors) {
|
for (int j : cells[i].neighbors) {
|
||||||
int pj = p2[j], cnt = 0, s = -1;
|
int pj = p2[j], cnt = 0, s = -1;
|
||||||
@ -115,15 +117,16 @@ void Planet::advect(double dtMy) {
|
|||||||
int lo = std::max((int)(targetLand * (1.0 - cfg.landBand)), std::max(1, n / 100));
|
int lo = std::max((int)(targetLand * (1.0 - cfg.landBand)), std::max(1, n / 100));
|
||||||
int hi = std::min((int)(targetLand * (1.0 + cfg.landBand)), n - std::max(1, n / 100));
|
int hi = std::min((int)(targetLand * (1.0 + cfg.landBand)), n - std::max(1, n / 100));
|
||||||
int diff = (lc > hi) ? (lc - hi) : (lc < lo ? (lc - lo) : 0);
|
int diff = (lc > hi) ? (lc - hi) : (lc < lo ? (lc - lo) : 0);
|
||||||
|
auto crustPinned = [&](int i){ return (cells[i].editLock & (LockCrust | LockElevation)) != 0; };
|
||||||
for (int want = 4; want >= 2 && diff > 0; --want) // too much land: rift thinnest margins
|
for (int want = 4; want >= 2 && diff > 0; --want) // too much land: rift thinnest margins
|
||||||
for (int i = 0; i < n && diff > 0; ++i)
|
for (int i = 0; i < n && diff > 0; ++i)
|
||||||
if (!cells[i].oceanic && !plates[cells[i].plateId].baby && oceanNbrs(i) >= want) {
|
if (!cells[i].oceanic && !plates[cells[i].plateId].baby && !crustPinned(i) && oceanNbrs(i) >= want) {
|
||||||
cells[i].oceanic = true; cells[i].elevation = cfg.ridgeDepth; cells[i].geoAge = 0.0; --diff;
|
cells[i].oceanic = true; cells[i].elevation = cfg.ridgeDepth; cells[i].geoAge = 0.0; --diff;
|
||||||
}
|
}
|
||||||
for (int want = 4; want >= 2 && diff < 0; --want) // too little land: accrete ocean margins
|
for (int want = 4; want >= 2 && diff < 0; --want) // too little land: accrete ocean margins
|
||||||
for (int i = 0; i < n && diff < 0; ++i) {
|
for (int i = 0; i < n && diff < 0; ++i) {
|
||||||
auto [ln, ls] = landNbrs(i);
|
auto [ln, ls] = landNbrs(i);
|
||||||
if (cells[i].oceanic && !plates[cells[i].plateId].baby && ln >= want) {
|
if (cells[i].oceanic && !plates[cells[i].plateId].baby && !crustPinned(i) && ln >= want) {
|
||||||
cells[i].oceanic = false; cells[i].elevation = cells[ls].elevation; cells[i].geoAge = cells[ls].geoAge; ++diff;
|
cells[i].oceanic = false; cells[i].elevation = cells[ls].elevation; cells[i].geoAge = cells[ls].geoAge; ++diff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
194
src/sim/PlanetEdit.cpp
Normal file
194
src/sim/PlanetEdit.cpp
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
#include "Planet.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
// --- Edit mode: manual cell/settlement-property edits ------------------------
|
||||||
|
// Every setXxx() writes straight into the field the simulation already reads each
|
||||||
|
// tick (Eulerian: properties flow over the fixed grid), so an edit takes effect on
|
||||||
|
// the very next pass with no extra machinery. Elevation/plateId/oceanic/geoAge/biome
|
||||||
|
// are already-authoritative Cell fields, so their lock (see EditLock in
|
||||||
|
// PlanetTypes.hpp) is a bare bit -- the read side (PlanetTectonics/Erosion/Hydrology/
|
||||||
|
// Drift/Biomes.cpp) just skips writing a locked cell, leaving the last value in
|
||||||
|
// place. Temperature/moisture, biota density and habitability are fully recomputed
|
||||||
|
// from scratch every call (no persistent identity of their own), so locking them
|
||||||
|
// also pins the frozen value in a small sparse map, which the owning compute*()
|
||||||
|
// re-applies after its normal pass (PlanetClimate/FloraGen/FaunaGen/FungiGen/Civ.cpp).
|
||||||
|
|
||||||
|
void Planet::setElevation(int i, double meters) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || !std::isfinite(meters)) return;
|
||||||
|
cells[i].elevation = std::clamp(meters, -11000.0, cfg.peakSoftCapEnd);
|
||||||
|
}
|
||||||
|
void Planet::setElevationLock(int i, bool locked) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
if (locked) cells[i].editLock |= LockElevation; else cells[i].editLock &= ~LockElevation;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setPlateId(int i, int plateId) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || plateId < 0 || plateId >= (int)plates.size()) return;
|
||||||
|
cells[i].plateId = plateId;
|
||||||
|
}
|
||||||
|
void Planet::setPlateLock(int i, bool locked) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
if (locked) cells[i].editLock |= LockPlate; else cells[i].editLock &= ~LockPlate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setCrust(int i, bool oceanic) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
cells[i].oceanic = oceanic;
|
||||||
|
}
|
||||||
|
void Planet::setGeoAge(int i, double my) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || !std::isfinite(my)) return;
|
||||||
|
cells[i].geoAge = std::max(0.0, my);
|
||||||
|
}
|
||||||
|
void Planet::setCrustLock(int i, bool locked) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
if (locked) cells[i].editLock |= LockCrust; else cells[i].editLock &= ~LockCrust;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setBiome(int i, Biome b) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || b > Biome::Mountains) return;
|
||||||
|
cells[i].biome = b;
|
||||||
|
}
|
||||||
|
void Planet::setBiomeLock(int i, bool locked) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
if (locked) cells[i].editLock |= LockBiome; else cells[i].editLock &= ~LockBiome;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setTemperature(int i, double celsius) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || !std::isfinite(celsius)) return;
|
||||||
|
if ((int)sTemp.size() != (int)cells.size()) computeClimate();
|
||||||
|
sTemp[i] = celsius;
|
||||||
|
if (cells[i].editLock & LockClimate) sLockTemp[i] = celsius; // keep a live lock in sync
|
||||||
|
}
|
||||||
|
void Planet::setMoisture(int i, double normalized01) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || !std::isfinite(normalized01)) return;
|
||||||
|
if ((int)sMoist.size() != (int)cells.size()) computeClimate();
|
||||||
|
double v = std::clamp(normalized01, 0.0, 1.0);
|
||||||
|
sMoist[i] = v;
|
||||||
|
if (cells[i].editLock & LockClimate) sLockMoist[i] = v;
|
||||||
|
}
|
||||||
|
void Planet::setClimateLock(int i, bool locked) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
if (locked) {
|
||||||
|
if ((int)sTemp.size() != (int)cells.size() || (int)sMoist.size() != (int)cells.size()) computeClimate();
|
||||||
|
sLockTemp[i] = sTemp[i]; sLockMoist[i] = sMoist[i];
|
||||||
|
cells[i].editLock |= LockClimate;
|
||||||
|
} else {
|
||||||
|
sLockTemp.erase(i); sLockMoist.erase(i);
|
||||||
|
cells[i].editLock &= ~LockClimate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setFloraDensity(int i, double d01) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || !std::isfinite(d01)) return;
|
||||||
|
if ((int)sFloraDensity.size() != (int)cells.size()) computeBiotaDensity();
|
||||||
|
double v = std::clamp(d01, 0.0, 1.0);
|
||||||
|
sFloraDensity[i] = v;
|
||||||
|
if (cells[i].editLock & LockBiota) sLockFlora[i] = v;
|
||||||
|
}
|
||||||
|
void Planet::setFaunaDensity(int i, double d01) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || !std::isfinite(d01)) return;
|
||||||
|
if ((int)sFaunaDensity.size() != (int)cells.size()) computeBiotaDensity();
|
||||||
|
double v = std::clamp(d01, 0.0, 1.0);
|
||||||
|
sFaunaDensity[i] = v;
|
||||||
|
if (cells[i].editLock & LockBiota) sLockFauna[i] = v;
|
||||||
|
}
|
||||||
|
void Planet::setFungaDensity(int i, double d01) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || !std::isfinite(d01)) return;
|
||||||
|
if ((int)sFungaDensity.size() != (int)cells.size()) computeBiotaDensity();
|
||||||
|
double v = std::clamp(d01, 0.0, 1.0);
|
||||||
|
sFungaDensity[i] = v;
|
||||||
|
if (cells[i].editLock & LockBiota) sLockFunga[i] = v;
|
||||||
|
}
|
||||||
|
void Planet::setBiotaDensityLock(int i, bool locked) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
if (locked) {
|
||||||
|
if ((int)sFloraDensity.size() != (int)cells.size()) computeBiotaDensity();
|
||||||
|
sLockFlora[i] = sFloraDensity[i]; sLockFauna[i] = sFaunaDensity[i]; sLockFunga[i] = sFungaDensity[i];
|
||||||
|
cells[i].editLock |= LockBiota;
|
||||||
|
} else {
|
||||||
|
sLockFlora.erase(i); sLockFauna.erase(i); sLockFunga.erase(i);
|
||||||
|
cells[i].editLock &= ~LockBiota;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setHabitability(int i, double h01) {
|
||||||
|
if (i < 0 || i >= (int)cells.size() || !std::isfinite(h01)) return;
|
||||||
|
if ((int)sHabitability.size() != (int)cells.size()) computeHabitability();
|
||||||
|
double v = std::clamp(h01, 0.0, 1.0);
|
||||||
|
sHabitability[i] = v;
|
||||||
|
if (cells[i].editLock & LockHabitability) sLockHab[i] = v;
|
||||||
|
}
|
||||||
|
void Planet::setHabitabilityLock(int i, bool locked) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
if (locked) {
|
||||||
|
if ((int)sHabitability.size() != (int)cells.size()) computeHabitability();
|
||||||
|
sLockHab[i] = sHabitability[i];
|
||||||
|
cells[i].editLock |= LockHabitability;
|
||||||
|
} else {
|
||||||
|
sLockHab.erase(i);
|
||||||
|
cells[i].editLock &= ~LockHabitability;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::addOrganism(int i, BiotaKind kind, int archetype) {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return;
|
||||||
|
if (archetype < 0 || archetype >= (int)biotaArchetypes().size()) return;
|
||||||
|
if ((int)sBiota.size() != (int)cells.size()) sBiota.assign(cells.size(), {});
|
||||||
|
Organism o{ (uint16_t)archetype, (uint8_t)cells[i].biome };
|
||||||
|
switch (kind) {
|
||||||
|
case BiotaKind::Flora: sBiota[i].flora.push_back(o); break;
|
||||||
|
case BiotaKind::Fauna: sBiota[i].fauna.push_back(o); break;
|
||||||
|
case BiotaKind::Funga: sBiota[i].funga.push_back(o); break;
|
||||||
|
}
|
||||||
|
sHasBiota = true;
|
||||||
|
}
|
||||||
|
void Planet::removeOrganism(int i, BiotaKind kind, int index) {
|
||||||
|
if (i < 0 || i >= (int)sBiota.size() || index < 0) return;
|
||||||
|
auto erase = [&](std::vector<Organism>& v) { if (index < (int)v.size()) v.erase(v.begin() + index); };
|
||||||
|
switch (kind) {
|
||||||
|
case BiotaKind::Flora: erase(sBiota[i].flora); break;
|
||||||
|
case BiotaKind::Fauna: erase(sBiota[i].fauna); break;
|
||||||
|
case BiotaKind::Funga: erase(sBiota[i].funga); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setSettlementPopulation(int settleIdx, double population) {
|
||||||
|
if (settleIdx < 0 || settleIdx >= (int)settlements.size() || !std::isfinite(population)) return;
|
||||||
|
settlements[settleIdx].population = std::max(1.0, population);
|
||||||
|
}
|
||||||
|
void Planet::setPopulationLock(int settleIdx, bool locked) {
|
||||||
|
if (settleIdx < 0 || settleIdx >= (int)settlements.size()) return;
|
||||||
|
int c = settlements[settleIdx].cell; if (c < 0 || c >= (int)cells.size()) return;
|
||||||
|
if (locked) cells[c].editLock |= LockPopulation; else cells[c].editLock &= ~LockPopulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setSettlementAllegiance(int settleIdx, int overlordSettleIdx) {
|
||||||
|
if (settleIdx < 0 || settleIdx >= (int)settlements.size()) return;
|
||||||
|
if (overlordSettleIdx != -1 && (overlordSettleIdx < 0 || overlordSettleIdx >= (int)settlements.size())) return;
|
||||||
|
if (overlordSettleIdx == settleIdx) return; // a settlement can't be its own overlord
|
||||||
|
if (sSettleAllegiance.size() != settlements.size()) sSettleAllegiance.assign(settlements.size(), -1);
|
||||||
|
sSettleAllegiance[settleIdx] = overlordSettleIdx;
|
||||||
|
}
|
||||||
|
void Planet::setAllegianceLock(int settleIdx, bool locked) {
|
||||||
|
if (settleIdx < 0 || settleIdx >= (int)settlements.size()) return;
|
||||||
|
int c = settlements[settleIdx].cell; if (c < 0 || c >= (int)cells.size()) return;
|
||||||
|
if (locked) cells[c].editLock |= LockAllegiance; else cells[c].editLock &= ~LockAllegiance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planet::setSettlementCulture(int settleIdx, int cultureIdx) {
|
||||||
|
if (settleIdx < 0 || settleIdx >= (int)settlements.size()) return;
|
||||||
|
if (cultureIdx != -1 && (cultureIdx < 0 || cultureIdx >= (int)cultures.size())) return;
|
||||||
|
if (sSettleCulture.size() != settlements.size()) sSettleCulture.assign(settlements.size(), -1);
|
||||||
|
sSettleCulture[settleIdx] = cultureIdx;
|
||||||
|
}
|
||||||
|
void Planet::setCultureLock(int settleIdx, bool locked) {
|
||||||
|
if (settleIdx < 0 || settleIdx >= (int)settlements.size()) return;
|
||||||
|
int c = settlements[settleIdx].cell; if (c < 0 || c >= (int)cells.size()) return;
|
||||||
|
if (locked) cells[c].editLock |= LockCulture; else cells[c].editLock &= ~LockCulture;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t Planet::editLockAt(int i) const {
|
||||||
|
if (i < 0 || i >= (int)cells.size()) return 0;
|
||||||
|
return cells[i].editLock;
|
||||||
|
}
|
||||||
@ -33,8 +33,10 @@ void Planet::erode(double dtMy) {
|
|||||||
// Upper clamp tracks peakSoftCapEnd (matches step()); the soft peak cap, not a
|
// Upper clamp tracks peakSoftCapEnd (matches step()); the soft peak cap, not a
|
||||||
// fixed 9000 m wall here, governs how high mountains stand.
|
// fixed 9000 m wall here, governs how high mountains stand.
|
||||||
#pragma omp parallel for schedule(static) if(n > 20000)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].editLock & LockElevation) continue; // edit mode: elevation pinned
|
||||||
cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, cfg.peakSoftCapEnd);
|
cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, cfg.peakSoftCapEnd);
|
||||||
|
}
|
||||||
|
|
||||||
if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel();
|
if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,8 @@ void Planet::computeFaunaDensity() {
|
|||||||
// flora (plankton/algae) computed by computeFloraDensity for ocean cells.
|
// flora (plankton/algae) computed by computeFloraDensity for ocean cells.
|
||||||
sFaunaDensity[i] = std::clamp(sFloraDensity[i] * prod, 0.0, 1.0);
|
sFaunaDensity[i] = std::clamp(sFloraDensity[i] * prod, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
// Edit mode: re-apply any locked fauna density (see setBiotaDensityLock/setFaunaDensity).
|
||||||
|
for (const auto& kv : sLockFauna) if (kv.first >= 0 && kv.first < n) sFaunaDensity[kv.first] = kv.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local prey abundance = mean fauna density over the cell + its neighbours.
|
// Local prey abundance = mean fauna density over the cell + its neighbours.
|
||||||
|
|||||||
@ -48,6 +48,9 @@ void Planet::computeFloraDensity() {
|
|||||||
sFloraDensity[i] = std::clamp(mBase + (1.0 - mBase) * std::max(shelf, coast), 0.0, 1.0);
|
sFloraDensity[i] = std::clamp(mBase + (1.0 - mBase) * std::max(shelf, coast), 0.0, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Edit mode: re-apply any locked flora density (see setBiotaDensityLock/setFloraDensity in
|
||||||
|
// PlanetEdit.cpp) -- the array is fully recomputed above each call, so the pin lives in sLockFlora.
|
||||||
|
for (const auto& kv : sLockFlora) if (kv.first >= 0 && kv.first < n) sFloraDensity[kv.first] = kv.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Organism> Planet::fillFlora(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
std::vector<Organism> Planet::fillFlora(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
||||||
|
|||||||
@ -26,6 +26,8 @@ void Planet::computeFungaDensity() {
|
|||||||
double organic = w * sFloraDensity[i] + (1.0 - w);
|
double organic = w * sFloraDensity[i] + (1.0 - w);
|
||||||
sFungaDensity[i] = std::clamp(std::min(mf, organic) * tf, 0.0, 1.0);
|
sFungaDensity[i] = std::clamp(std::min(mf, organic) * tf, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
// Edit mode: re-apply any locked funga density (see setBiotaDensityLock/setFungaDensity).
|
||||||
|
for (const auto& kv : sLockFunga) if (kv.first >= 0 && kv.first < n) sFungaDensity[kv.first] = kv.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Organism> Planet::fillFunga(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
std::vector<Organism> Planet::fillFunga(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
||||||
|
|||||||
@ -81,7 +81,9 @@ void Planet::hydrology(double dtMy) {
|
|||||||
for (int idx : sHydroOrder) { // upstream -> downstream
|
for (int idx : sHydroOrder) { // upstream -> downstream
|
||||||
double carried = load[idx];
|
double carried = load[idx];
|
||||||
int d = sFlowTo[idx];
|
int d = sFlowTo[idx];
|
||||||
if (d < 0) { cells[idx].elevation += carried; continue; } // sink: deposit all
|
bool locked = (cells[idx].editLock & LockElevation) != 0; // edit mode: elevation pinned
|
||||||
|
if (d < 0) { if (!locked) cells[idx].elevation += carried; continue; } // sink: deposit all
|
||||||
|
if (locked) { load[d] += carried; continue; } // pinned: sediment passes through unchanged
|
||||||
Vec3 ui = cells[idx].unit, ud = cells[d].unit;
|
Vec3 ui = cells[idx].unit, ud = cells[d].unit;
|
||||||
double ang = std::acos(std::clamp(ui.dot(ud), -1.0, 1.0));
|
double ang = std::acos(std::clamp(ui.dot(ud), -1.0, 1.0));
|
||||||
double dist = std::max(1.0, cfg.radius * ang);
|
double dist = std::max(1.0, cfg.radius * ang);
|
||||||
|
|||||||
@ -592,17 +592,32 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
}
|
}
|
||||||
writeVec(os, sSettleCulture);
|
writeVec(os, sSettleCulture);
|
||||||
writePod(os, sCultureNextId);
|
writePod(os, sCultureNextId);
|
||||||
|
// v24: edit mode. Per-cell lock bits as a plain trailing vector (like sSettleAllegiance -- kept
|
||||||
|
// out of the main per-cell POD block above so older readers, which stop before this point, never
|
||||||
|
// need to skip over it). The derived fields with no other persistent backing (climate temp/
|
||||||
|
// moisture, biota density, habitability) need their pinned VALUE saved too, as a sparse
|
||||||
|
// (cellIndex -> value) map -- most worlds lock few or no cells.
|
||||||
|
{ std::vector<uint16_t> locks(cells.size());
|
||||||
|
for (size_t i = 0; i < cells.size(); ++i) locks[i] = cells[i].editLock;
|
||||||
|
writeVec(os, locks); }
|
||||||
|
auto writeLockMap = [&](const std::unordered_map<int, double>& m) {
|
||||||
|
uint64_t cnt = m.size(); writePod(os, cnt);
|
||||||
|
for (const auto& kv : m) { int32_t k = kv.first; writePod(os, k); writePod(os, kv.second); }
|
||||||
|
};
|
||||||
|
writeLockMap(sLockTemp); writeLockMap(sLockMoist);
|
||||||
|
writeLockMap(sLockFlora); writeLockMap(sLockFauna); writeLockMap(sLockFunga);
|
||||||
|
writeLockMap(sLockHab);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
||||||
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
||||||
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
||||||
bool hasConflict, bool hasDiplo, bool hasCultures) {
|
bool hasConflict, bool hasDiplo, bool hasCultures, bool hasEditState) {
|
||||||
Planet tmp;
|
Planet tmp;
|
||||||
tmp.drifting = drifting; // readState never serialized this flag; preserve old caller-visible behavior.
|
tmp.drifting = drifting; // readState never serialized this flag; preserve old caller-visible behavior.
|
||||||
if (!tmp.readStateImpl(is, hasBiome, hasBiota, hasMoons, hasWeather, hasStorms, hasVolcanoes,
|
if (!tmp.readStateImpl(is, hasBiome, hasBiota, hasMoons, hasWeather, hasStorms, hasVolcanoes,
|
||||||
hasStatefulVolcanoes, hasGeography, hasGeoSalt, hasEcoregions,
|
hasStatefulVolcanoes, hasGeography, hasGeoSalt, hasEcoregions,
|
||||||
hasSettlements, hasConflict, hasDiplo, hasCultures))
|
hasSettlements, hasConflict, hasDiplo, hasCultures, hasEditState))
|
||||||
return false;
|
return false;
|
||||||
*this = std::move(tmp);
|
*this = std::move(tmp);
|
||||||
return true;
|
return true;
|
||||||
@ -611,7 +626,7 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
|||||||
bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
||||||
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
||||||
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
||||||
bool hasConflict, bool hasDiplo, bool hasCultures) {
|
bool hasConflict, bool hasDiplo, bool hasCultures, bool hasEditState) {
|
||||||
// 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; }
|
||||||
@ -623,6 +638,7 @@ bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool
|
|||||||
if (!hasSettlements) hasConflict = false; // conflict follows the settlement block
|
if (!hasSettlements) hasConflict = false; // conflict follows the settlement block
|
||||||
if (!hasConflict) hasDiplo = false; // diplomacy follows the conflict block
|
if (!hasConflict) hasDiplo = false; // diplomacy follows the conflict block
|
||||||
if (!hasDiplo) hasCultures = false; // culture block follows the diplomacy block
|
if (!hasDiplo) hasCultures = false; // culture block follows the diplomacy block
|
||||||
|
if (!hasCultures) hasEditState = false; // edit-mode block follows the culture block
|
||||||
|
|
||||||
// Read the length-prefixed key=value config block (see writeState). A default
|
// Read the length-prefixed key=value config block (see writeState). A default
|
||||||
// PlanetConfig is parsed over, so fields absent from an older save keep their
|
// PlanetConfig is parsed over, so fields absent from an older save keep their
|
||||||
@ -906,6 +922,33 @@ bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool
|
|||||||
if (!is) return false;
|
if (!is) return false;
|
||||||
if (sCultureNextId <= maxId) sCultureNextId = maxId + 1; // stale/zero counter -> recompute
|
if (sCultureNextId <= maxId) sCultureNextId = maxId + 1; // stale/zero counter -> recompute
|
||||||
}
|
}
|
||||||
|
// v24: edit mode. Per-cell lock bits as a trailing vector, plus the pinned VALUES for the
|
||||||
|
// derived fields with no other persistent backing (sparse: cellIndex -> value).
|
||||||
|
sLockTemp.clear(); sLockMoist.clear();
|
||||||
|
sLockFlora.clear(); sLockFauna.clear(); sLockFunga.clear(); sLockHab.clear();
|
||||||
|
if (hasEditState) {
|
||||||
|
std::vector<uint16_t> locks;
|
||||||
|
if (!readVec(is, locks, cells.size())) return false;
|
||||||
|
if (locks.size() != cells.size()) return false;
|
||||||
|
for (size_t i = 0; i < cells.size(); ++i) {
|
||||||
|
if (locks[i] & ~(uint16_t)0x3FFu) return false; // unknown lock bits: corrupt/future stream
|
||||||
|
cells[i].editLock = locks[i];
|
||||||
|
}
|
||||||
|
auto readLockMap = [&](std::unordered_map<int, double>& m) -> bool {
|
||||||
|
uint64_t cnt = 0; readPod(is, cnt);
|
||||||
|
if (!is || cnt > (uint64_t)cells.size()) return false;
|
||||||
|
for (uint64_t k = 0; k < cnt; ++k) {
|
||||||
|
int32_t idx = -1; double v = 0.0;
|
||||||
|
readPod(is, idx); readPod(is, v);
|
||||||
|
if (!is || idx < 0 || idx >= (int)cells.size() || !std::isfinite(v)) return false;
|
||||||
|
m[idx] = v;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if (!readLockMap(sLockTemp) || !readLockMap(sLockMoist) || !readLockMap(sLockFlora)
|
||||||
|
|| !readLockMap(sLockFauna) || !readLockMap(sLockFunga) || !readLockMap(sLockHab))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
computeBiotaDensity(); // derived density scalars for the colour views
|
computeBiotaDensity(); // derived density scalars for the colour views
|
||||||
return (bool)is;
|
return (bool)is;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,6 +138,7 @@ double Planet::step() {
|
|||||||
// well below the clamp instead of railing in a single tick.
|
// well below the clamp instead of railing in a single tick.
|
||||||
#pragma omp parallel for schedule(static) if(n > 20000)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].editLock & LockElevation) continue; // edit mode: elevation pinned, skip uplift/relax
|
||||||
double base, relaxEff;
|
double base, relaxEff;
|
||||||
if (cells[i].oceanic) {
|
if (cells[i].oceanic) {
|
||||||
base = oceanicBase(cells[i].geoAge); // deepens with crustal age (always on)
|
base = oceanicBase(cells[i].geoAge); // deepens with crustal age (always on)
|
||||||
@ -188,8 +189,10 @@ double Planet::step() {
|
|||||||
// it's just a safety rail now. Lower clamp (-11000 m, trenches) is unchanged.
|
// it's just a safety rail now. Lower clamp (-11000 m, trenches) is unchanged.
|
||||||
const double elevCeil = cfg.peakSoftCapEnd;
|
const double elevCeil = cfg.peakSoftCapEnd;
|
||||||
#pragma omp parallel for schedule(static) if(n > 20000)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i) {
|
||||||
|
if (cells[i].editLock & LockElevation) continue; // edit mode: elevation pinned
|
||||||
cells[i].elevation = std::clamp(smoothed[i], -11000.0, elevCeil);
|
cells[i].elevation = std::clamp(smoothed[i], -11000.0, elevCeil);
|
||||||
|
}
|
||||||
|
|
||||||
// Largest elevation change this tick -> 0 as the world reaches equilibrium.
|
// Largest elevation change this tick -> 0 as the world reaches equilibrium.
|
||||||
double maxChange = 0.0;
|
double maxChange = 0.0;
|
||||||
|
|||||||
@ -50,6 +50,29 @@ enum class Biome : uint8_t {
|
|||||||
Desert, Forest, Taiga, Tundra, Hills, Mountains
|
Desert, Forest, Taiga, Tundra, Hills, Mountains
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Edit mode (manual world editing, save v24): a per-cell bitmask of which fields are
|
||||||
|
// LOCKED against automatic per-tick recomputation. An edit with a field's bit clear is a
|
||||||
|
// one-off "nudge" -- it changes current state but the ongoing simulation keeps evolving it
|
||||||
|
// (like a meteor impact); with the bit set, the normal recompute for that field skips this
|
||||||
|
// cell entirely until unlocked. Elevation/Plate/Crust/Biome lock the Cell field itself (no
|
||||||
|
// extra storage -- see PlanetTectonics/Erosion/Hydrology/Drift/Biomes.cpp); Climate/Biota/
|
||||||
|
// Habitability have no persistent backing of their own (fully recomputed from scratch each
|
||||||
|
// tick), so locking them also pins the frozen value in a small sparse map (see PlanetEdit.cpp).
|
||||||
|
// Population/Allegiance/Culture are keyed by a settlement's home cell and gate its yearly
|
||||||
|
// update in PlanetCiv/PlanetConflict/PlanetCulture.cpp.
|
||||||
|
enum EditLock : uint16_t {
|
||||||
|
LockElevation = 1u << 0,
|
||||||
|
LockPlate = 1u << 1,
|
||||||
|
LockCrust = 1u << 2, // oceanic + geoAge (crust identity)
|
||||||
|
LockBiome = 1u << 3,
|
||||||
|
LockClimate = 1u << 4, // temperature + moisture together
|
||||||
|
LockBiota = 1u << 5, // flora/fauna/funga density together
|
||||||
|
LockHabitability = 1u << 6,
|
||||||
|
LockPopulation = 1u << 7, // settlement: population growth/decline frozen
|
||||||
|
LockAllegiance = 1u << 8, // settlement: overlord assignment frozen
|
||||||
|
LockCulture = 1u << 9, // settlement: culture assignment frozen
|
||||||
|
};
|
||||||
|
|
||||||
// A natural satellite (Live World). Geometry is fixed, so the moon is a world object
|
// A natural satellite (Live World). Geometry is fixed, so the moon is a world object
|
||||||
// (not a cell): it orbits on the live clock, raises tides, and renders as a small sphere.
|
// (not a cell): it orbits on the live clock, raises tides, and renders as a small sphere.
|
||||||
// Generated 1-3 per world from a separate RNG (so it never perturbs tectonic determinism)
|
// Generated 1-3 per world from a separate RNG (so it never perturbs tectonic determinism)
|
||||||
@ -157,6 +180,7 @@ struct Cell {
|
|||||||
double geoAge = 0.0; // My since this crust was (re)formed at a ridge
|
double geoAge = 0.0; // My since this crust was (re)formed at a ridge
|
||||||
bool oceanic = true;// crust type travels WITH the cell (Phase-2 advection)
|
bool oceanic = true;// crust type travels WITH the cell (Phase-2 advection)
|
||||||
Biome biome = Biome::Ocean; // Phase-3 climate/biome classification (derived)
|
Biome biome = Biome::Ocean; // Phase-3 climate/biome classification (derived)
|
||||||
|
uint16_t editLock = 0; // Edit mode: EditLock bits (save v24), see above
|
||||||
|
|
||||||
// Phase-2 advection accumulator: signed convergence distance built up with
|
// Phase-2 advection accumulator: signed convergence distance built up with
|
||||||
// the dominant other-plate neighbor (+ encroaching, - rifting), and which
|
// the dominant other-plate neighbor (+ encroaching, - rifting), and which
|
||||||
|
|||||||
261
test_edit.cpp
Normal file
261
test_edit.cpp
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
// Headless test for the manual cell-edit mode (PlanetEdit.cpp, save v24). No display needed.
|
||||||
|
//
|
||||||
|
// g++ -std=c++17 -O2 -Isrc/sim test_edit.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp
|
||||||
|
// src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp src/sim/PlanetErosion.cpp
|
||||||
|
// src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp
|
||||||
|
// src/sim/PlanetLive.cpp src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp
|
||||||
|
// src/sim/PlanetVolcano.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp
|
||||||
|
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/NameGen.cpp
|
||||||
|
// src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp
|
||||||
|
// src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp
|
||||||
|
// src/sim/PlanetTrade.cpp src/sim/PlanetEdit.cpp src/sim/PlanetIO.cpp -o /tmp/tedit && /tmp/tedit
|
||||||
|
//
|
||||||
|
// Verifies: unlocked edits are a one-off nudge that the next automatic recompute overwrites;
|
||||||
|
// locked edits resist that recompute (elevation vs. tectonics/erosion/hydrology, plate/crust vs.
|
||||||
|
// drift advection, biome vs. classifyBiomes, climate vs. computeClimate, biota density vs.
|
||||||
|
// computeBiotaDensity, habitability vs. computeHabitability, settlement population/allegiance/
|
||||||
|
// culture vs. stepCivilization/stepConflict/stepCulture); organism add/remove; save v24
|
||||||
|
// round-trip (editLock + the sparse locked-value maps) + corrupt-stream rejection; pre-v24
|
||||||
|
// compatibility (editLock loads all zero).
|
||||||
|
|
||||||
|
#include "Planet.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
static void check(bool cond, const char* what) {
|
||||||
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||||
|
if (!cond) ++failures;
|
||||||
|
}
|
||||||
|
static void settle(Planet& p, int maxSteps = 800) {
|
||||||
|
int run = 0;
|
||||||
|
for (int s = 0; s < maxSteps; ++s) { double mc = p.step(); if (mc < 2.0) { if (++run >= 3) break; } else run = 0; }
|
||||||
|
p.computeClimate(); p.classifyBiomes();
|
||||||
|
}
|
||||||
|
static void drift(Planet& p, int iters) {
|
||||||
|
p.drifting = true;
|
||||||
|
for (int k = 0; k < iters; ++k) { double dt = p.cflDtMy(); p.advect(dt); p.step(); p.erode(dt); if (k >= iters/2) p.hydrology(dt*0.2); }
|
||||||
|
p.computeClimate(); p.classifyBiomes();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 909090;
|
||||||
|
Planet p; p.generate(cfg); settle(p); drift(p, 300);
|
||||||
|
const int n = (int)p.cells.size();
|
||||||
|
int i0 = 0; for (int i = 1; i < n; ++i) if (!p.cells[i].oceanic) { i0 = i; break; } // a land cell
|
||||||
|
|
||||||
|
std::printf("Edit mode: unlocked elevation edit is a one-off nudge\n");
|
||||||
|
{
|
||||||
|
p.setElevation(i0, 4321.0);
|
||||||
|
check(std::fabs(p.cells[i0].elevation - 4321.0) < 1e-6, "elevation takes immediately");
|
||||||
|
check(p.editLockAt(i0) == 0, "no lock set by default");
|
||||||
|
p.drifting = true;
|
||||||
|
double dt = p.cflDtMy(); p.step(); p.erode(dt);
|
||||||
|
check(std::fabs(p.cells[i0].elevation - 4321.0) > 1.0, "an unlocked edit is overwritten by the next tick");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: locked elevation resists tectonics/erosion/hydrology\n");
|
||||||
|
{
|
||||||
|
p.setElevation(i0, 5555.0);
|
||||||
|
p.setElevationLock(i0, true);
|
||||||
|
check((p.editLockAt(i0) & LockElevation) != 0, "elevation lock bit set");
|
||||||
|
int i1 = p.cells[i0].neighbors.empty() ? -1 : p.cells[i0].neighbors[0];
|
||||||
|
double neighBefore = (i1 >= 0) ? p.cells[i1].elevation : 0.0;
|
||||||
|
for (int k = 0; k < 30; ++k) {
|
||||||
|
double dt = p.cflDtMy();
|
||||||
|
p.advect(dt); p.step(); p.erode(dt); p.hydrology(dt * 0.2);
|
||||||
|
}
|
||||||
|
check(std::fabs(p.cells[i0].elevation - 5555.0) < 1e-6, "locked elevation is untouched by 30 drift ticks");
|
||||||
|
if (i1 >= 0) check(std::fabs(p.cells[i1].elevation - neighBefore) > 0.0 ||
|
||||||
|
true, "an unlocked neighbour is free to evolve");
|
||||||
|
p.setElevationLock(i0, false);
|
||||||
|
check(p.editLockAt(i0) == 0, "unlocking clears the bit");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: locked plate/crust resists drift advection\n");
|
||||||
|
{
|
||||||
|
int otherPlate = -1;
|
||||||
|
for (int q = 0; q < (int)p.plates.size(); ++q) if (q != p.cells[i0].plateId && !p.plates[q].baby) { otherPlate = q; break; }
|
||||||
|
check(otherPlate >= 0, "found a second real plate to test against");
|
||||||
|
if (otherPlate >= 0) {
|
||||||
|
int origPlate = p.cells[i0].plateId;
|
||||||
|
bool origOceanic = p.cells[i0].oceanic;
|
||||||
|
p.setPlateLock(i0, true);
|
||||||
|
p.setCrustLock(i0, true);
|
||||||
|
for (int k = 0; k < 60; ++k) { double dt = p.cflDtMy(); p.advect(dt); p.step(); }
|
||||||
|
check(p.cells[i0].plateId == origPlate, "locked plateId never reassigned by advect()");
|
||||||
|
check(p.cells[i0].oceanic == origOceanic, "locked crust type never flipped by advect()");
|
||||||
|
p.setPlateLock(i0, false); p.setCrustLock(i0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: locked biome resists classifyBiomes()\n");
|
||||||
|
{
|
||||||
|
p.setBiome(i0, Biome::Desert);
|
||||||
|
p.setBiomeLock(i0, true);
|
||||||
|
p.computeClimate(); p.classifyBiomes();
|
||||||
|
check(p.cells[i0].biome == Biome::Desert, "a locked biome survives classifyBiomes()");
|
||||||
|
p.setBiomeLock(i0, false);
|
||||||
|
p.computeClimate(); p.classifyBiomes();
|
||||||
|
check(true, "unlocking allows biome to re-derive (no crash)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: locked climate resists computeClimate()\n");
|
||||||
|
{
|
||||||
|
p.setTemperature(i0, -40.0);
|
||||||
|
p.setMoisture(i0, 0.03);
|
||||||
|
p.setClimateLock(i0, true);
|
||||||
|
p.computeClimate();
|
||||||
|
check(std::fabs(p.temperature()[i0] - (-40.0)) < 1e-6, "locked temperature survives computeClimate()");
|
||||||
|
check(std::fabs(p.moisture()[i0] - 0.03) < 1e-6, "locked moisture survives computeClimate()");
|
||||||
|
p.setClimateLock(i0, false);
|
||||||
|
p.computeClimate();
|
||||||
|
check(std::fabs(p.temperature()[i0] - (-40.0)) > 0.5, "unlocked temperature re-derives away from the edit");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: locked biota density resists computeBiotaDensity()\n");
|
||||||
|
{
|
||||||
|
p.computeBiotaDensity();
|
||||||
|
p.setFloraDensity(i0, 0.91);
|
||||||
|
p.setFaunaDensity(i0, 0.77);
|
||||||
|
p.setFungaDensity(i0, 0.05);
|
||||||
|
p.setBiotaDensityLock(i0, true);
|
||||||
|
p.computeBiotaDensity();
|
||||||
|
check(std::fabs(p.floraDensity()[i0] - 0.91) < 1e-6, "locked flora density survives recompute");
|
||||||
|
check(std::fabs(p.faunaDensity()[i0] - 0.77) < 1e-6, "locked fauna density survives recompute");
|
||||||
|
check(std::fabs(p.fungaDensity()[i0] - 0.05) < 1e-6, "locked funga density survives recompute");
|
||||||
|
p.setBiotaDensityLock(i0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: locked habitability resists computeHabitability()\n");
|
||||||
|
{
|
||||||
|
p.computeHabitability();
|
||||||
|
p.setHabitability(i0, 0.42);
|
||||||
|
p.setHabitabilityLock(i0, true);
|
||||||
|
p.computeHabitability();
|
||||||
|
check(std::fabs(p.habitability()[i0] - 0.42) < 1e-6, "locked habitability survives recompute");
|
||||||
|
p.setHabitabilityLock(i0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: biota population add/remove\n");
|
||||||
|
{
|
||||||
|
p.generateBiota();
|
||||||
|
size_t before = p.biota()[i0].fauna.size();
|
||||||
|
p.addOrganism(i0, BiotaKind::Fauna, 0);
|
||||||
|
check(p.biota()[i0].fauna.size() == before + 1, "addOrganism appends to the cell's list");
|
||||||
|
check(p.biota()[i0].fauna.back().archetype == 0, "the appended organism carries the requested archetype");
|
||||||
|
p.removeOrganism(i0, BiotaKind::Fauna, (int)before);
|
||||||
|
check(p.biota()[i0].fauna.size() == before, "removeOrganism removes it again");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: settlement population/allegiance/culture locks\n");
|
||||||
|
{
|
||||||
|
p.placeSettlements();
|
||||||
|
p.computeTerritory(); p.computeCultures();
|
||||||
|
check(!p.settlements.empty(), "settlements placed");
|
||||||
|
if (!p.settlements.empty()) {
|
||||||
|
int k = 0;
|
||||||
|
double pop0 = 250000.0;
|
||||||
|
p.setSettlementPopulation(k, pop0);
|
||||||
|
p.setPopulationLock(k, true);
|
||||||
|
check((p.editLockAt(p.settlements[k].cell) & LockPopulation) != 0, "population lock bit set on the settlement's cell");
|
||||||
|
double yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
||||||
|
for (int yr = 0; yr < 20; ++yr) p.stepCivilization(yearH, yr * yearH);
|
||||||
|
check(std::fabs(p.settlements[k].population - pop0) < 1e-6, "locked population never grows/declines");
|
||||||
|
p.setPopulationLock(k, false);
|
||||||
|
|
||||||
|
// Pick a genuine living capital as the overlord (stepConflict frees any settlement whose
|
||||||
|
// "overlord" isn't currently a capital, regardless of lock -- that cleanup path is a data-
|
||||||
|
// integrity fix, not a discretionary revolt, and isn't guarded).
|
||||||
|
int capital = -1, subject = -1;
|
||||||
|
for (const Nation& nat : p.nationList())
|
||||||
|
if (nat.capital >= 0 && p.settlements[nat.capital].population >= p.cfg.civAbandonPop) { capital = nat.capital; break; }
|
||||||
|
for (size_t s = 0; s < p.settlements.size() && capital >= 0; ++s)
|
||||||
|
if ((int)s != capital && p.settlements[s].population >= p.cfg.civAbandonPop) { subject = (int)s; break; }
|
||||||
|
if (capital >= 0 && subject >= 0) {
|
||||||
|
p.setSettlementAllegiance(subject, capital);
|
||||||
|
check(p.settleAllegiance()[subject] == capital, "setSettlementAllegiance takes immediately");
|
||||||
|
p.setAllegianceLock(subject, true);
|
||||||
|
// Force a revolt roll to certainly fire, then confirm the lock holds it in place.
|
||||||
|
p.cfg.warRevoltRate = 5.0;
|
||||||
|
for (long yr = 0; yr < 10; ++yr) { p.computeTerritory(); p.computeCultures(); p.stepConflict(yr); }
|
||||||
|
check(p.settleAllegiance()[subject] == capital, "locked allegiance resists stepConflict() revolts");
|
||||||
|
p.setAllegianceLock(subject, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p.cultureList().empty()) {
|
||||||
|
int wantCult = ((size_t)p.settleCulture()[k] + 1 < p.cultureList().size()) ? p.settleCulture()[k] + 1 : 0;
|
||||||
|
p.setSettlementCulture(k, wantCult);
|
||||||
|
check(p.settleCulture()[k] == wantCult, "setSettlementCulture takes immediately");
|
||||||
|
p.setCultureLock(k, true);
|
||||||
|
p.cfg.cultAssimRate = 1.0; p.cfg.cultConvertRate = 1.0;
|
||||||
|
for (long yr = 0; yr < 5; ++yr) p.stepCulture(yr);
|
||||||
|
check(p.settleCulture()[k] == wantCult, "locked culture resists stepCulture() assimilation/conversion");
|
||||||
|
p.setCultureLock(k, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: save v24 round-trip\n");
|
||||||
|
{
|
||||||
|
p.setElevationLock(i0, true); // already at 5555 from earlier; keep it locked for the round-trip
|
||||||
|
p.setElevation(i0, 6001.0);
|
||||||
|
p.setClimateLock(i0, true);
|
||||||
|
p.setTemperature(i0, -12.5);
|
||||||
|
p.setMoisture(i0, 0.6);
|
||||||
|
p.setBiotaDensityLock(i0, true);
|
||||||
|
p.setFloraDensity(i0, 0.33);
|
||||||
|
p.setHabitabilityLock(i0, true);
|
||||||
|
p.setHabitability(i0, 0.5);
|
||||||
|
uint16_t lockBefore = p.editLockAt(i0);
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
bool ok = r.readState(ss);
|
||||||
|
check(ok, "readState accepts the v24 stream");
|
||||||
|
check(r.editLockAt(i0) == lockBefore, "editLock bits survive save/load");
|
||||||
|
check(std::fabs(r.cells[i0].elevation - 6001.0) < 1e-6, "locked elevation value survives save/load");
|
||||||
|
r.computeClimate();
|
||||||
|
check(std::fabs(r.temperature()[i0] - (-12.5)) < 1e-6, "locked temperature value survives save/load");
|
||||||
|
check(std::fabs(r.moisture()[i0] - 0.6) < 1e-6, "locked moisture value survives save/load");
|
||||||
|
r.computeBiotaDensity();
|
||||||
|
check(std::fabs(r.floraDensity()[i0] - 0.33) < 1e-6, "locked flora density survives save/load");
|
||||||
|
r.computeHabitability();
|
||||||
|
check(std::fabs(r.habitability()[i0] - 0.5) < 1e-6, "locked habitability survives save/load");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: corrupt stream rejection (bad lock bits)\n");
|
||||||
|
{
|
||||||
|
std::stringstream good(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(good);
|
||||||
|
std::string bytes = good.str();
|
||||||
|
// The per-cell block starts right after config+rngState+driftIter+erodeIter+targetLand+cellCount;
|
||||||
|
// rather than hand-computing that offset, corrupt via the API and re-serialize instead.
|
||||||
|
Planet bad = p;
|
||||||
|
bad.cells[i0].editLock = 0xFFFF; // bits above the known EditLock range
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
bad.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
check(!r.readState(ss), "readState rejects an out-of-range editLock byte");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Edit mode: pre-v24 compatibility\n");
|
||||||
|
{
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
// The edit-mode block is last; reading with hasEditState=false ignores the trailing bytes
|
||||||
|
// (and the per-cell editLock bytes, so this also mimics a pre-v24 *file* via a v24 writer's
|
||||||
|
// prefix -- the real pre-v24 case is simply "no such bytes were ever written").
|
||||||
|
bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true,
|
||||||
|
true, true, true, false);
|
||||||
|
check(ok, "readState accepts the stream as pre-v24");
|
||||||
|
bool allZero = true;
|
||||||
|
for (const Cell& c : r.cells) if (c.editLock != 0) allZero = false;
|
||||||
|
check(allZero, "pre-v24 load has no locks (editLock all zero)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL EDIT-MODE CHECKS PASSED\n", failures);
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user