Add a clickable toolbar menu (raygui) alongside the existing keybindings

The app was 100% keyboard-driven (~40 single-key shortcuts dumped as a
dense text list in the HUD) -- this adds a discoverable, mouse-friendly
sidebar menu overlaying the top-right of the 3D globe, built with
raygui (raylib's official header-only widget library, fetched via
CMake FetchContent). Every existing keyboard shortcut keeps working
unchanged: 16 key-handler bodies were extracted from ViewerInput.cpp
into named Viewer methods so both the key and the matching toolbar
widget call the exact same code, and can never diverge.

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
This commit is contained in:
Jonas Reith 2026-08-30 17:17:40 +02:00
parent 8fec3d82ad
commit 56d74aba7b
10 changed files with 559 additions and 144 deletions

View File

@ -73,6 +73,8 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
F5 / F9 save / load full state (planet.save) F5 / F9 save / load full state (planet.save)
F12 screenshot to screenshot.png F12 screenshot to screenshot.png
F2 reload planet.cfg (validated) and regenerate F2 reload planet.cfg (validated) and regenerate
F1 toggle the clickable toolbar menu (top-right of the 3D globe; a button/
checkbox/dropdown for every key above, built with raygui)
CLI flags (applied before the first load/generate): CLI flags (applied before the first load/generate):

View File

@ -310,6 +310,31 @@ on the Live World clock). **Steps 17 of the roadmap are done (plus a derived
per-cell `editLock` bitmask + the sparse locked-value maps (climate/biota-density/habitability); older 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 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. add/remove, save v24 round-trip, corrupt-stream rejection, pre-v24 compatibility.
- **Toolbar (clickable menu)** *(done — see `src/render/Toolbar.{hpp,cpp}`)* — the app was previously
100% keyboard-driven (~40 single-key shortcuts dumped as a dense text list in the HUD); this adds a
discoverable, mouse-friendly **sidebar menu** overlaying the top-right of the 3D globe, built with
**raygui** (raylib's official header-only widget library, fetched via CMake `FetchContent`
`DOWNLOAD_ONLY` like raylib itself — `src/render/RayGuiImpl.cpp` holds the one
`RAYGUI_IMPLEMENTATION` translation unit, re-skinned dark via `setupToolbarStyle()` to match the
app's palette). Every existing keyboard shortcut still works **unchanged** — the toolbar is purely
additive, and its widgets call the exact same `Viewer::` methods the keys do (16 key-handler bodies
`P`/`X`/`Z`/`U`/`I`/`E`/`L`/`H`/`W`/`Y`/`M`/`Shift+M`/`R`/`S`/`F`/`F3` — were extracted from
`ViewerInput.cpp` into named methods in `Viewer.cpp` for exactly this sharing, guard included, so
keyboard and toolbar can never diverge in behaviour). Sections: a **View Mode** dropdown (the 13
plain colour modes, replacing keys `1`-`0` and expanding the old `6`-key cycle into 4 explicit
entries), **Overlays** (checkboxes for `B`/`D`/`G`/`J`/`N`/`T`/`O`/`K`/`V`/`M`), **World/Time**
(play/pause, step, fast-forward, a log-scaled speed slider over `driftRate`/`liveRate`, hydrology,
reseed, save/load), **Civilization & Ecology** (`E`/`I`/`L`/`U`/`P`/`X`/`Z`, grey until `settled`),
**Live World** (`W`/`Y`/`.`/`,`) and **Edit Mode** (`F3`). Key **`F1`** collapses/expands the sidebar
(default expanded, for first-run discoverability); a `bool inToolbar` gate in `handleInput()` (mirrors
the existing `inPanel`/`inLiveInfo` pattern) keeps interacting with it from also orbiting the camera
or picking a globe tile underneath, and the whole panel is `GuiDisable()`d while the Phase-3 modal
prompt is up (same exclusivity the rest of the input already has). One real bug found + fixed while
building this: `GuiCheckBox` treats its `bounds` as the checkbox glyph itself (raygui auto-positions
the label outside it using the label's own width) rather than as a whole clickable row — a naive
full-row-width checkbox call just drew an oversized glyph with the label clipped off-panel; fixed by
a small `guiCheckRow()` helper (`Toolbar.cpp`) that passes a compact glyph rect and draws the label
itself at a known position. Purely `src/render` — no save-format or `src/sim` change.
## Current state ## Current state
@ -731,6 +756,8 @@ src/
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, edit-mode panel Panels.* right-column UI: detail panel, hover info, world stats, edit-mode panel
Toolbar.* clickable raygui sidebar menu (mouse-friendly counterpart to the key list)
RayGuiImpl.cpp the one RAYGUI_IMPLEMENTATION translation unit (raygui.h is declaration-only elsewhere)
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
@ -774,7 +801,9 @@ cmake --build build -j
Target OS is Nobara Linux (KDE/Wayland, Intel Arc A770). Dependency install Target OS is Nobara Linux (KDE/Wayland, Intel Arc A770). Dependency install
line is in BUILD.md (`dnf install cmake gcc-c++ mesa-libGL-devel ...`). line is in BUILD.md (`dnf install cmake gcc-c++ mesa-libGL-devel ...`).
raylib 5.5 is fetched automatically — do not vendor it. raylib 5.5 is fetched automatically — do not vendor it. raygui 5.0 (the toolbar's widget
library) is likewise fetched via `FetchContent` (`DOWNLOAD_ONLY` — it's a single header,
no build step of its own).
### Quick headless logic test (no display needed) ### Quick headless logic test (no display needed)
@ -845,6 +874,8 @@ on a settled world; re-press regenerates) · `W` enter/leave **Live World** (set
`R` reseed · `R` reseed ·
`F3` **edit mode** (a settled world; click a tile to edit every property — geology/climate/biota/ `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) · settlement — with per-field lock against automatic recompute; see below) ·
`F1` toggle the clickable **toolbar menu** (top-right of the 3D globe; every key above also has a
button/checkbox/dropdown there — 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.
@ -887,6 +918,15 @@ automatic recompute until unlocked again (elevation resists erosion/relaxation,
density/habitability stay pinned, a settlement's population/allegiance/culture stop changing on density/habitability stay pinned, a settlement's population/allegiance/culture stop changing on
their own). Saved (v24). their own). Saved (v24).
Toolbar (`F1`, default expanded): a clickable raygui sidebar over the top-right of the 3D globe with
a button/checkbox/dropdown for essentially every key above, grouped into View Mode / Overlays /
World & Time / Civilization & Ecology / Live World / Edit Mode. Purely additive — every keyboard
shortcut keeps working exactly as documented above; the sidebar is just a more discoverable way to
reach the same actions. Grey/disabled rows mean the action needs a settled world (or Live World);
the panel scrolls (mouse wheel) past the World/Time controls to reach Civilization/Live World/Edit
Mode. Locked out (`GuiDisable`d) while the Phase-3 "start hydrology?" prompt is up, same as
everything else.
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).

View File

@ -15,6 +15,18 @@ set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_GAMES OFF CACHE BOOL "" FORCE) set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(raylib) FetchContent_MakeAvailable(raylib)
# raygui: raylib's official header-only companion widget library (buttons, checkboxes,
# dropdowns, sliders, scroll panels -- used by the Toolbar sidebar, src/render/Toolbar.cpp).
# Download-only: it's a single header (src/raygui.h), no build step of its own needed.
FetchContent_Declare(
raygui
GIT_REPOSITORY https://github.com/raysan5/raygui.git
GIT_TAG 5.0
GIT_SHALLOW TRUE
DOWNLOAD_ONLY TRUE
)
FetchContent_MakeAvailable(raygui)
set(SIM_SOURCES set(SIM_SOURCES
src/sim/IcoSphere.cpp src/sim/IcoSphere.cpp
src/sim/Planet.cpp src/sim/Planet.cpp
@ -50,6 +62,8 @@ set(RENDER_SOURCES
src/render/Overlays.cpp src/render/Overlays.cpp
src/render/Picking.cpp src/render/Picking.cpp
src/render/Panels.cpp src/render/Panels.cpp
src/render/Toolbar.cpp
src/render/RayGuiImpl.cpp
src/render/Viewer.cpp src/render/Viewer.cpp
src/render/ViewerInput.cpp src/render/ViewerInput.cpp
src/render/ViewerRender.cpp src/render/ViewerRender.cpp
@ -63,7 +77,7 @@ add_executable(planetsim
${RENDER_SOURCES} ${RENDER_SOURCES}
) )
# Flat includes ("Planet.hpp", "Viewer.hpp", ...) resolve across both folders. # Flat includes ("Planet.hpp", "Viewer.hpp", ...) resolve across both folders.
target_include_directories(planetsim PRIVATE src/sim src/render) target_include_directories(planetsim PRIVATE src/sim src/render ${raygui_SOURCE_DIR}/src)
target_link_libraries(planetsim PRIVATE planetsim_sim raylib) target_link_libraries(planetsim PRIVATE planetsim_sim raylib)
# OpenMP parallelizes the per-cell passes in Planet::step(). Optional: without # OpenMP parallelizes the per-cell passes in Planet::step(). Optional: without
@ -86,7 +100,7 @@ foreach(test_name logic biota ocean live weather volcano geography ecoregions ci
endforeach() endforeach()
add_executable(test_events test_events.cpp ${RENDER_SOURCES}) add_executable(test_events test_events.cpp ${RENDER_SOURCES})
target_include_directories(test_events PRIVATE src/sim src/render) target_include_directories(test_events PRIVATE src/sim src/render ${raygui_SOURCE_DIR}/src)
target_link_libraries(test_events PRIVATE planetsim_sim raylib) target_link_libraries(test_events PRIVATE planetsim_sim raylib)
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
target_link_libraries(test_events PRIVATE m pthread dl) target_link_libraries(test_events PRIVATE m pthread dl)

View File

@ -0,0 +1,7 @@
// raygui's single-header implementation, isolated in its own translation unit so
// RAYGUI_IMPLEMENTATION is defined exactly once (a second definition anywhere else
// in the program would be a duplicate-symbol link error). Every other file that
// needs Gui* calls just #include "raygui.h" without the macro (declarations only).
#include "raylib.h" // must come first: raygui expects Rectangle/Color/Vector2 already declared
#define RAYGUI_IMPLEMENTATION
#include "raygui.h"

251
src/render/Toolbar.cpp Normal file
View File

@ -0,0 +1,251 @@
#include "Toolbar.hpp"
#include "Viewer.hpp"
#include "raygui.h"
#include <algorithm>
#include <cmath>
// A clickable raygui sidebar overlaying the top-right of the 3D globe (see Toolbar.hpp). Every
// widget here is either DIRECT-BIND (a plain bool with no side effect -- GuiCheckBox writes
// straight into the Viewer field, exactly what the matching key's bare flip does) or
// SHADOW+METHOD (a throw-away local bool seeded from the real state each frame; if raygui flips
// it in response to a click, the paired Viewer:: method -- the single source of truth, same one
// the keyboard shortcut calls -- runs instead of trusting the shadow's own value). The
// before/after compare is used rather than the widget's own return value so this doesn't depend
// on remembering the exact "pressed vs. held" return-code convention for every control type.
namespace {
const float PAD = 8.0f, ROWH = 24.0f, GAP = 4.0f, LABEL_H = 16.0f, SECTION_GAP = 10.0f;
void sectionLabel(float x, float& y, const char* text) {
DrawText(text, (int)x, (int)y, 14, Color{170, 175, 190, 255});
y += LABEL_H;
}
Rectangle takeRow(float x, float w, float& y, float h = ROWH) {
Rectangle r{ x, y, w, h };
y += h + GAP;
return r;
}
void toggleAction(Rectangle r, const char* label, bool current, void (Viewer::*action)(), Viewer& v) {
bool shadow = current;
GuiToggle(r, label, &shadow);
if (shadow != current) (v.*action)();
}
// GuiCheckBox treats `bounds` as the glyph square itself and auto-positions its label OUTSIDE
// that rect using the label's own natural width -- it doesn't compose with a fixed-width
// column layout (a wide `bounds` just draws an oversized square, and the label runs off past
// it). So: pass it a small square glyph rect and draw our own label at a known position
// instead. Returns whether *flag changed (a click), for callers using the shadow+method
// pattern; direct-bind callers can just ignore the return value.
bool guiCheckRow(Rectangle row, const char* label, bool* flag) {
Rectangle box{ row.x, row.y + (row.height - 18.0f) * 0.5f, 18.0f, 18.0f };
bool before = *flag;
GuiCheckBox(box, "", flag);
DrawText(label, (int)(row.x + 26.0f), (int)(row.y + (row.height - 13.0f) * 0.5f), 13, Color{200, 205, 215, 255});
return *flag != before;
}
// View-mode dropdown: the 13 "plain" colour modes (Ecoregion/Habitability/Territory/Culture/
// Wealth are separate side-effecting actions in the Civilization & Ecology section instead --
// same split the original keys 1-0 vs E/I/P/X/Z already had).
const ColorMode kViewModes[] = {
ColorMode::Elevation, ColorMode::Plate, ColorMode::Age, ColorMode::Crust, ColorMode::Biome,
ColorMode::Temperature, ColorMode::TempSummer, ColorMode::TempWinter, ColorMode::Seasonality,
ColorMode::Precip, ColorMode::FloraDensity, ColorMode::FaunaDensity, ColorMode::FungaDensity,
};
const int kViewModeCount = 13;
const char* kViewModeList =
"Elevation;Plates;Crust age;Crust type;Biome;Temperature;Temperature (summer);"
"Temperature (winter);Seasonality (summer-winter);Precipitation;Flora density;"
"Fauna density;Funga density";
// Log-scaled slider mapping so one slider usefully covers driftRate's 0.5..80 range and
// liveRate's 0.25..liveRateMax() range (up to ~175000, a full My/live-clock speed span).
float toSliderT(double val, double lo, double hi) {
double lv = std::log(std::clamp(val, lo, hi)), llo = std::log(lo), lhi = std::log(hi);
return (float)std::clamp((lv - llo) / (lhi - llo), 0.0, 1.0);
}
double fromSliderT(float t, double lo, double hi) {
double llo = std::log(lo), lhi = std::log(hi);
return std::exp(llo + (double)std::clamp(t, 0.0f, 1.0f) * (lhi - llo));
}
}
void setupToolbarStyle() {
GuiSetFont(GetFontDefault());
GuiSetStyle(DEFAULT, TEXT_SIZE, 14);
GuiSetStyle(DEFAULT, BACKGROUND_COLOR, ColorToInt(Color{12, 14, 22, 235}));
GuiSetStyle(DEFAULT, LINE_COLOR, ColorToInt(Color{90, 90, 110, 255}));
GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, ColorToInt(Color{90, 90, 110, 255}));
GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, ColorToInt(Color{28, 30, 42, 255}));
GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, ColorToInt(Color{210, 210, 220, 255}));
GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, ColorToInt(Color{235, 225, 140, 255}));
GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, ColorToInt(Color{60, 66, 92, 255}));
GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, ColorToInt(RAYWHITE));
GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, ColorToInt(Color{255, 200, 120, 255}));
GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, ColorToInt(Color{80, 88, 120, 255}));
GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, ColorToInt(RAYWHITE));
GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, ColorToInt(Color{50, 52, 60, 255}));
GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, ColorToInt(Color{20, 22, 30, 255}));
GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, ColorToInt(Color{95, 98, 110, 255}));
}
void drawToolbar(Viewer& v) {
// Always-visible collapse/expand tab, independent of showToolbar.
bool wasOpen = v.showToolbar;
bool openShadow = wasOpen;
GuiToggle(v.toolbarToggleBtn, wasOpen ? "Menu x" : "Menu", &openShadow);
if (openShadow != wasOpen) v.showToolbar = openShadow;
if (!v.showToolbar) return;
bool wasModal = v.phase3Prompt;
if (wasModal) GuiDisable();
Rectangle panel = v.toolbarRect;
DrawRectangleRec(panel, Color{12, 14, 22, 235});
DrawRectangleLinesEx(panel, 1, Color{90, 90, 110, 255});
float x = panel.x + PAD, w = panel.width - 2 * PAD;
float y = panel.y + PAD;
Rectangle dropRect = takeRow(x, w, y, 24.0f); // reserved now, drawn LAST so its open list is on top
y += SECTION_GAP * 0.5f;
Rectangle scrollBounds{ panel.x, y, panel.width, (panel.y + panel.height) - y };
Rectangle content{ 0, 0, panel.width - 14.0f, 900.0f };
Rectangle view{};
bool dropdownOpen = v.toolbarModeEditMode;
if (dropdownOpen) GuiLock();
GuiScrollPanel(scrollBounds, nullptr, content, &v.toolbarScroll, &view);
BeginScissorMode((int)view.x, (int)view.y, (int)view.width, (int)view.height);
{
float cx = scrollBounds.x + PAD + v.toolbarScroll.x;
float cw = content.width - 2 * PAD;
float cy = scrollBounds.y + PAD + v.toolbarScroll.y;
// --- Overlays ---------------------------------------------------------
sectionLabel(cx, cy, "Overlays");
struct Ov { const char* label; bool* flag; };
Ov overlays[] = {
{"Borders (B)", &v.showBorders}, {"Drift (D)", &v.showDrift},
{"Grid (G)", &v.showGrat}, {"Rivers (J)", &v.showRivers},
{"Day/night (N)", &v.dayNightOn}, {"Tides (T)", &v.showTides},
{"Currents (O)", &v.showCurrents}, {"Clouds (K)", &v.showClouds},
{"Volcanoes (V)", &v.showVolcanoes},
};
float halfW = (cw - GAP) * 0.5f;
for (int i = 0; i < 9; i += 2) {
guiCheckRow(Rectangle{ cx, cy, halfW, ROWH }, overlays[i].label, overlays[i].flag);
if (i + 1 < 9) guiCheckRow(Rectangle{ cx + halfW + GAP, cy, halfW, ROWH }, overlays[i + 1].label, overlays[i + 1].flag);
cy += ROWH + GAP;
}
{
bool namesShadow = v.showNames;
if (guiCheckRow(Rectangle{ cx, cy, halfW, ROWH }, "Names (M)", &namesShadow)) v.toggleNames();
if (GuiButton(Rectangle{ cx + halfW + GAP, cy, halfW, ROWH }, "Reshuffle")) v.reshuffleNames();
cy += ROWH + GAP;
}
cy += SECTION_GAP;
// --- World / Time -------------------------------------------------------
sectionLabel(cx, cy, "World / Time");
{
bool pauseShadow = v.paused;
GuiToggle(takeRow(cx, cw, cy), v.paused ? "Resume" : "Pause", &pauseShadow);
if (pauseShadow != v.paused) v.pauseAction();
}
if (GuiButton(takeRow(cx, cw, cy), "Step (S)")) v.stepAction();
{
bool settledNow = v.settled;
if (settledNow) GuiDisable();
if (GuiButton(takeRow(cx, cw, cy), "Fast-forward (F)")) v.fastForward();
if (settledNow) GuiEnable();
}
{
bool isLive = v.liveWorld;
double lo = isLive ? 0.25 : 0.5;
double hi = isLive ? v.liveRateMax() : 80.0;
double cur = isLive ? v.liveRate : v.driftRate;
DrawText(TextFormat(isLive ? "Speed: %.1f h/s (live clock)" : "Speed: %.1f My/s", cur),
(int)cx, (int)cy, 13, Color{170, 175, 190, 255});
cy += LABEL_H;
float t = toSliderT(cur, lo, hi);
Rectangle r = takeRow(cx, cw, cy);
if (GuiSlider(r, "", "", &t, 0.0f, 1.0f)) {
double nv = fromSliderT(t, lo, hi);
if (isLive) v.liveRate = nv; else v.driftRate = nv;
}
}
{
bool hydroShadow = v.phase3;
if (guiCheckRow(takeRow(cx, cw, cy), "Hydrology (H)", &hydroShadow)) v.toggleHydrology();
}
if (GuiButton(takeRow(cx, cw, cy), "Reseed (R)")) v.reseed();
{
Rectangle r = takeRow(cx, cw, cy);
if (GuiButton(Rectangle{ r.x, r.y, halfW, r.height }, "Save (F5)")) v.saveGame(v.SAVE_PATH);
if (GuiButton(Rectangle{ r.x + halfW + GAP, r.y, halfW, r.height }, "Load (F9)")) v.loadGame(v.SAVE_PATH);
}
cy += SECTION_GAP;
// --- Civilization & Ecology (all need a settled world) ------------------
sectionLabel(cx, cy, "Civilization & Ecology");
{
bool settledNow = v.settled;
if (!settledNow) GuiDisable();
toggleAction(takeRow(cx, cw, cy), "Ecoregions (E)", v.mode == ColorMode::Ecoregion, &Viewer::toggleEcoregionView, v);
toggleAction(takeRow(cx, cw, cy), "Habitability (I)", v.mode == ColorMode::Habitability, &Viewer::toggleHabitabilityView, v);
if (GuiButton(takeRow(cx, cw, cy), "Generate biota (L)")) v.generateOrRegenerateBiota();
toggleAction(takeRow(cx, cw, cy), v.planet.settlementsPlaced() ? "Settlements (U)" : "Found settlements (U)",
v.showSettlements && v.planet.settlementsPlaced(), &Viewer::placeOrToggleSettlements, v);
toggleAction(takeRow(cx, cw, cy), "Territory (P)", v.mode == ColorMode::Territory, &Viewer::toggleTerritoryView, v);
toggleAction(takeRow(cx, cw, cy), "Culture (X)", v.mode == ColorMode::Culture, &Viewer::toggleCultureView, v);
toggleAction(takeRow(cx, cw, cy), "Trade / wealth (Z)", v.mode == ColorMode::Wealth, &Viewer::toggleTradeView, v);
if (!settledNow) GuiEnable();
}
cy += SECTION_GAP;
// --- Live World -----------------------------------------------------------
sectionLabel(cx, cy, "Live World");
{
bool settledNow = v.settled;
if (!settledNow) GuiDisable();
toggleAction(takeRow(cx, cw, cy), v.liveWorld ? "Exit Live World" : "Enter Live World (W)", v.liveWorld, &Viewer::enterOrLeaveLiveWorld, v);
if (!settledNow) GuiEnable();
bool liveNow = v.liveWorld;
if (!liveNow) GuiDisable();
if (GuiButton(takeRow(cx, cw, cy), "Follow storm (Y)")) v.cycleFollowStorm();
{
Rectangle r = takeRow(cx, cw, cy);
if (GuiButton(Rectangle{ r.x, r.y, halfW, r.height }, "Step + (.)")) { v.liveStepForward(); v.setStatus("Step forward"); }
if (GuiButton(Rectangle{ r.x + halfW + GAP, r.y, halfW, r.height }, "Step - (,)")) v.liveStepBack();
}
if (!liveNow) GuiEnable();
}
cy += SECTION_GAP;
// --- Edit Mode --------------------------------------------------------
sectionLabel(cx, cy, "Edit Mode");
{
bool settledNow = v.settled;
if (!settledNow) GuiDisable();
toggleAction(takeRow(cx, cw, cy), v.editMode ? "Exit Edit Mode" : "Edit Mode (F3)", v.editMode, &Viewer::toggleEditMode, v);
if (!settledNow) GuiEnable();
}
}
EndScissorMode();
if (dropdownOpen) GuiUnlock();
// View-mode dropdown, drawn LAST so its open list renders on top of the scroll body below it
// (see Toolbar.hpp / the gotcha this avoids: raygui doesn't auto-exclude overlapping controls).
if (!v.toolbarModeEditMode) {
for (int i = 0; i < kViewModeCount; ++i) if (kViewModes[i] == v.mode) { v.toolbarModeActive = i; break; }
}
if (GuiDropdownBox(dropRect, kViewModeList, &v.toolbarModeActive, v.toolbarModeEditMode))
v.toolbarModeEditMode = !v.toolbarModeEditMode;
if (!v.toolbarModeEditMode) {
ColorMode picked = kViewModes[std::clamp(v.toolbarModeActive, 0, kViewModeCount - 1)];
if (picked != v.mode) { v.mode = picked; v.recolor(); }
}
if (wasModal) GuiEnable();
}

20
src/render/Toolbar.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
struct Viewer;
// A clickable, discoverable raygui sidebar overlaying the top-right of the 3D globe
// viewport -- the mouse-friendly counterpart to the ~40 keyboard shortcuts in
// ViewerInput.cpp. Every shortcut keeps working unchanged; each toolbar widget calls
// the exact same Viewer method its matching key does (see Viewer.hpp's "Actions shared
// by keyboard shortcuts + the toolbar" section), so the two paths can never diverge.
//
// Unlike Panels.cpp's read-only draw*() functions, this both reads AND WRITES Viewer
// state: a raygui widget call is simultaneously "draw this frame" and "was this clicked
// this frame" -- there is no separate input-handling pass for it (input gating against
// the rest of the app -- not orbiting the camera while dragging inside the sidebar --
// still happens in ViewerInput.cpp's `inToolbar` check).
void drawToolbar(Viewer& v);
// Re-skins raygui's (light, Windows95-grey) default theme to match this app's dark palette.
// Call once, after InitWindow(), before the first drawToolbar().
void setupToolbarStyle();

View File

@ -1,6 +1,7 @@
#include "Viewer.hpp" #include "Viewer.hpp"
#include "Picking.hpp" // angBetween (rebuildSub) #include "Picking.hpp" // angBetween (rebuildSub)
#include "Projection.hpp" // EqualEarth (layout) #include "Projection.hpp" // EqualEarth (layout)
#include "Toolbar.hpp" // setupToolbarStyle
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
@ -117,6 +118,12 @@ bool Viewer::init(int argc, char** argv) {
p3ContinueBtn = Rectangle{ pbCx - pbW - pbGap * 0.5f, pbCy + 8.0f, pbW, pbH }; p3ContinueBtn = Rectangle{ pbCx - pbW - pbGap * 0.5f, pbCy + 8.0f, pbW, pbH };
p3StartBtn = Rectangle{ pbCx + pbGap * 0.5f, pbCy + 8.0f, pbW, pbH }; p3StartBtn = Rectangle{ pbCx + pbGap * 0.5f, pbCy + 8.0f, pbW, pbH };
// Toolbar (raygui sidebar): right edge of the 3D viewport, doesn't overlap the top-center
// view-mode label, the phase-3 modal, or the bottom-left pause button.
toolbarRect = Rectangle{ (float)view3DW - 300.0f, 0.0f, 300.0f, (float)view3DH };
toolbarToggleBtn = Rectangle{ (float)view3DW - 92.0f, 4.0f, 84.0f, 26.0f };
setupToolbarStyle();
cfg.subdivisions = 5; cfg.subdivisions = 5;
if (!loadConfig(configPath, cfg)) saveConfig(configPath, cfg); // load, or create a default if (!loadConfig(configPath, cfg)) saveConfig(configPath, cfg); // load, or create a default
if (cliSeed != 0) cfg.seed = cliSeed; // CLI --seed overrides config if (cliSeed != 0) cfg.seed = cliSeed; // CLI --seed overrides config
@ -897,6 +904,157 @@ void Viewer::rebuildTerritory() {
lastTerritoryYear = (long)std::floor(liveTime / yearHours); lastTerritoryYear = (long)std::floor(liveTime / yearHours);
} }
// ---- Actions shared by keyboard shortcuts (ViewerInput.cpp) and the toolbar (Toolbar.cpp) -----
// Each extracted verbatim from its key handler's body (guard moved inside), so a keypress and the
// matching toolbar widget can never diverge in behaviour -- see Viewer.hpp for the full list.
void Viewer::toggleTerritoryView() { // P: territory/realms colour view + political borders
if (!settled) return;
if (!planet.settlementsPlaced()) { setStatus("Press U for the dawn of civilization first"); return; }
if (!planet.nationsBuilt() || (int)planet.cellNation().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Territory) ? ColorMode::Biome : ColorMode::Territory;
showNationBorders = (mode == ColorMode::Territory);
recolor();
setStatus(mode == ColorMode::Territory ? "Territory / realms on" : "Territory off");
}
void Viewer::toggleCultureView() { // X: culture/faiths colour view + cultural borders
if (!settled) return;
if (!planet.settlementsPlaced()) { setStatus("Press U for the dawn of civilization first"); return; }
if (!planet.culturesBuilt() || (int)planet.cellCulture().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Culture) ? ColorMode::Biome : ColorMode::Culture;
showCultureBorders = (mode == ColorMode::Culture);
recolor();
setStatus(mode == ColorMode::Culture ? "Cultures / faiths on" : "Cultures off");
}
void Viewer::toggleTradeView() { // Z: wealth/trade colour view + trade routes
if (!settled) return;
if (!planet.settlementsPlaced()) { setStatus("Press U for the dawn of civilization first"); return; }
if (!planet.tradeBuilt() || (int)planet.cellWealth().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Wealth) ? ColorMode::Biome : ColorMode::Wealth;
showTradeRoutes = (mode == ColorMode::Wealth);
recolor();
setStatus(mode == ColorMode::Wealth ? "Wealth / trade on" : "Wealth off");
}
void Viewer::placeOrToggleSettlements() { // U: seed on first press ("the dawn"), then toggle markers
if (!settled) return;
if (!planet.settlementsPlaced()) {
planet.placeSettlements();
rebuildTerritory(); // initial realms + borders
showSettlements = true;
appendEvent(3, 1, liveTime, planet.settlements.empty() ? 0 : planet.settlements[0].cell, 0,
"Civilization begins",
std::string(TextFormat("%d villages founded", (int)planet.settlements.size())));
setStatus(TextFormat("Civilization begins (%d settlements)", (int)planet.settlements.size()));
} else {
showSettlements = !showSettlements;
setStatus(showSettlements ? "Settlements on" : "Settlements off");
}
}
void Viewer::toggleHabitabilityView() { // I: habitability heat-map view
if (!settled) return;
planet.computeHabitability();
mode = (mode == ColorMode::Habitability) ? ColorMode::Biome : ColorMode::Habitability;
recolor();
setStatus(mode == ColorMode::Habitability ? "Habitability on" : "Habitability off");
}
void Viewer::toggleEcoregionView() { // E: ecoregion atlas colour view (names ecology on first use)
if (!settled) return;
if (!planet.ecoregionsBuilt()) planet.generateEcoregions();
mode = (mode == ColorMode::Ecoregion) ? ColorMode::Biome : ColorMode::Ecoregion;
recolor();
setStatus(mode == ColorMode::Ecoregion ? "Ecoregions on" : "Ecoregions off");
}
void Viewer::generateOrRegenerateBiota() { // L: generate / regenerate the biota population
if (!settled) return;
planet.generateBiota();
if (planet.ecoregionsBuilt()) planet.generateEcoregions();
if (mode != ColorMode::FaunaDensity && mode != ColorMode::FungaDensity)
{ mode = ColorMode::FloraDensity; recolor(); }
setStatus("Biota generated (flora/fauna/funga)");
}
void Viewer::toggleHydrology() { // H: toggle Phase 3 (hydrology)
if (!settled) return;
phase3 = !phase3; phase3Prompt = false;
paused = true; // switching stage auto-pauses (SPACE to run)
if (phase3) { phase3PromptAt = elapsedMy; setStatus("Hydrology ON - paused (SPACE to run)"); }
else { phase3PromptAt = elapsedMy + planet.cfg.phase3AfterMy; rivers.clear(); bigRivers.clear(); setStatus("Hydrology OFF - paused"); }
refreshView();
}
void Viewer::enterOrLeaveLiveWorld() { // W: enter / leave Live World (slow real-time clock)
if (!settled) return;
liveWorld = !liveWorld;
if (liveWorld) {
phase3Prompt = false; paused = true; wxUndo.clear(); // enter Live World paused (SPACE to run)
if (planet.volcanoes.empty()) planet.placeVolcanoes(liveTime); // one-time tectonic-context placement
if (!planet.geographyBuilt()) planet.generateGeography(); // name the world's geography (the atlas)
refreshView(); // fresh base colours; overlay builds in stepSim
setStatus("Live World started - paused (SPACE to run)");
} else {
paused = true; followId = 0; wxUndo.clear(); refreshView(); // back to World Creation, paused
setStatus("Live World stopped");
}
}
void Viewer::cycleFollowStorm() { // Y: cycle the 3D camera through active storms
if (!liveWorld) return;
const auto& st = planet.storms();
if (st.empty()) { followId = 0; setStatus("No weather systems to follow"); return; }
std::vector<int> idx(st.size()); for (size_t i = 0; i < st.size(); ++i) idx[i] = (int)i;
std::sort(idx.begin(), idx.end(), [&](int a, int b){ return st[a].strength > st[b].strength; });
int cur = -1; for (size_t k = 0; k < idx.size(); ++k) if (st[idx[k]].id == followId) { cur = (int)k; break; }
int next = (cur < 0) ? 0 : cur + 1;
if (next >= (int)idx.size()) { followId = 0; setStatus("Follow cam off"); return; }
const WeatherSystem& ws = st[idx[next]];
followId = ws.id;
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
setStatus(hur ? "Following tropical cyclone" : "Following weather system");
}
void Viewer::toggleNames() { // M: toggle place-name labels (the atlas)
if (!settled) return;
if (!planet.geographyBuilt()) planet.generateGeography(); // lazily name the world on first use
showNames = !showNames;
setStatus(showNames ? "Place names on" : "Place names off");
}
void Viewer::reshuffleNames() { // Shift+M: re-extract current geography + use different names
if (!settled) return;
planet.reshuffleGeography();
showNames = true;
setStatus("Place names reshuffled");
}
void Viewer::reseed() { // R: reseed
cfg.seed = (uint32_t)(GetTime() * 100000) | 1;
regen();
}
void Viewer::stepAction() { // S: one step (forming/drift tick, or Live World clock step)
if (liveWorld) liveStepForward();
else { stepOnce(); refreshView(); }
}
void Viewer::fastForward() { // F: fast-forward Phase-1 forming to settled
if (settled) return;
while (!settled) stepOnce();
dtMy = planet.cflDtMy(); planet.drifting = true; refreshView();
}
void Viewer::toggleEditMode() { // F3: toggle edit mode (manual cell/settlement edits)
if (!settled) return;
editMode = !editMode;
if (editMode) { paused = true; setStatus("Edit mode on - click a tile to edit (F3 to exit)"); }
else { exitEditMode(); setStatus("Edit mode off"); }
}
// Push the current (pre-advance) weather state onto the bounded step-back ring. // Push the current (pre-advance) weather state onto the bounded step-back ring.
void Viewer::wxPushSnapshot() { void Viewer::wxPushSnapshot() {
if ((int)wxUndo.size() >= wxUndoMax) wxUndo.erase(wxUndo.begin()); if ((int)wxUndo.size() >= wxUndoMax) wxUndo.erase(wxUndo.begin());

View File

@ -143,6 +143,16 @@ struct Viewer {
bool editMode = false; bool editMode = false;
EditPanelState editPanel; EditPanelState editPanel;
// Toolbar (raygui sidebar, key F1, Toolbar.cpp): a clickable, discoverable menu for the
// most-used actions, overlaying the top-right of the 3D globe. Additive -- every keyboard
// shortcut above keeps working unchanged; the toolbar's widgets call the same Viewer methods.
bool showToolbar = true; // expanded by default (first-run discoverability)
Rectangle toolbarRect{}; // set once in init()
Rectangle toolbarToggleBtn{}; // always-visible collapse/expand tab
Vector2 toolbarScroll{0, 0}; // GuiScrollPanel scroll offset
int toolbarModeActive = 0; // GuiDropdownBox: selected view-mode index
bool toolbarModeEditMode = false; // GuiDropdownBox: is the list currently open
// Transient on-screen status line. // Transient on-screen status line.
std::string statusMsg; double statusUntil = 0.0; std::string statusMsg; double statusUntil = 0.0;
@ -198,6 +208,27 @@ struct Viewer {
void detectNationEvents(const std::vector<Nation>& beforeNations); void detectNationEvents(const std::vector<Nation>& beforeNations);
void focusCell(int idx, const std::string& status = ""); void focusCell(int idx, const std::string& status = "");
// ---- Actions shared by keyboard shortcuts + the toolbar (Viewer.cpp) ---
// Each mirrors the body of the matching key handler in ViewerInput.cpp (its own
// guard, e.g. "only on a settled world", moved inside) so a keypress and the
// corresponding toolbar widget can never diverge in behaviour.
void toggleTerritoryView(); // P
void toggleCultureView(); // X
void toggleTradeView(); // Z
void placeOrToggleSettlements(); // U
void toggleHabitabilityView(); // I
void toggleEcoregionView(); // E
void generateOrRegenerateBiota(); // L
void toggleHydrology(); // H
void enterOrLeaveLiveWorld(); // W
void cycleFollowStorm(); // Y
void toggleNames(); // M
void reshuffleNames(); // Shift+M
void reseed(); // R
void stepAction(); // S
void fastForward(); // F
void toggleEditMode(); // F3
// ---- Input (ViewerInput.cpp) -------------------------------------------- // ---- Input (ViewerInput.cpp) --------------------------------------------
void handleInput(); void handleInput();
void handleEditClick(); // dispatch a click inside the edit panel (tabs/fields/locks/picker/remove) void handleEditClick(); // dispatch a click inside the edit panel (tabs/fields/locks/picker/remove)

View File

@ -16,6 +16,11 @@ void Viewer::handleInput() {
bool inPanel = (selectedCell >= 0) && CheckCollisionPointRec(mp, panelRect); bool inPanel = (selectedCell >= 0) && CheckCollisionPointRec(mp, panelRect);
bool inLiveInfo = liveWorld && CheckCollisionPointRec(mp, liveInfoRect); bool inLiveInfo = liveWorld && CheckCollisionPointRec(mp, liveInfoRect);
onPause = CheckCollisionPointRec(mp, pauseBtn); onPause = CheckCollisionPointRec(mp, pauseBtn);
// The raygui toolbar sidebar (Toolbar.cpp) reads real mouse state itself when drawn later this
// frame; gate it out of camera orbit/pick/zoom here so interacting with it doesn't also spin
// the globe or select a tile underneath (mirrors inPanel/inLiveInfo below).
bool inToolbar = CheckCollisionPointRec(mp, toolbarToggleBtn)
|| (showToolbar && CheckCollisionPointRec(mp, toolbarRect));
// --- Camera input (LMB drag orbits; tracks drag distance for clicks) -- // --- Camera input (LMB drag orbits; tracks drag distance for clicks) --
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
@ -66,7 +71,7 @@ void Viewer::handleInput() {
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !phase3Prompt) { if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !phase3Prompt) {
Vector2 d = GetMouseDelta(); Vector2 d = GetMouseDelta();
dragDist += fabsf(d.x) + fabsf(d.y); dragDist += fabsf(d.x) + fabsf(d.y);
if (in3D && !onPause && !following) { // orbit (disabled while following a storm) if (in3D && !onPause && !following && !inToolbar) { // orbit (disabled while following a storm)
camYaw += d.x * 0.005f; camYaw += d.x * 0.005f;
camPitch += d.y * 0.005f; camPitch += d.y * 0.005f;
camPitch = std::clamp(camPitch, -1.5f, 1.5f); camPitch = std::clamp(camPitch, -1.5f, 1.5f);
@ -84,7 +89,9 @@ void Viewer::handleInput() {
// Wheel: zoom the 2D map toward the cursor when hovering it, else zoom the camera. // Wheel: zoom the 2D map toward the cursor when hovering it, else zoom the camera.
float wheel = GetMouseWheelMove(); float wheel = GetMouseWheelMove();
if (inMap && wheel != 0.0f) { if (inToolbar) {
// leave the wheel value alone; the toolbar's GuiScrollPanel reads it later this frame
} else if (inMap && wheel != 0.0f) {
Rectangle vr = mapViewRect(); Rectangle vr = mapViewRect();
double u = (mp.x - vr.x) / vr.width, v = (mp.y - vr.y) / vr.height; // projection coord under cursor double u = (mp.x - vr.x) / vr.width, v = (mp.y - vr.y) / vr.height; // projection coord under cursor
double nz = std::clamp(mapZoom * (wheel > 0 ? 1.2 : 1.0 / 1.2), 1.0, 8.0); double nz = std::clamp(mapZoom * (wheel > 0 ? 1.2 : 1.0 / 1.2), 1.0, 8.0);
@ -125,7 +132,7 @@ void Viewer::handleInput() {
hoveredSubIdx = j * R + i; hoveredSubIdx = j * R + i;
hoverSub = sg->sub[hoveredSubIdx]; hasHoverSub = true; // marks it on the globe hoverSub = sg->sub[hoveredSubIdx]; hasHoverSub = true; // marks it on the globe
} }
} else if (in3D) { } else if (in3D && !inToolbar) {
Vec3 d = rayDirFromMouse(cam.position, cam.target, cam.fovy, Vec3 d = rayDirFromMouse(cam.position, cam.target, cam.fovy,
mp.x, mp.y, (float)view3DW, (float)view3DH); mp.x, mp.y, (float)view3DW, (float)view3DH);
Vec3 o{cam.position.x, cam.position.y, cam.position.z}; Vec3 o{cam.position.x, cam.position.y, cam.position.z};
@ -153,7 +160,7 @@ void Viewer::handleInput() {
} }
// --- Click = select a tile (ignored over panel/button / while dragging) - // --- Click = select a tile (ignored over panel/button / while dragging) -
if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && dragDist < 6.0f && !inPanel && !inLiveInfo && !onPause && !phase3Prompt && hovered >= 0) if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && dragDist < 6.0f && !inPanel && !inLiveInfo && !onPause && !phase3Prompt && !inToolbar && hovered >= 0)
selectCell(hovered); selectCell(hovered);
// --- Keys ------------------------------------------------------------- // --- Keys -------------------------------------------------------------
@ -190,148 +197,39 @@ void Viewer::handleInput() {
if (IsKeyPressed(KEY_D)) showDrift = !showDrift; if (IsKeyPressed(KEY_D)) showDrift = !showDrift;
if (IsKeyPressed(KEY_G)) showGrat = !showGrat; if (IsKeyPressed(KEY_G)) showGrat = !showGrat;
if (IsKeyPressed(KEY_J)) showRivers = !showRivers; if (IsKeyPressed(KEY_J)) showRivers = !showRivers;
if (IsKeyPressed(KEY_H) && settled) { // toggle Phase 3 (hydrology) if (IsKeyPressed(KEY_H)) toggleHydrology();
phase3 = !phase3; phase3Prompt = false; if (IsKeyPressed(KEY_L)) generateOrRegenerateBiota();
paused = true; // switching stage auto-pauses (SPACE to run) if (IsKeyPressed(KEY_E)) toggleEcoregionView();
if (phase3) { phase3PromptAt = elapsedMy; setStatus("Hydrology ON - paused (SPACE to run)"); } if (IsKeyPressed(KEY_I)) toggleHabitabilityView();
else { phase3PromptAt = elapsedMy + planet.cfg.phase3AfterMy; rivers.clear(); bigRivers.clear(); setStatus("Hydrology OFF - paused"); } if (IsKeyPressed(KEY_U)) placeOrToggleSettlements();
refreshView(); if (IsKeyPressed(KEY_P)) toggleTerritoryView();
} if (IsKeyPressed(KEY_X)) toggleCultureView();
if (IsKeyPressed(KEY_L) && settled) { // generate / regenerate biota population if (IsKeyPressed(KEY_Z)) toggleTradeView();
planet.generateBiota(); if (IsKeyPressed(KEY_W)) enterOrLeaveLiveWorld();
if (planet.ecoregionsBuilt()) planet.generateEcoregions(); if (IsKeyPressed(KEY_Y)) cycleFollowStorm();
if (mode != ColorMode::FaunaDensity && mode != ColorMode::FungaDensity)
{ mode = ColorMode::FloraDensity; recolor(); }
setStatus("Biota generated (flora/fauna/funga)");
}
if (IsKeyPressed(KEY_E) && settled) { // generate / toggle ecoregion atlas colour view
if (!planet.ecoregionsBuilt()) planet.generateEcoregions();
mode = (mode == ColorMode::Ecoregion) ? ColorMode::Biome : ColorMode::Ecoregion;
recolor();
setStatus(mode == ColorMode::Ecoregion ? "Ecoregions on" : "Ecoregions off");
}
if (IsKeyPressed(KEY_I) && settled) { // toggle the habitability heat-map view
planet.computeHabitability();
mode = (mode == ColorMode::Habitability) ? ColorMode::Biome : ColorMode::Habitability;
recolor();
setStatus(mode == ColorMode::Habitability ? "Habitability on" : "Habitability off");
}
if (IsKeyPressed(KEY_U) && settled) { // civilization: seed on first press ("the dawn"), then toggle markers
if (!planet.settlementsPlaced()) {
planet.placeSettlements();
rebuildTerritory(); // initial realms + borders
showSettlements = true;
appendEvent(3, 1, liveTime, planet.settlements.empty() ? 0 : planet.settlements[0].cell, 0,
"Civilization begins",
std::string(TextFormat("%d villages founded", (int)planet.settlements.size())));
setStatus(TextFormat("Civilization begins (%d settlements)", (int)planet.settlements.size()));
} else {
showSettlements = !showSettlements;
setStatus(showSettlements ? "Settlements on" : "Settlements off");
}
}
if (IsKeyPressed(KEY_P) && settled) { // toggle the territory / realms colour view + borders
if (!planet.settlementsPlaced()) setStatus("Press U for the dawn of civilization first");
else {
if (!planet.nationsBuilt() || (int)planet.cellNation().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Territory) ? ColorMode::Biome : ColorMode::Territory;
showNationBorders = (mode == ColorMode::Territory);
recolor();
setStatus(mode == ColorMode::Territory ? "Territory / realms on" : "Territory off");
}
}
if (IsKeyPressed(KEY_X) && settled) { // toggle the culture / faiths colour view + cultural borders
if (!planet.settlementsPlaced()) setStatus("Press U for the dawn of civilization first");
else {
if (!planet.culturesBuilt() || (int)planet.cellCulture().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Culture) ? ColorMode::Biome : ColorMode::Culture;
showCultureBorders = (mode == ColorMode::Culture);
recolor();
setStatus(mode == ColorMode::Culture ? "Cultures / faiths on" : "Cultures off");
}
}
if (IsKeyPressed(KEY_Z) && settled) { // toggle the wealth / trade colour view + trade routes
if (!planet.settlementsPlaced()) setStatus("Press U for the dawn of civilization first");
else {
if (!planet.tradeBuilt() || (int)planet.cellWealth().size() != (int)planet.cells.size()) rebuildTerritory();
mode = (mode == ColorMode::Wealth) ? ColorMode::Biome : ColorMode::Wealth;
showTradeRoutes = (mode == ColorMode::Wealth);
recolor();
setStatus(mode == ColorMode::Wealth ? "Wealth / trade on" : "Wealth off");
}
}
if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock)
liveWorld = !liveWorld;
if (liveWorld) {
phase3Prompt = false; paused = true; wxUndo.clear(); // enter Live World paused (SPACE to run)
if (planet.volcanoes.empty()) planet.placeVolcanoes(liveTime); // one-time tectonic-context placement
if (!planet.geographyBuilt()) planet.generateGeography(); // name the world's geography (the atlas)
refreshView(); // fresh base colours; overlay builds in stepSim
setStatus("Live World started - paused (SPACE to run)");
} else {
paused = true; followId = 0; wxUndo.clear(); refreshView(); // back to World Creation, paused
setStatus("Live World stopped");
}
}
if (IsKeyPressed(KEY_Y) && liveWorld) { // cycle the 3D camera through active storms
const auto& st = planet.storms();
if (st.empty()) { followId = 0; setStatus("No weather systems to follow"); }
else {
std::vector<int> idx(st.size()); for (size_t i = 0; i < st.size(); ++i) idx[i] = (int)i;
std::sort(idx.begin(), idx.end(), [&](int a, int b){ return st[a].strength > st[b].strength; });
int cur = -1; for (size_t k = 0; k < idx.size(); ++k) if (st[idx[k]].id == followId) { cur = (int)k; break; }
int next = (cur < 0) ? 0 : cur + 1;
if (next >= (int)idx.size()) { followId = 0; setStatus("Follow cam off"); }
else {
const WeatherSystem& ws = st[idx[next]];
followId = ws.id;
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
setStatus(hur ? "Following tropical cyclone" : "Following weather system");
}
}
}
if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator
if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline
if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows
if (IsKeyPressed(KEY_K)) showClouds = !showClouds; // toggle weather cloud/rain cover if (IsKeyPressed(KEY_K)) showClouds = !showClouds; // toggle weather cloud/rain cover
if (IsKeyPressed(KEY_V)) showVolcanoes = !showVolcanoes; // toggle volcano markers (Live World) if (IsKeyPressed(KEY_V)) showVolcanoes = !showVolcanoes; // toggle volcano markers (Live World)
if (IsKeyPressed(KEY_M) && settled) { // toggle / reshuffle place-name labels (the atlas) if (IsKeyPressed(KEY_M)) {
bool reshuffle = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) reshuffleNames();
if (reshuffle) { else toggleNames();
planet.reshuffleGeography(); // re-extract current geography + use different names
showNames = true;
setStatus("Place names reshuffled");
} else {
if (!planet.geographyBuilt()) planet.generateGeography(); // lazily name the world on first use
showNames = !showNames;
setStatus(showNames ? "Place names on" : "Place names off");
}
}
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_F3)) toggleEditMode();
if (IsKeyPressed(KEY_C)) { if (IsKeyPressed(KEY_C)) {
selectedCell = -1; subgrids.clear(); selectedCell = -1; subgrids.clear();
editPanel.editingField = EditField::None; editPanel.editBuffer.clear(); editPanel.pickerField = EditField::None; 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)) reseed();
if (IsKeyPressed(KEY_S)) { // one step if (IsKeyPressed(KEY_S)) stepAction();
if (liveWorld) liveStepForward(); // Live World: step the clock forward
else { stepOnce(); refreshView(); } // forming/drift: one tectonic tick
}
// Live World clock stepper: step by one rate-unit (liveRate hours). Forward integrates weather; // Live World clock stepper: step by one rate-unit (liveRate hours). Forward integrates weather;
// backward restores the snapshot from the last forward step -> everything (incl. weather + // backward restores the snapshot from the last forward step -> everything (incl. weather +
// storms) steps back, within the current paused stepping session. // storms) steps back, within the current paused stepping session.
if (IsKeyPressed(KEY_PERIOD) && liveWorld) { liveStepForward(); setStatus("Step forward"); } if (IsKeyPressed(KEY_PERIOD) && liveWorld) { liveStepForward(); setStatus("Step forward"); }
if (IsKeyPressed(KEY_COMMA) && liveWorld) { liveStepBack(); } // liveStepBack sets its own status if (IsKeyPressed(KEY_COMMA) && liveWorld) { liveStepBack(); } // liveStepBack sets its own status
if (IsKeyPressed(KEY_F)) { // fast-forward to settled if (IsKeyPressed(KEY_F)) fastForward();
if (!settled) { if (IsKeyPressed(KEY_F1)) showToolbar = !showToolbar; // additive: toggle the toolbar sidebar
while (!settled) stepOnce();
dtMy = planet.cflDtMy(); planet.drifting = true; refreshView();
}
}
if (IsKeyPressed(KEY_EQUAL) && cfg.subdivisions < 7) { cfg.subdivisions++; regen(); } if (IsKeyPressed(KEY_EQUAL) && cfg.subdivisions < 7) { cfg.subdivisions++; regen(); }
if (IsKeyPressed(KEY_MINUS) && cfg.subdivisions > 1) { cfg.subdivisions--; regen(); } if (IsKeyPressed(KEY_MINUS) && cfg.subdivisions > 1) { cfg.subdivisions--; regen(); }
if (IsKeyPressed(KEY_F2)) { if (IsKeyPressed(KEY_F2)) {

View File

@ -2,6 +2,7 @@
#include "Overlays.hpp" #include "Overlays.hpp"
#include "Map2D.hpp" #include "Map2D.hpp"
#include "Panels.hpp" #include "Panels.hpp"
#include "Toolbar.hpp" // drawToolbar
#include "Picking.hpp" // rotateZ (axial-tilt transform for labels) #include "Picking.hpp" // rotateZ (axial-tilt transform for labels)
#include "rlgl.h" #include "rlgl.h"
#include "Projection.hpp" // dirToLonLat (plate labels) #include "Projection.hpp" // dirToLonLat (plate labels)
@ -908,16 +909,8 @@ void Viewer::renderHUD() {
} }
y += 8; y += 8;
line("hover: cell info | click tile: open detail panel | C close"); line("hover: cell info | click tile: open detail panel | C close");
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga E eco (*6 cycles mean/summer/winter/season)"); line(showToolbar ? "Menu: top-right of the globe (F1 to hide) -- or use the keyboard shortcuts below"
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]", : "F1 or the Menu tab (top-right of the globe): a clickable menu for every action below");
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | E eco | I habitability | U settlements [%s] | P territory [%s] | X culture [%s] | Z trade [%s]",
showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off",
!planet.settlementsPlaced() ? "seed" : showSettlements ? "on" : "off",
showNationBorders ? "on" : "off", showCultureBorders ? "on" : "off", showTradeRoutes ? "on" : "off"));
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
if (!statusMsg.empty() && GetTime() < statusUntil) { if (!statusMsg.empty() && GetTime() < statusUntil) {
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22; y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
} }
@ -1078,6 +1071,7 @@ void Viewer::renderFrame() {
renderLiveInfo(); renderLiveInfo();
renderPanels(); renderPanels();
renderHUD(); renderHUD();
drawToolbar(*this); // drawn after the HUD, before the modal so it dims/locks together with it
renderPrompt(); renderPrompt();
EndDrawing(); EndDrawing();