F3 opens a tabbed panel (Geology/Climate/Biota/Settlement) for the selected tile. Every field writes straight into the field the sim already reads next tick (Planet::setXxx(), PlanetEdit.cpp), so an unlocked edit is a one-off nudge the simulation keeps evolving afterward; a per-field lock button sets a new per-cell EditLock bitmask that exempts it from automatic recompute (tectonics/erosion/ hydrology/drift/biomes skip a locked cell's write; climate/biota- density/habitability, which have no other persistent storage, pin a value in a small sparse map instead). Settlement population/ allegiance/culture locks are keyed by the settlement's home cell. Adds headless test_edit.cpp (nudge vs. lock for every field, save round-trip, corrupt-stream rejection, pre-v24 compatibility) and fixes a real bug found while testing: raylib's default ESC-exits-app behavior collided with edit mode's Escape-to-cancel, so SetExitKey is now disabled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
544 lines
32 KiB
C++
544 lines
32 KiB
C++
#include "Panels.hpp"
|
|
#include "Colors.hpp" // elevationColor (subtile grid)
|
|
#include "PlanetBiota.hpp" // organismName / sizeName / roleName
|
|
#include "Projection.hpp" // dirToLonLat
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <sstream>
|
|
|
|
// Draw `text` word-wrapped to `maxW` pixels starting at (x,y); continuation lines
|
|
// are indented. Returns the y after the last line; stops drawing past `maxY` (but
|
|
// keeps advancing y so callers can detect the overflow). Long biota lists would
|
|
// otherwise run off the right edge of the cell-info panel.
|
|
static int drawWrapped(const std::string& text, int x, int y, int font, Color col,
|
|
int maxW, int lineH, int maxY) {
|
|
std::istringstream iss(text);
|
|
std::string word, line;
|
|
int indent = 0;
|
|
auto flush = [&]() {
|
|
if (!line.empty()) { if (y + lineH <= maxY) DrawText(line.c_str(), x + indent, y, font, col);
|
|
y += lineH; line.clear(); indent = 14; }
|
|
};
|
|
while (iss >> word) {
|
|
std::string test = line.empty() ? word : line + " " + word;
|
|
if (MeasureText(test.c_str(), font) > maxW - indent && !line.empty()) { flush(); line = word; }
|
|
else line = test;
|
|
}
|
|
flush();
|
|
return y;
|
|
}
|
|
|
|
// elev/age come from the display snapshot so the readout matches what is drawn.
|
|
static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, double age) {
|
|
const Cell& c = p.cells[i];
|
|
double lon, lat; dirToLonLat(c.unit, lon, lat);
|
|
static const Plate kUnknownPlate{}; // defensive: a cell should always own a valid plate
|
|
const Plate& pl = (c.plateId >= 0 && c.plateId < (int)p.plates.size()) ? p.plates[c.plateId] : kUnknownPlate;
|
|
const int n = (int)p.cells.size();
|
|
auto sized = [&](const std::vector<double>& v) { return (int)v.size() == n; };
|
|
std::vector<std::string> L;
|
|
L.push_back(std::string(TextFormat("Cell #%d", i)));
|
|
L.push_back(std::string(TextFormat("lat %+6.1f lon %+6.1f", lat * 180.0 / M_PI, lon * 180.0 / M_PI)));
|
|
L.push_back(std::string(TextFormat("elev %.0f m (%s)", elev,
|
|
elev < p.cfg.seaLevel ? "ocean" : "land")));
|
|
L.push_back(std::string(TextFormat("plate %d (%s) crust %s", c.plateId,
|
|
pl.type == PlateType::Oceanic ? "Oceanic" : "Continental",
|
|
c.oceanic ? "Oceanic" : "Continental")));
|
|
L.push_back(std::string(TextFormat("biome: %s", biomeName(c.biome))));
|
|
// Geography (the atlas): which named features this cell belongs to.
|
|
if (p.geographyBuilt()) {
|
|
const auto& F = p.geography();
|
|
auto nameOf = [&](const std::vector<int>& arr) -> const char* {
|
|
int fi = (i < (int)arr.size()) ? arr[i] : -1;
|
|
return (fi >= 0 && fi < (int)F.size()) ? F[fi].name.c_str() : nullptr;
|
|
};
|
|
const char* loc = nameOf(c.elevation > p.cfg.seaLevel ? p.cellLand() : p.cellWater());
|
|
if (loc) L.push_back(std::string("region: ") + loc);
|
|
if (const char* rg = nameOf(p.cellRange())) L.push_back(std::string(" ") + rg);
|
|
if (const char* rv = nameOf(p.cellRiver())) L.push_back(std::string(" on the ") + rv);
|
|
// A lake cell sits on land but its water feature is the lake.
|
|
const char* lk = (c.elevation > p.cfg.seaLevel) ? nameOf(p.cellWater()) : nullptr;
|
|
if (lk) L.push_back(std::string(" ") + lk);
|
|
}
|
|
if (p.ecoregionsBuilt()) {
|
|
const auto& E = p.ecoregions();
|
|
const auto& ce = p.cellEcoregion();
|
|
int ei = (i < (int)ce.size()) ? ce[i] : -1;
|
|
if (ei >= 0 && ei < (int)E.size()) {
|
|
const Ecoregion& e = E[ei];
|
|
L.push_back(std::string("ecoregion: ") + e.name);
|
|
auto dom = [&](const char* tag, int arch) {
|
|
if (arch < 0 || arch >= (int)biotaArchetypes().size()) return;
|
|
Organism o{ (uint16_t)arch, (uint8_t)e.biome };
|
|
const BiotaArchetype& a = biotaArchetypes()[arch];
|
|
L.push_back(std::string(" ") + tag + ": " + organismName(o) +
|
|
" (" + roleName(a.role) + ")");
|
|
};
|
|
dom("flora", e.dominantFlora);
|
|
dom("fauna", e.dominantFauna);
|
|
dom("funga", e.dominantFunga);
|
|
}
|
|
}
|
|
// Civilization: the cell's settlement (if any) + its habitability/food score.
|
|
if (sized(p.habitability()))
|
|
L.push_back(std::string(TextFormat("habitability %.0f%%", p.habitability()[i] * 100.0)));
|
|
if (p.settlementsPlaced()) {
|
|
const auto& cs = p.cellSettlement();
|
|
int si = (i < (int)cs.size()) ? cs[i] : -1;
|
|
if (si >= 0 && si < (int)p.settlements.size()) {
|
|
const Settlement& s = p.settlements[si];
|
|
SettleTier t = settleTierOf(s.population, p.cfg.civTownPop, p.cfg.civCityPop);
|
|
bool alive = s.population >= p.cfg.civAbandonPop;
|
|
const char* pop = s.population >= 1.0e6 ? TextFormat("%.2fM", s.population / 1.0e6)
|
|
: s.population >= 1.0e3 ? TextFormat("%.0fk", s.population / 1.0e3)
|
|
: TextFormat("%.0f", s.population);
|
|
L.push_back(std::string(alive ? settleTierName(t) : "Ruins of") + " " + s.name
|
|
+ " (pop " + pop + ")");
|
|
// Live conditions: drought / hardship / boom / plague (derived each civ step).
|
|
const auto& cond = p.settlementCondition(); const auto& dro = p.settlementDrought();
|
|
if (si < (int)cond.size()) {
|
|
double cd = cond[si], dr = (si < (int)dro.size()) ? dro[si] : 0.0;
|
|
if (dr > 0.15) L.push_back(std::string(TextFormat(" drought %.0f%% conditions %.0f%%", dr * 100.0, cd * 100.0)));
|
|
else L.push_back(std::string(TextFormat(" conditions %.0f%% (%s)", cd * 100.0,
|
|
cd > 1.05 ? "good harvest" : cd < 0.8 ? "hardship" : "normal")));
|
|
const auto& plv = p.settlementPlague();
|
|
double pl = (si < (int)plv.size()) ? plv[si] : 0.0;
|
|
if (pl > 0.01) L.push_back(std::string(TextFormat(" PLAGUE -%.0f%%/yr", pl * 100.0)));
|
|
}
|
|
}
|
|
}
|
|
// Territory: which realm controls this cell (civ Step 3).
|
|
if (p.nationsBuilt()) {
|
|
const auto& cn = p.cellNation();
|
|
int ni = (i < (int)cn.size()) ? cn[i] : -1;
|
|
if (ni >= 0 && ni < (int)p.nationList().size()) {
|
|
const Nation& nat = p.nationList()[ni];
|
|
bool war = false; // civ Step 5: is this realm at war?
|
|
for (const War& w : p.warList()) if (w.attacker == nat.capital || w.defender == nat.capital) { war = true; break; }
|
|
L.push_back(std::string("realm: ") + nat.name + " (" + nationTierName(nat.tier) + ")" + (war ? " - AT WAR" : ""));
|
|
int al = 0, rv = 0; // civ Step 6: standing relations
|
|
for (const DiploTie& t : p.diploList()) {
|
|
if (t.a != nat.capital && t.b != nat.capital) continue;
|
|
if (t.kind == DiploKind::Alliance) ++al; else if (t.kind == DiploKind::Rival) ++rv;
|
|
}
|
|
if (al > 0 || rv > 0) L.push_back("allies: " + std::to_string(al) + " / rivals: " + std::to_string(rv));
|
|
} else if (p.cells[i].elevation > p.cfg.seaLevel) {
|
|
L.push_back(std::string("realm: wilderness"));
|
|
}
|
|
}
|
|
// Trade wealth of the settlement on this cell (civ Step 7).
|
|
if (p.tradeBuilt() && i < (int)p.cellSettlement().size() && p.cellSettlement()[i] >= 0) {
|
|
int si = p.cellSettlement()[i];
|
|
if (si < (int)p.prosperity().size()) {
|
|
int routes = 0; for (const TradeLink& t : p.tradeLinks()) if (t.a == si || t.b == si) ++routes;
|
|
L.push_back(std::string(TextFormat("prosperity: %.2f / trade: %d routes", p.prosperity()[si], routes)));
|
|
}
|
|
}
|
|
// Culture, ethos & faith of this cell's people (civ Step 4).
|
|
if (p.culturesBuilt()) {
|
|
const auto& cc = p.cellCulture();
|
|
int ci = (i < (int)cc.size()) ? cc[i] : -1;
|
|
if (ci >= 0 && ci < (int)p.cultureList().size()) {
|
|
const Culture& cu = p.cultureList()[ci];
|
|
L.push_back(std::string("culture: ") + cu.name + " (" + cultureEthosName(cu.ethos) + ")");
|
|
L.push_back(std::string("faith: ") + cu.faithName + " (" + faithFocusName(cu.faith) + ")");
|
|
}
|
|
}
|
|
// Climate (derived; present once computeClimate() has run).
|
|
if (sized(p.temperature()) && sized(p.moisture()))
|
|
L.push_back(std::string(TextFormat("temp %.1f C precip %.0f%%",
|
|
p.temperature()[i], p.moisture()[i] * 100.0)));
|
|
if (sized(p.summerTemp()) && sized(p.winterTemp()))
|
|
L.push_back(std::string(TextFormat(" summer %.0f C / winter %.0f C",
|
|
p.summerTemp()[i], p.winterTemp()[i])));
|
|
// Live World: current-season temperature + whether it's day or night + snow cover.
|
|
if (sized(p.liveTemp())) {
|
|
bool day = sized(p.insolation()) && p.insolation()[i] > 0.05;
|
|
bool snow = (elev > p.cfg.seaLevel) ? (p.liveTemp()[i] < p.cfg.snowTemp)
|
|
: (p.liveTemp()[i] < p.cfg.seaIceTemp);
|
|
L.push_back(std::string(TextFormat("live %.1f C %s%s", p.liveTemp()[i],
|
|
day ? "day" : "night", snow ? " snow" : "")));
|
|
}
|
|
if (sized(p.tide()))
|
|
L.push_back(std::string(TextFormat("tide %+.2f m (%s)", p.tide()[i],
|
|
p.tide()[i] >= 0.0 ? "high" : "low")));
|
|
if (sized(p.cloud()))
|
|
L.push_back(std::string(TextFormat("weather: cloud %.0f%% humidity %.0f%%%s",
|
|
p.cloud()[i] * 100.0,
|
|
sized(p.humidity()) ? p.humidity()[i] * 100.0 : 0.0,
|
|
(sized(p.rain()) && p.rain()[i] > 0.02) ? " raining" : "")));
|
|
L.push_back(std::string(TextFormat("geoAge %.0f My neighbors %d", age, (int)c.neighbors.size())));
|
|
// Hydrology (derived; present once routeFlow()/hydrology() has run).
|
|
if (sized(p.discharge()) && p.discharge()[i] > p.cfg.riverThreshold)
|
|
L.push_back(std::string(TextFormat("river: discharge %.0f", p.discharge()[i])));
|
|
if (sized(p.lakeDepth()) && p.lakeDepth()[i] > p.cfg.biomeLakeMinDepth && elev > p.cfg.seaLevel)
|
|
L.push_back(std::string(TextFormat("lake: depth %.0f m", p.lakeDepth()[i])));
|
|
// Volcano (Live World): lifecycle phase and current built height.
|
|
for (const Volcano& vc : p.volcanoes) {
|
|
if (vc.cell != i) continue;
|
|
const char* kn = vc.kind == 0 ? "ridge" : vc.kind == 1 ? "border" : "hotspot";
|
|
if (vc.ashTimer > 0.0) {
|
|
L.push_back(std::string(TextFormat("volcano: %s erupting +%.0f m activity %.0f%%",
|
|
kn, vc.built, vc.activity * 100.0)));
|
|
} else if (vc.phase == 1) {
|
|
L.push_back(std::string(TextFormat("volcano: %s dormant %.0f y +%.0f m",
|
|
kn, vc.timer / (24.0 * 365.25), vc.built)));
|
|
} else {
|
|
L.push_back(std::string(TextFormat("volcano: %s growing +%.0f m activity %.0f%%",
|
|
kn, vc.built, vc.activity * 100.0)));
|
|
}
|
|
break;
|
|
}
|
|
// Biota: density scalars (present after computeBiotaDensity()) + the discrete
|
|
// population list (present once generateBiota()/L has run).
|
|
if (sized(p.floraDensity()) && sized(p.faunaDensity()) && sized(p.fungaDensity()))
|
|
L.push_back(std::string(TextFormat("flora %.0f%% fauna %.0f%% funga %.0f%%",
|
|
p.floraDensity()[i] * 100.0, p.faunaDensity()[i] * 100.0, p.fungaDensity()[i] * 100.0)));
|
|
if (p.biotaPopulated() && i < (int)p.biota().size()) {
|
|
const CellBiota& cb = p.biota()[i];
|
|
// Each organism reads as Family (Size, Role) -- proper taxonomy, never an
|
|
// informal common name; generalists carry a biome adjective ("Forest Felidae").
|
|
// Identical archetypes in a cell aggregate to "... xN" so the list stays clean.
|
|
auto listKind = [&](const char* tag, const std::vector<Organism>& v) {
|
|
if (v.empty()) return;
|
|
std::vector<std::pair<Organism, int>> uniq; // representative + count, first-seen order
|
|
for (const Organism& o : v) {
|
|
bool found = false;
|
|
for (auto& u : uniq) if (u.first.archetype == o.archetype) { ++u.second; found = true; break; }
|
|
if (!found) uniq.push_back({o, 1});
|
|
}
|
|
std::string s = tag;
|
|
int shown = (int)std::min<size_t>(uniq.size(), 6);
|
|
for (int k = 0; k < shown; ++k) {
|
|
const BiotaArchetype& a = biotaArchetypes()[uniq[k].first.archetype];
|
|
s += (k ? ", " : " ") + organismName(uniq[k].first) +
|
|
" (" + sizeName(a.size) + ", " + roleName(a.role) + ")";
|
|
if (uniq[k].second > 1) s += TextFormat(" x%d", uniq[k].second);
|
|
}
|
|
if ((int)uniq.size() > shown) s += TextFormat(", +%d more", (int)uniq.size() - shown);
|
|
L.push_back(s);
|
|
};
|
|
listKind("Flora:", cb.flora);
|
|
listKind("Fauna:", cb.fauna);
|
|
listKind("Funga:", cb.funga);
|
|
}
|
|
return L;
|
|
}
|
|
|
|
void drawDetailPanel(const Planet& p, const std::shared_ptr<SubGrid>& sg,
|
|
int macro, double macroElev, double macroAge,
|
|
Rectangle panel, Rectangle grid, int hoveredSub) {
|
|
DrawRectangleRec(panel, Color{12, 14, 22, 235});
|
|
DrawRectangleLinesEx(panel, 1, Color{120, 120, 150, 255});
|
|
int tx = (int)panel.x + 10, ty = (int)panel.y + 8;
|
|
DrawText(TextFormat("Tile #%d", macro), tx, ty, 20, RAYWHITE);
|
|
DrawText("C: close", (int)(panel.x + panel.width) - 78, ty + 4, 14, Color{170, 170, 185, 255});
|
|
ty += 28;
|
|
// Cramped above the subtile grid -> stop before overlapping it (the full list is
|
|
// always shown in the top-right hover panel, which has room).
|
|
int infoMaxW = (int)(panel.x + panel.width) - tx - 10;
|
|
for (auto& s : cellInfo(p, macro, macroElev, macroAge)) {
|
|
if (ty + 16 > (int)grid.y) break;
|
|
ty = drawWrapped(s, tx, ty, 14, Color{210, 210, 220, 255}, infoMaxW, 16, (int)grid.y);
|
|
}
|
|
|
|
if (!sg || sg->res < 2) return;
|
|
int R = sg->res;
|
|
float cw = grid.width / R, ch = grid.height / R;
|
|
DrawText(TextFormat("Subtiles %dx%d (elevation; dim = neighbor)", R, R),
|
|
(int)grid.x, (int)grid.y - 18, 14, Color{200, 200, 210, 255});
|
|
for (int j = 0; j < R; ++j)
|
|
for (int i = 0; i < R; ++i) {
|
|
const SubCell& s = sg->sub[(size_t)j * R + i];
|
|
Color col = elevationColor(s.elevation, p.cfg.seaLevel);
|
|
if (s.nearestMacro != macro) { // territory of a neighbor
|
|
col.r = (unsigned char)(col.r * 0.55); col.g = (unsigned char)(col.g * 0.55);
|
|
col.b = (unsigned char)(col.b * 0.55);
|
|
}
|
|
DrawRectangle((int)(grid.x + i * cw), (int)(grid.y + j * ch),
|
|
(int)std::ceil(cw), (int)std::ceil(ch), col);
|
|
}
|
|
DrawRectangleLinesEx(grid, 1, Color{90, 90, 110, 255});
|
|
|
|
int by = (int)(grid.y + grid.height) + 6;
|
|
if (hoveredSub >= 0) {
|
|
int i = hoveredSub % R, j = hoveredSub / R;
|
|
DrawRectangleLinesEx(Rectangle{grid.x + i * cw, grid.y + j * ch, cw, ch}, 2, WHITE);
|
|
const SubCell& s = sg->sub[hoveredSub];
|
|
double lon, lat; dirToLonLat(s.unit, lon, lat);
|
|
DrawText(TextFormat("subtile [%d,%d] elev %.0f m", i, j, s.elevation),
|
|
(int)panel.x + 10, by, 15, RAYWHITE);
|
|
DrawText(TextFormat("under macro #%d lat %+.2f lon %+.2f",
|
|
s.nearestMacro, lat * 180.0 / M_PI, lon * 180.0 / M_PI),
|
|
(int)panel.x + 10, by + 18, 14, Color{200, 200, 210, 255});
|
|
} else {
|
|
DrawText("hover a subtile for detail", (int)panel.x + 10, by, 14, Color{170, 170, 185, 255});
|
|
}
|
|
}
|
|
|
|
void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected) {
|
|
DrawRectangleRec(r, Color{12, 14, 22, 235});
|
|
DrawRectangleLinesEx(r, 1, Color{120, 120, 150, 255});
|
|
int x = (int)r.x + 18, y = (int)r.y + 14;
|
|
DrawText("Cell info", x, y, 24, RAYWHITE);
|
|
y += 46;
|
|
int shown = (hovered >= 0) ? hovered : selected;
|
|
if (shown < 0) {
|
|
DrawText("hover the 3D globe or the 2D map", x, y, 20, Color{170, 170, 185, 255});
|
|
return;
|
|
}
|
|
if (hovered < 0) { DrawText("(selected tile)", x, y, 18, Color{210, 180, 120, 255}); y += 30; }
|
|
int maxW = (int)(r.x + r.width) - x - 14; // wrap to the panel's inner width
|
|
int maxY = (int)(r.y + r.height) - 10; // clamp to the panel bottom
|
|
for (auto& s : cellInfo(p, shown, p.cells[shown].elevation, p.cells[shown].geoAge)) {
|
|
y = drawWrapped(s, x, y, 20, Color{215, 220, 230, 255}, maxW, 26, maxY);
|
|
if (y > maxY) break;
|
|
}
|
|
}
|
|
|
|
// Draw one editable row: a label + a value box (click to type / toggle / open a picker) + its
|
|
// lock-toggle button. Rows sharing a lock bit (e.g. Crust + GeoAge -> LockCrust) each draw their
|
|
// own button wired to the same id -- redundant but simplest, and both stay visually in sync since
|
|
// the lock state is re-read from the cell every frame. Advances and returns `y`.
|
|
static int editRow(EditPanelState& ed, EditField valueId, const std::string& label, const std::string& value,
|
|
EditField lockId, bool locked, int x, int y, int w) {
|
|
Rectangle r{ (float)x, (float)y, (float)(w - 72), 22.0f };
|
|
bool active = (ed.editingField == valueId);
|
|
DrawRectangleRec(r, active ? Color{40, 46, 64, 255} : Color{22, 24, 34, 255});
|
|
DrawRectangleLinesEx(r, 1, Color{70, 74, 90, 255});
|
|
DrawText(label.c_str(), (int)r.x + 4, (int)r.y + 3, 14, Color{190, 195, 210, 255});
|
|
std::string val = active ? (ed.editBuffer + "_") : value;
|
|
int vw = MeasureText(val.c_str(), 14);
|
|
DrawText(val.c_str(), (int)(r.x + r.width) - vw - 6, (int)r.y + 3, 14, active ? Color{255, 230, 140, 255} : RAYWHITE);
|
|
ed.fieldRects.push_back(r); ed.fieldIds.push_back(valueId);
|
|
|
|
Rectangle lr{ r.x + r.width + 4, r.y, 64.0f, 22.0f };
|
|
DrawRectangleRec(lr, locked ? Color{120, 60, 50, 255} : Color{34, 50, 40, 255});
|
|
DrawRectangleLinesEx(lr, 1, Color{100, 104, 120, 255});
|
|
const char* t = locked ? "LOCKED" : "lock";
|
|
int tw = MeasureText(t, 12);
|
|
DrawText(t, (int)(lr.x + lr.width / 2 - tw / 2), (int)lr.y + 5, 12, locked ? Color{255, 210, 200, 255} : Color{190, 220, 195, 255});
|
|
ed.lockRects.push_back(lr); ed.lockIds.push_back(lockId);
|
|
return y + 24;
|
|
}
|
|
|
|
void drawEditPanel(const Planet& p, int cellIdx, Rectangle panel, EditPanelState& ed) {
|
|
ed.tabRects.clear(); ed.fieldRects.clear(); ed.fieldIds.clear();
|
|
ed.lockRects.clear(); ed.lockIds.clear();
|
|
ed.pickerRects.clear(); ed.pickerValues.clear();
|
|
ed.removeRects.clear(); ed.removeKind.clear(); ed.removeIndex.clear();
|
|
if (cellIdx < 0 || cellIdx >= (int)p.cells.size()) return;
|
|
|
|
DrawRectangleRec(panel, Color{12, 14, 22, 235});
|
|
DrawRectangleLinesEx(panel, 2, Color{210, 150, 70, 255}); // orange border marks edit mode
|
|
int x = (int)panel.x + 10, y = (int)panel.y + 8;
|
|
int w = (int)panel.width - 20;
|
|
int maxY = (int)(panel.y + panel.height) - 8;
|
|
DrawText(TextFormat("EDIT MODE - Tile #%d", cellIdx), x, y, 18, Color{255, 200, 120, 255});
|
|
const char* hint = "F3: exit";
|
|
DrawText(hint, (int)(panel.x + panel.width) - MeasureText(hint, 13) - 10, y + 3, 13, Color{170, 170, 185, 255});
|
|
y += 26;
|
|
|
|
const Cell& c = p.cells[cellIdx];
|
|
bool hasSettle = p.settlementsPlaced() && cellIdx < (int)p.cellSettlement().size() && p.cellSettlement()[cellIdx] >= 0;
|
|
|
|
const char* tabNames[4] = { "Geology", "Climate", "Biota", "Settlement" };
|
|
int nTabs = hasSettle ? 4 : 3;
|
|
if (ed.tab >= nTabs) ed.tab = 0;
|
|
int tabW = w / nTabs;
|
|
for (int i = 0; i < nTabs; ++i) {
|
|
Rectangle tr{ (float)(x + i * tabW), (float)y, (float)(tabW - 2), 24.0f };
|
|
bool on = (ed.tab == i);
|
|
DrawRectangleRec(tr, on ? Color{60, 66, 92, 255} : Color{28, 30, 42, 255});
|
|
DrawRectangleLinesEx(tr, 1, Color{90, 94, 115, 255});
|
|
int tw = MeasureText(tabNames[i], 14);
|
|
DrawText(tabNames[i], (int)(tr.x + tr.width / 2 - tw / 2), (int)tr.y + 5, 14, on ? RAYWHITE : Color{170, 175, 190, 255});
|
|
ed.tabRects.push_back(tr);
|
|
}
|
|
y += 30;
|
|
|
|
// An open picker list replaces the row list for this tab until a row (or the tab strip) is clicked.
|
|
if (ed.pickerField != EditField::None) {
|
|
DrawText("Select a value (click a tab to cancel):", x, y, 13, Color{190, 195, 210, 255}); y += 20;
|
|
auto pickRow = [&](const std::string& label, int value) {
|
|
if (y + 22 > maxY) return;
|
|
Rectangle r{ (float)x, (float)y, (float)w, 22.0f };
|
|
DrawRectangleLinesEx(r, 1, Color{70, 74, 90, 255});
|
|
DrawText(label.c_str(), x + 4, y + 3, 14, RAYWHITE);
|
|
ed.pickerRects.push_back(r); ed.pickerValues.push_back(value);
|
|
y += 24;
|
|
};
|
|
switch (ed.pickerField) {
|
|
case EditField::PlateId:
|
|
for (int pi = 0; pi < (int)p.plates.size(); ++pi) {
|
|
if (p.plates[pi].baby) continue;
|
|
pickRow(TextFormat("Plate %d (%s)", pi, p.plates[pi].type == PlateType::Oceanic ? "Oceanic" : "Continental"), pi);
|
|
}
|
|
break;
|
|
case EditField::Biome:
|
|
for (int b = 0; b <= (int)Biome::Mountains; ++b) pickRow(biomeName((Biome)b), b);
|
|
break;
|
|
case EditField::Allegiance:
|
|
pickRow("Independent", -1);
|
|
for (size_t k = 0; k < p.settlements.size(); ++k)
|
|
if (p.settlements[k].population >= p.cfg.civAbandonPop) pickRow(p.settlements[k].name, (int)k);
|
|
break;
|
|
case EditField::Culture:
|
|
pickRow("None", -1);
|
|
for (size_t k = 0; k < p.cultureList().size(); ++k) pickRow(p.cultureList()[k].name, (int)k);
|
|
break;
|
|
case EditField::AddFlora: case EditField::AddFauna: case EditField::AddFunga: {
|
|
BiotaKind kind = ed.pickerField == EditField::AddFlora ? BiotaKind::Flora
|
|
: ed.pickerField == EditField::AddFauna ? BiotaKind::Fauna : BiotaKind::Funga;
|
|
const auto& AR = biotaArchetypes();
|
|
for (int a = 0; a < (int)AR.size(); ++a) if (AR[a].kind == kind) pickRow(AR[a].name, a);
|
|
break;
|
|
}
|
|
default: break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
uint16_t lock = c.editLock;
|
|
if (ed.tab == 0) { // Geology
|
|
static const Plate kUnknown{};
|
|
const Plate& pl = (c.plateId >= 0 && c.plateId < (int)p.plates.size()) ? p.plates[c.plateId] : kUnknown;
|
|
y = editRow(ed, EditField::Elevation, "Elevation", TextFormat("%.0f m", c.elevation),
|
|
EditField::LockElevation, (lock & LockElevation) != 0, x, y, w);
|
|
y = editRow(ed, EditField::PlateId, "Plate", TextFormat("P%d (%s)", c.plateId, pl.type == PlateType::Oceanic ? "Oceanic" : "Continental"),
|
|
EditField::LockPlate, (lock & LockPlate) != 0, x, y, w);
|
|
y = editRow(ed, EditField::Crust, "Crust type", c.oceanic ? "Oceanic" : "Continental",
|
|
EditField::LockCrust, (lock & LockCrust) != 0, x, y, w);
|
|
y = editRow(ed, EditField::GeoAge, "Geo age", TextFormat("%.0f My", c.geoAge),
|
|
EditField::LockCrust, (lock & LockCrust) != 0, x, y, w);
|
|
y = editRow(ed, EditField::Biome, "Biome", biomeName(c.biome),
|
|
EditField::LockBiome, (lock & LockBiome) != 0, x, y, w);
|
|
} else if (ed.tab == 1) { // Climate
|
|
double temp = (cellIdx < (int)p.temperature().size()) ? p.temperature()[cellIdx] : 0.0;
|
|
double moist = (cellIdx < (int)p.moisture().size()) ? p.moisture()[cellIdx] : 0.0;
|
|
y = editRow(ed, EditField::Temperature, "Temperature", TextFormat("%.1f C", temp),
|
|
EditField::LockClimate, (lock & LockClimate) != 0, x, y, w);
|
|
y = editRow(ed, EditField::Moisture, "Moisture", TextFormat("%.0f%%", moist * 100.0),
|
|
EditField::LockClimate, (lock & LockClimate) != 0, x, y, w);
|
|
} else if (ed.tab == 2) { // Biota
|
|
double fl = (cellIdx < (int)p.floraDensity().size()) ? p.floraDensity()[cellIdx] : 0.0;
|
|
double fa = (cellIdx < (int)p.faunaDensity().size()) ? p.faunaDensity()[cellIdx] : 0.0;
|
|
double fu = (cellIdx < (int)p.fungaDensity().size()) ? p.fungaDensity()[cellIdx] : 0.0;
|
|
double hab = (cellIdx < (int)p.habitability().size()) ? p.habitability()[cellIdx] : 0.0;
|
|
y = editRow(ed, EditField::FloraDensity, "Flora density", TextFormat("%.0f%%", fl * 100.0),
|
|
EditField::LockBiota, (lock & LockBiota) != 0, x, y, w);
|
|
y = editRow(ed, EditField::FaunaDensity, "Fauna density", TextFormat("%.0f%%", fa * 100.0),
|
|
EditField::LockBiota, (lock & LockBiota) != 0, x, y, w);
|
|
y = editRow(ed, EditField::FungaDensity, "Funga density", TextFormat("%.0f%%", fu * 100.0),
|
|
EditField::LockBiota, (lock & LockBiota) != 0, x, y, w);
|
|
y = editRow(ed, EditField::Habitability, "Habitability", TextFormat("%.0f%%", hab * 100.0),
|
|
EditField::LockHabitability, (lock & LockHabitability) != 0, x, y, w);
|
|
y += 8;
|
|
auto listOrganisms = [&](const char* tag, const std::vector<Organism>& v, BiotaKind kind, EditField addId) {
|
|
if (y + 18 > maxY) return;
|
|
DrawText(tag, x, y, 13, Color{170, 175, 190, 255});
|
|
Rectangle ar{ (float)(x + w - 48), (float)y - 2, 48.0f, 18.0f };
|
|
DrawRectangleRec(ar, Color{34, 50, 40, 255}); DrawRectangleLinesEx(ar, 1, Color{100, 104, 120, 255});
|
|
DrawText("+ add", (int)ar.x + 3, (int)ar.y + 3, 11, Color{190, 220, 195, 255});
|
|
ed.fieldRects.push_back(ar); ed.fieldIds.push_back(addId);
|
|
y += 18;
|
|
for (size_t oi = 0; oi < v.size() && y + 16 <= maxY; ++oi) {
|
|
std::string nm = organismName(v[oi]);
|
|
DrawText(nm.c_str(), x + 10, y, 12, Color{200, 205, 215, 255});
|
|
Rectangle xr{ (float)(x + w - 20), (float)y - 1, 18.0f, 16.0f };
|
|
DrawRectangleRec(xr, Color{60, 30, 30, 255}); DrawRectangleLinesEx(xr, 1, Color{100, 60, 60, 255});
|
|
DrawText("x", (int)xr.x + 6, (int)xr.y + 1, 12, Color{230, 180, 180, 255});
|
|
ed.removeRects.push_back(xr); ed.removeKind.push_back((int)kind); ed.removeIndex.push_back((int)oi);
|
|
y += 16;
|
|
}
|
|
};
|
|
if (p.biotaPopulated() && cellIdx < (int)p.biota().size()) {
|
|
const CellBiota& cb = p.biota()[cellIdx];
|
|
listOrganisms("Flora population", cb.flora, BiotaKind::Flora, EditField::AddFlora);
|
|
listOrganisms("Fauna population", cb.fauna, BiotaKind::Fauna, EditField::AddFauna);
|
|
listOrganisms("Funga population", cb.funga, BiotaKind::Funga, EditField::AddFunga);
|
|
} else {
|
|
DrawText("(press L to generate a biota population first)", x, y, 13, Color{150, 150, 165, 255});
|
|
}
|
|
} else if (ed.tab == 3 && hasSettle) { // Settlement
|
|
int si = p.cellSettlement()[cellIdx];
|
|
const Settlement& s = p.settlements[si];
|
|
int ov = (si < (int)p.settleAllegiance().size()) ? p.settleAllegiance()[si] : -1;
|
|
std::string ovName = (ov >= 0 && ov < (int)p.settlements.size()) ? p.settlements[ov].name : "Independent";
|
|
int ci = (si < (int)p.settleCulture().size()) ? p.settleCulture()[si] : -1;
|
|
std::string ciName = (ci >= 0 && ci < (int)p.cultureList().size()) ? p.cultureList()[ci].name : "None";
|
|
y = editRow(ed, EditField::Population, "Population", TextFormat("%.0f", s.population),
|
|
EditField::LockPopulation, (lock & LockPopulation) != 0, x, y, w);
|
|
y = editRow(ed, EditField::Allegiance, "Allegiance", ovName,
|
|
EditField::LockAllegiance, (lock & LockAllegiance) != 0, x, y, w);
|
|
y = editRow(ed, EditField::Culture, "Culture", ciName,
|
|
EditField::LockCulture, (lock & LockCulture) != 0, x, y, w);
|
|
}
|
|
}
|
|
|
|
void drawStats(const Planet& p, Rectangle r, double elapsedMy, bool drifting,
|
|
bool live, double liveHours) {
|
|
DrawRectangleRec(r, Color{12, 14, 22, 235});
|
|
DrawRectangleLinesEx(r, 1, Color{120, 120, 150, 255});
|
|
int x = (int)r.x + 16, y = (int)r.y + 12;
|
|
DrawText("World statistics", x, y, 22, RAYWHITE); y += 38;
|
|
|
|
int N = (int)p.cells.size(), np = (int)p.plates.size();
|
|
double Rkm = p.cfg.radius / 1000.0;
|
|
double surfKm2 = 4.0 * M_PI * Rkm * Rkm, cellKm2 = surfKm2 / std::max(1, N);
|
|
std::vector<int> pc(np, 0), pl(np, 0);
|
|
double seaLvl = p.cfg.seaLevel;
|
|
int landGeo = 0, cont = 0; double mn = 1e30, mx = -1e30, sum = 0;
|
|
for (const auto& c : p.cells) {
|
|
if (c.plateId >= 0 && c.plateId < np) { pc[c.plateId]++; if (!c.oceanic) pl[c.plateId]++; }
|
|
if (c.elevation > seaLvl) ++landGeo; // geographic land (above sea level)
|
|
if (!c.oceanic) ++cont; // continental crust
|
|
mn = std::min(mn, c.elevation); mx = std::max(mx, c.elevation); sum += c.elevation;
|
|
}
|
|
// Real plates vs. baby (young spreading-ridge) plates are counted separately.
|
|
int contPlates = 0, used = 0, babyPlates = 0, babyCells = 0;
|
|
for (int q = 0; q < np; ++q) {
|
|
if (pc[q] == 0) continue;
|
|
if (p.plates[q].baby) { ++babyPlates; babyCells += pc[q]; continue; }
|
|
++used;
|
|
if (pl[q] * 2 > pc[q]) ++contPlates;
|
|
}
|
|
|
|
auto L = [&](const char* s) { DrawText(s, x, y, 17, Color{210, 215, 225, 255}); y += 23; };
|
|
L(TextFormat("Cells: %d cell area %.1fk km2 R %.0f km", N, cellKm2 / 1000.0, Rkm));
|
|
L(TextFormat("Surface area: %.0f M km2", surfKm2 / 1.0e6));
|
|
L(TextFormat("Plates: %d active %d continental / %d oceanic", used, contPlates, used - contPlates));
|
|
L(TextFormat("Young ridges: %d strips %d cells", babyPlates, babyCells));
|
|
L(TextFormat("Moons: %d", (int)p.getMoons().size()));
|
|
int water = N - landGeo;
|
|
double wlRatio = landGeo > 0 ? (double)water / landGeo : 0.0;
|
|
L(TextFormat("Land %.0f%% Ocean %.0f%% (water:land %.2f:1)",
|
|
100.0 * landGeo / N, 100.0 * water / N, wlRatio));
|
|
L(TextFormat("Sea level: %+.0f m", seaLvl));
|
|
L(TextFormat("Crust: %.0f%% continental / %.0f%% oceanic", 100.0 * cont / N, 100.0 * (N - cont) / N));
|
|
L(TextFormat("Elevation: %.0f .. %.0f m mean %.0f m", mn, mx, sum / N));
|
|
if (live) L(TextFormat("Live World: year %ld, day %.1f",
|
|
(long)(liveHours / p.cfg.dayLengthHours / p.cfg.yearLengthDays) + 1,
|
|
std::fmod(liveHours / p.cfg.dayLengthHours, p.cfg.yearLengthDays)));
|
|
else if (drifting) L(TextFormat("Sim time: %.0f My", elapsedMy));
|
|
y += 8;
|
|
DrawText("plate cells size area speed land", x, y, 15, Color{150, 155, 170, 255}); y += 21;
|
|
|
|
std::vector<int> idx(np); for (int q = 0; q < np; ++q) idx[q] = q;
|
|
std::sort(idx.begin(), idx.end(), [&](int a, int b){ return pc[a] > pc[b]; });
|
|
int rows = 0, rowMax = 13;
|
|
for (int q : idx) {
|
|
if (pc[q] == 0 || p.plates[q].baby) continue; // baby ridges summarised above
|
|
if (rows++ >= rowMax) break;
|
|
const char* ty = (pl[q] * 2 > pc[q]) ? "cont" : "ocn ";
|
|
DrawText(TextFormat("P%-2d %s %5dc %4.1f%% %6.1fM %4.1fcm/y %3.0f%%", q, ty, pc[q],
|
|
100.0 * pc[q] / N, pc[q] * cellKm2 / 1.0e6, p.plates[q].speedCmYr, 100.0 * pl[q] / pc[q]),
|
|
x, y, 16, Color{200, 205, 220, 255});
|
|
y += 21;
|
|
}
|
|
DrawText("click a tile to inspect its subtiles",
|
|
x, (int)(r.y + r.height) - 24, 14, Color{150, 150, 165, 255});
|
|
}
|