diff --git a/BUILD.md b/BUILD.md index 7283288..b01fd32 100644 --- a/BUILD.md +++ b/BUILD.md @@ -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) F12 screenshot to screenshot.png 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): diff --git a/CLAUDE.md b/CLAUDE.md index c468c2f..cbe7d1b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -310,6 +310,31 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived 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. +- **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 @@ -731,6 +756,8 @@ src/ Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids Picking.* mouse ray / sphere hit / nearest-cell / angle helpers 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 ViewerInput.cpp handleInput(): camera, hover picking, click, keys 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 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) @@ -845,6 +874,8 @@ on a settled world; re-press regenerates) · `W` enter/leave **Live World** (set `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) · +`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 · `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 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 file (both applied before the initial load/generate). diff --git a/CMakeLists.txt b/CMakeLists.txt index 16820e8..f1a3477 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,18 @@ set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(BUILD_GAMES OFF CACHE BOOL "" FORCE) 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 src/sim/IcoSphere.cpp src/sim/Planet.cpp @@ -50,6 +62,8 @@ set(RENDER_SOURCES src/render/Overlays.cpp src/render/Picking.cpp src/render/Panels.cpp + src/render/Toolbar.cpp + src/render/RayGuiImpl.cpp src/render/Viewer.cpp src/render/ViewerInput.cpp src/render/ViewerRender.cpp @@ -63,7 +77,7 @@ add_executable(planetsim ${RENDER_SOURCES} ) # 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) # 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() 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) if(UNIX AND NOT APPLE) target_link_libraries(test_events PRIVATE m pthread dl) diff --git a/src/render/RayGuiImpl.cpp b/src/render/RayGuiImpl.cpp new file mode 100644 index 0000000..174952e --- /dev/null +++ b/src/render/RayGuiImpl.cpp @@ -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" diff --git a/src/render/Toolbar.cpp b/src/render/Toolbar.cpp new file mode 100644 index 0000000..10a2955 --- /dev/null +++ b/src/render/Toolbar.cpp @@ -0,0 +1,251 @@ +#include "Toolbar.hpp" +#include "Viewer.hpp" +#include "raygui.h" +#include +#include + +// 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(); +} diff --git a/src/render/Toolbar.hpp b/src/render/Toolbar.hpp new file mode 100644 index 0000000..0f1b74d --- /dev/null +++ b/src/render/Toolbar.hpp @@ -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(); diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index c1add4b..37636e4 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -1,6 +1,7 @@ #include "Viewer.hpp" #include "Picking.hpp" // angBetween (rebuildSub) #include "Projection.hpp" // EqualEarth (layout) +#include "Toolbar.hpp" // setupToolbarStyle #include #include #include @@ -117,6 +118,12 @@ bool Viewer::init(int argc, char** argv) { p3ContinueBtn = Rectangle{ pbCx - pbW - 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; if (!loadConfig(configPath, cfg)) saveConfig(configPath, cfg); // load, or create a default if (cliSeed != 0) cfg.seed = cliSeed; // CLI --seed overrides config @@ -897,6 +904,157 @@ void Viewer::rebuildTerritory() { 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 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. void Viewer::wxPushSnapshot() { if ((int)wxUndo.size() >= wxUndoMax) wxUndo.erase(wxUndo.begin()); diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index 49ef78d..04fc3c5 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -143,6 +143,16 @@ struct Viewer { bool editMode = false; 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. std::string statusMsg; double statusUntil = 0.0; @@ -198,6 +208,27 @@ struct Viewer { void detectNationEvents(const std::vector& beforeNations); 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) -------------------------------------------- void handleInput(); void handleEditClick(); // dispatch a click inside the edit panel (tabs/fields/locks/picker/remove) diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index 5adc60e..69b659e 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -16,6 +16,11 @@ void Viewer::handleInput() { bool inPanel = (selectedCell >= 0) && CheckCollisionPointRec(mp, panelRect); bool inLiveInfo = liveWorld && CheckCollisionPointRec(mp, liveInfoRect); 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) -- if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { @@ -66,7 +71,7 @@ void Viewer::handleInput() { if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && !phase3Prompt) { Vector2 d = GetMouseDelta(); 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; camPitch += d.y * 0.005f; 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. 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(); 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); @@ -125,7 +132,7 @@ void Viewer::handleInput() { hoveredSubIdx = j * R + i; 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, mp.x, mp.y, (float)view3DW, (float)view3DH); 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) - - 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); // --- Keys ------------------------------------------------------------- @@ -190,148 +197,39 @@ void Viewer::handleInput() { if (IsKeyPressed(KEY_D)) showDrift = !showDrift; if (IsKeyPressed(KEY_G)) showGrat = !showGrat; if (IsKeyPressed(KEY_J)) showRivers = !showRivers; - if (IsKeyPressed(KEY_H) && settled) { // toggle Phase 3 (hydrology) - 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(); - } - if (IsKeyPressed(KEY_L) && settled) { // generate / regenerate biota population - planet.generateBiota(); - if (planet.ecoregionsBuilt()) planet.generateEcoregions(); - 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 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_H)) toggleHydrology(); + if (IsKeyPressed(KEY_L)) generateOrRegenerateBiota(); + if (IsKeyPressed(KEY_E)) toggleEcoregionView(); + if (IsKeyPressed(KEY_I)) toggleHabitabilityView(); + if (IsKeyPressed(KEY_U)) placeOrToggleSettlements(); + if (IsKeyPressed(KEY_P)) toggleTerritoryView(); + if (IsKeyPressed(KEY_X)) toggleCultureView(); + if (IsKeyPressed(KEY_Z)) toggleTradeView(); + if (IsKeyPressed(KEY_W)) enterOrLeaveLiveWorld(); + if (IsKeyPressed(KEY_Y)) cycleFollowStorm(); if (IsKeyPressed(KEY_N)) dayNightOn = !dayNightOn; // toggle the day/night terminator if (IsKeyPressed(KEY_T)) showTides = !showTides; // toggle tide-coloured coastline if (IsKeyPressed(KEY_O)) showCurrents = !showCurrents; // toggle ocean current arrows 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_M) && settled) { // toggle / reshuffle place-name labels (the atlas) - bool reshuffle = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); - if (reshuffle) { - 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_M)) { + if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) reshuffleNames(); + else toggleNames(); } + if (IsKeyPressed(KEY_F3)) toggleEditMode(); 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_S)) { // one step - if (liveWorld) liveStepForward(); // Live World: step the clock forward - else { stepOnce(); refreshView(); } // forming/drift: one tectonic tick - } + if (IsKeyPressed(KEY_R)) reseed(); + if (IsKeyPressed(KEY_S)) stepAction(); // 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 + // storms) steps back, within the current paused stepping session. if (IsKeyPressed(KEY_PERIOD) && liveWorld) { liveStepForward(); setStatus("Step forward"); } if (IsKeyPressed(KEY_COMMA) && liveWorld) { liveStepBack(); } // liveStepBack sets its own status - if (IsKeyPressed(KEY_F)) { // fast-forward to settled - if (!settled) { - while (!settled) stepOnce(); - dtMy = planet.cflDtMy(); planet.drifting = true; refreshView(); - } - } + if (IsKeyPressed(KEY_F)) fastForward(); + if (IsKeyPressed(KEY_F1)) showToolbar = !showToolbar; // additive: toggle the toolbar sidebar if (IsKeyPressed(KEY_EQUAL) && cfg.subdivisions < 7) { cfg.subdivisions++; regen(); } if (IsKeyPressed(KEY_MINUS) && cfg.subdivisions > 1) { cfg.subdivisions--; regen(); } if (IsKeyPressed(KEY_F2)) { diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index 3c545af..d47cc15 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -2,6 +2,7 @@ #include "Overlays.hpp" #include "Map2D.hpp" #include "Panels.hpp" +#include "Toolbar.hpp" // drawToolbar #include "Picking.hpp" // rotateZ (axial-tilt transform for labels) #include "rlgl.h" #include "Projection.hpp" // dirToLonLat (plate labels) @@ -908,16 +909,8 @@ void Viewer::renderHUD() { } y += 8; 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(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]", - showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off")); - 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"); + line(showToolbar ? "Menu: top-right of the globe (F1 to hide) -- or use the keyboard shortcuts below" + : "F1 or the Menu tab (top-right of the globe): a clickable menu for every action below"); if (!statusMsg.empty() && GetTime() < statusUntil) { y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22; } @@ -1078,6 +1071,7 @@ void Viewer::renderFrame() { renderLiveInfo(); renderPanels(); renderHUD(); + drawToolbar(*this); // drawn after the HUD, before the modal so it dims/locks together with it renderPrompt(); EndDrawing();