A second, reference-style export alongside exportMapImage(): renders at ~8K and labels every named geography feature and every living settlement (settlement names weren't drawn as text anywhere before this), using a greedy priority-based placement pass that only draws a label if its box doesn't collide with one already placed -- so text never overlaps regardless of how densely settlements cluster. Keeps label font size well below the resolution scale, since scaling text 1:1 with a bigger canvas would just reproduce the same crowding at a bigger size. Also uses the plain colour-mode vcolors rather than displayColors(), since in Live World the latter is day/night-dimmed -- a reference atlas should read the same everywhere, not have an arbitrary half of it darkened by whatever moment it was exported.
255 lines
13 KiB
C++
255 lines
13 KiB
C++
#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);
|
|
}
|
|
// Deferred to after drawToolbar() returns -- see Viewer::pendingMapExport/pendingAtlasExport.
|
|
if (GuiButton(takeRow(cx, cw, cy), "Export Map (F11)")) v.pendingMapExport = true;
|
|
if (GuiButton(takeRow(cx, cw, cy), "Export Atlas (Shift+F11)")) v.pendingAtlasExport = true;
|
|
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();
|
|
}
|