The app used to always generate a world straight from planet.cfg on launch and F5 always overwrote a single fixed planet.save. Now it opens on a full-screen Main Menu (Home / Settings / Load World) so every PlanetConfig field, the seed, starting a new world, and browsing saves are all reachable without hand-editing planet.cfg, and saves are timestamped so nothing is silently overwritten. A toolbar button reopens the menu later without disturbing a running world. The settings editor is generic (no per-field UI code): configFieldTable() reuses the existing CONFIG_FIELDS X-macro to build a runtime field table, grouped into 25 categories via an explicit name->category lookup (a "first field of each section" boundary scan was tried first and is wrong, since CONFIG_FIELDS emits all doubles, then all ints, then the seed -- not struct declaration order, so most categories aren't contiguous in that order). Save v25 adds the viewer's colour-mode/overlay state, kept out of Planet::writeState/readState's signature so it touches none of the existing headless tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
292 lines
18 KiB
C++
292 lines
18 KiB
C++
#pragma once
|
|
#include "raylib.h"
|
|
#include "Planet.hpp"
|
|
#include "Colors.hpp" // ColorMode
|
|
#include "Overlays.hpp" // PlateLabel
|
|
#include "Map2D.hpp" // Map2D
|
|
#include "Panels.hpp" // EditField, EditPanelState
|
|
#include "MainMenu.hpp" // AppScreen, MenuTab, MainMenuState
|
|
#include <vector>
|
|
#include <string>
|
|
#include <memory>
|
|
|
|
// The interactive viewer: owns all window/sim/view state and runs the frame
|
|
// loop. The old free-standing main() lived as one giant function with capturing
|
|
// lambdas; those lambdas are now methods and their captured locals are members,
|
|
// so the body splits cleanly across Viewer.cpp (setup + sim orchestration),
|
|
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
|
struct Viewer {
|
|
// ---- Files / save format ------------------------------------------------
|
|
static constexpr uint32_t SAVE_VERSION = 25; // v25: +viewer colour-mode/overlay state (Viewer::saveGame/loadGame only, not Planet); v24: edit mode (per-cell lock bits + locked-value maps); v23: civ cultural evolution; v22: civ diplomacy; v21: civ conflict/wars; v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
|
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
|
static constexpr int EVENT_LOG_MAX = 200;
|
|
const char* CONFIG_PATH = "planet.cfg";
|
|
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
|
|
|
// ---- App screen (Main Menu vs. a live world) ----------------------------
|
|
AppScreen screen = AppScreen::MainMenu; // init() no longer auto-generates a world
|
|
MainMenuState menuState;
|
|
|
|
// ---- Window / layout (set in init) --------------------------------------
|
|
int screenW = 1920, screenH = 1080;
|
|
int leftW = 0, rightX = 0, rightW = 0, rightH = 0;
|
|
int view3DW = 0, view3DH = 0;
|
|
RenderTexture2D rt3d{};
|
|
Rectangle mapRect{}, hoverRect{}, panelRect{}, gridRect{};
|
|
Rectangle liveInfoRect{}; // free space right of the (left-aligned) 2D map: moon/tide phase
|
|
Rectangle pauseBtn{}, p3ContinueBtn{}, p3StartBtn{};
|
|
float pbCx = 0.0f, pbCy = 0.0f;
|
|
|
|
const float visBase = 2.0f;
|
|
const float elevExagg = 0.00000004f;
|
|
const float borderR = visBase + 0.004f;
|
|
const float driftR = visBase + 0.006f;
|
|
const float riverR = visBase + 0.005f;
|
|
const float gratR = visBase + 0.003f;
|
|
const int subRes = 16;
|
|
|
|
// ---- Sim / world --------------------------------------------------------
|
|
Planet planet;
|
|
PlanetConfig cfg;
|
|
|
|
Camera3D cam{};
|
|
float camYaw = 0.4f, camPitch = 0.3f, camDist = 6.0f;
|
|
|
|
ColorMode mode = ColorMode::Elevation;
|
|
std::vector<Color> vcolors;
|
|
|
|
std::vector<Vector3> borders, ridgeBorders; bool showBorders = true;
|
|
std::vector<Vector3> driftArrows; bool showDrift = true;
|
|
std::vector<PlateLabel> plateLabels;
|
|
std::vector<Vector3> rivers, bigRivers; bool showRivers = true;
|
|
std::vector<std::vector<Vector2>> graticule; bool showGrat = false;
|
|
Map2D map2D;
|
|
|
|
// Phase-1 forming model.
|
|
bool paused = false, settled = false;
|
|
long long stepCount = 0;
|
|
double maxChange = 0.0;
|
|
int settleRun = 0;
|
|
const double settleThresh = 2.0; // m/tick at or below which it's "settled"
|
|
const int settleNeed = 3; // consecutive settled ticks before pausing
|
|
const double formRate = 55.0; // forming ticks per second (watchable)
|
|
double formAccum = 0.0;
|
|
double minE = 0.0, maxE = 0.0;
|
|
|
|
// Phase 2 drift.
|
|
double elapsedMy = 0.0, dtMy = 0.0, driftRate = 4.0, driftAccum = 0.0;
|
|
|
|
// Phase 3 hydrology.
|
|
bool phase3 = false, phase3Prompt = false;
|
|
double phase3PromptAt = 0.0;
|
|
|
|
// Live World: a slow real-time clock (hours -> weeks/months) over the finished planet.
|
|
// Geological drift freezes while it runs; a moving day/night terminator, a live seasonal
|
|
// temperature cycle and a moving snow line animate. liveRate is sim hours per real second.
|
|
bool liveWorld = false, dayNightOn = true;
|
|
double liveTime = 0.0; // hours since the live clock started
|
|
double liveRate = 1.0; // sim hours advanced per real second (ramps hour -> ~20 years)
|
|
// Top clock speed: ~20 sim-years per real second (a century in ~5 s). Kept a code constant like the
|
|
// old 720 (1 month/s); the [ / ] ramp and the load-resume clamp both clamp to this.
|
|
double liveRateMax() const { return 20.0 * planet.cfg.dayLengthHours * planet.cfg.yearLengthDays; }
|
|
// Step-back undo history: each forward step snapshots the clock + full weather state so a
|
|
// backward step restores everything (weather is an integrated path, not analytically reversible).
|
|
struct WxFrame { double t; WeatherSnapshot w; };
|
|
std::vector<WxFrame> wxUndo;
|
|
static constexpr int wxUndoMax = 180;
|
|
std::vector<float> illum; // per-cell day/night brightness (1 = day, floor = night)
|
|
std::vector<Color> shadedColors;// vcolors + snow/ice tint + day/night dim (live overlay)
|
|
Vector3 sunDir{0.0f, 0.0f, 1.0f}; // model-space sub-solar direction (for the 3D sun marker)
|
|
std::vector<Vector3> moonDirs; // model-space sub-lunar directions (one per moon, for render)
|
|
std::vector<Vector3> moonNormals;// model-space orbit-plane normals (one per moon, for the ring)
|
|
std::vector<Vector3> coast; // coastline segments (land/ocean boundary, rebuilt with terrain)
|
|
std::vector<int> coastOcean; // ocean cell per coast segment (to sample tide)
|
|
std::vector<Color> coastCols; // per-segment tide colour (filled each frame when showTides)
|
|
bool showTides = false; // colour the coastline by the live tide level (key T)
|
|
std::vector<Vector3> currentSegs; std::vector<Color> currentCols; // ocean-current arrows
|
|
bool showCurrents = false; // ocean current arrows, warm/cold (key O)
|
|
bool showClouds = true; // Live World cloud/rain cover overlay (key K)
|
|
bool showVolcanoes = true; // Live World volcano markers (cones + eruption glow, key V)
|
|
bool showNames = false; // geographic place-name labels (the atlas, key M)
|
|
bool showSettlements = true; // civilization settlement markers (key U seeds + toggles)
|
|
std::vector<int> atlasRowCells; // cell to focus per visible Atlas/Eco/Civ/Realms-tab row (parallel to the list)
|
|
std::vector<Vector3> nationBorders; // political border segments (rebuilt on year tick / placement / load)
|
|
std::vector<Vector3> cultureBorders; // cultural-region border segments (civ Step 4, rebuilt with territory)
|
|
std::vector<Vector3> warFrontier; // red frontier segments between realms currently at war (civ Step 5)
|
|
std::vector<Vector3> allyLinks, rivalLinks; // civ Step 6: capital-to-capital arcs (allies green, rivals red)
|
|
bool showDiplomacy = true; // draw the alliance/rivalry arcs above with the Territory view (key A) --
|
|
// independent of showNationBorders() so Territory can be shown without them
|
|
std::vector<Vector3> tradeSea, tradeLand; // civ Step 7: trade routes (sea cyan / river+land amber)
|
|
long lastTerritoryYear = -1; // sim year territory was last recomputed (recompute when it ticks)
|
|
// Whether to draw the political/cultural/trade overlays is *derived* from `mode` -- deliberately
|
|
// not a separately-stored bool. It used to be (showNationBorders et al., set only inside the P/X/Z
|
|
// toggle methods), and every other way of changing `mode` (number keys, the toolbar's view-mode
|
|
// dropdown) left it stale: switching away from the Territory view by any means other than pressing
|
|
// P again left showNationBorders stuck true forever (realm borders/war fronts/alliance arcs kept
|
|
// drawing over every other view), while pressing P/X/Z never actually stuck because of a second,
|
|
// compounding bug in the dropdown sync (see drawToolbar()) -- so it looked like the toggle
|
|
// "immediately turned back off". Deriving it removes the class of bug entirely.
|
|
bool showNationBorders() const { return mode == ColorMode::Territory; }
|
|
bool showCultureBorders() const { return mode == ColorMode::Culture; }
|
|
bool showTradeRoutes() const { return mode == ColorMode::Wealth; }
|
|
bool showRealms = false; // political borders + realm-name (+dominant-culture) labels over
|
|
// ANY colour mode, not just Territory (key Q) -- ||-ed with
|
|
// showNationBorders() at just the border-line and realm-label draw
|
|
// sites, so e.g. the Biome view can show realm borders/names
|
|
// without nation-tinting cells. War fronts and diplomacy arcs stay
|
|
// showNationBorders()-only on purpose (Territory-mode-exclusive),
|
|
// so this overlay never drags in unrelated clutter.
|
|
|
|
// World event journal: currently Live World events, shaped to be reused by later phases.
|
|
struct WorldEvent {
|
|
uint32_t id = 0;
|
|
uint8_t kind = 0; // 1 weather, 2 volcano, later phases can append new kinds
|
|
uint8_t severity = 0; // 0 info, 1 notable, 2 severe
|
|
double timeHours = 0.0;
|
|
int cell = -1;
|
|
uint32_t sourceId = 0;
|
|
std::string title, detail;
|
|
};
|
|
std::vector<WorldEvent> events;
|
|
uint32_t nextEventId = 1;
|
|
int liveInfoTab = 0; // 0 Sky, 1 Tides, 2 Weather, 3 Events, 4 Atlas, 5 Eco
|
|
std::vector<Rectangle> liveInfoTabRects;
|
|
std::vector<Rectangle> eventRowRects;
|
|
std::vector<int> eventRowIndices; // indices into events for visible event rows
|
|
|
|
// Selection + subgrid (phase 4/5 preview).
|
|
int selectedCell = -1;
|
|
double selectedThresh = 0.06;
|
|
std::vector<std::shared_ptr<SubGrid>> subgrids;
|
|
|
|
// Edit mode (key F3, on a settled world; save v24): manually edit every property of the
|
|
// selected cell, with locking against automatic per-tick recomputation. Reuses selectedCell.
|
|
bool editMode = false;
|
|
EditPanelState editPanel;
|
|
|
|
// 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
|
|
bool pendingMapExport = false; // Export-Map button: deferred until after drawToolbar()
|
|
// returns, so its own BeginScissorMode/EndScissorMode
|
|
// (around the scroll content) isn't still active --
|
|
// that clip rect would otherwise also clip the export
|
|
// render-texture pass.
|
|
bool pendingAtlasExport = false; // Export-Atlas button: same deferral, see above.
|
|
|
|
// Transient on-screen status line.
|
|
std::string statusMsg; double statusUntil = 0.0;
|
|
|
|
// Input state.
|
|
float dragDist = 0.0f;
|
|
double mapLon = 0.0; // 2D map longitude pan (radians)
|
|
double mapZoom = 1.0; // 2D map zoom factor (1 = whole map; up to 8x)
|
|
double mapPanX = 0.0, mapPanY = 0.0; // 2D map screen-space pan (pixels, used when zoomed)
|
|
bool pressInMap = false; // a drag that started on the map pans it
|
|
uint32_t followId = 0; // Live World: id of the storm the 3D camera follows (0 = none)
|
|
|
|
// Per-frame picking state (set by handleInput, read by render).
|
|
Vector2 mp{};
|
|
bool onPause = false;
|
|
int hovered = -1;
|
|
bool hasHoverSub = false;
|
|
SubCell hoverSub;
|
|
int hoveredSubIdx = -1;
|
|
|
|
// ---- Lifecycle ----------------------------------------------------------
|
|
bool init(int argc, char** argv); // window, layout, config, first world
|
|
void run(); // the frame loop (until window closes)
|
|
|
|
// ---- Sim orchestration (Viewer.cpp) -------------------------------------
|
|
void rebuildSub();
|
|
void selectCell(int idx);
|
|
void recolor();
|
|
void refreshView();
|
|
void rebuildTerritory(); // recompute nations/territory + nation-border segments
|
|
void rebuildLiveOverlay(); // Live World: fill illum + shadedColors from sim fields
|
|
// Colors the 3D globe + 2D map actually draw: the live overlay when in Live World, else the
|
|
// plain per-cell colours.
|
|
const std::vector<Color>& displayColors() const { return liveWorld ? shadedColors : vcolors; }
|
|
void regenWorld(); // after generate(): geometry changed
|
|
void regen(); // generate(cfg) + regenWorld()
|
|
void startNewWorld(); // validate cfg, regen(), switch screen to World (Main Menu "New World")
|
|
void commitMenuField(); // parse menuState.editBuffer and apply it to the focused config field
|
|
void stepOnce(); // one tick + settle bookkeeping
|
|
void pauseAction();
|
|
void setStatus(const std::string& m);
|
|
void saveGame(const char* path);
|
|
void loadGame(const char* path);
|
|
void stepSim(); // advance forming / drift+hydrology this frame
|
|
void liveAdvance(double dtClock, double dtWeather); // advance the Live World clock + fields
|
|
void liveStepForward(); // step the clock forward one rate-unit (snapshots for undo)
|
|
void liveStepBack(); // step everything back one frame (restores weather/storms)
|
|
void wxPushSnapshot(); // push the current weather state onto the step-back ring
|
|
Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect)
|
|
// Shared 2D-map overlay draw (borders/rivers/markers/labels): used by both the on-screen
|
|
// renderMap2D() (scale=1, markerZoom=on-screen zoom clamp) and exportMapImage() (both = the
|
|
// export resolution multiplier, so lines/fonts/markers scale up together at high-res).
|
|
void drawMapOverlays(Rectangle vr, double lonOffset, float scale, float markerZoom, bool showMinorLabels,
|
|
bool drawLabels = true);
|
|
void exportMapImage(); // render the 2D map at high resolution and save as PNG (F11)
|
|
void exportAtlasImage(); // even-higher-res export with every place/settlement name
|
|
// laid out with collision avoidance so text never overlaps
|
|
// (Shift+F11)
|
|
void appendEvent(uint8_t kind, uint8_t severity, double timeHours, int cell, uint32_t sourceId,
|
|
const std::string& title, const std::string& detail);
|
|
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
|
const std::vector<Volcano>& beforeVolcanoes,
|
|
const std::vector<Settlement>& beforeSettlements,
|
|
const std::vector<double>& beforePlague);
|
|
void detectNationEvents(const std::vector<Nation>& 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 toggleRealmOverlay(); // Q: realm borders + names (+culture) over any colour mode
|
|
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)
|
|
bool nudgeEditField(float wheel);// mouse-wheel adjusts the hovered numeric field; false if none hovered
|
|
void commitEditField(); // parse editPanel.editBuffer and apply it to editPanel.editingField
|
|
void exitEditMode(); // clear edit-mode UI state (on F3 off / reseed / deselect)
|
|
|
|
// ---- Render (ViewerRender.cpp) ------------------------------------------
|
|
void renderFrame();
|
|
void renderGlobe3D();
|
|
void renderMap2D();
|
|
void renderLiveInfo(); // Live World: moon phases + (coastal) tidal phase, beside the 2D map
|
|
void renderPanels();
|
|
void renderHUD();
|
|
void renderPrompt();
|
|
};
|
|
|
|
// Free helpers for the timestamped save scheme (no Viewer state needed):
|
|
// F5 / the toolbar "Save" button always write a fresh file; F9 / "Load" pick the newest.
|
|
std::string makeTimestampedSavePath(); // "world_YYYY-MM-DD_HHMMSS.save"
|
|
std::vector<std::string> listSaveFiles(); // *.save in the working dir, newest first
|