#include "Panels.hpp" #include "Colors.hpp" // elevationColor (subtile grid) #include "PlanetBiota.hpp" // organismName / sizeName / roleName #include "Projection.hpp" // dirToLonLat #include #include #include #include #include // 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 cellInfo(const Planet& p, int i, double elev, double age) { const Cell& c = p.cells[i]; double lon, lat; dirToLonLat(c.unit, lon, lat); const Plate& pl = p.plates[c.plateId]; const int n = (int)p.cells.size(); auto sized = [&](const std::vector& v) { return (int)v.size() == n; }; std::vector 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& 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 (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"))); } } } // 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")); } } // 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& v) { if (v.empty()) return; std::vector> 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(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& 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; } } 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 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 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}); }