diff --git a/BUILD.md b/BUILD.md index fe5f51b..ccd56fc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -59,6 +59,7 @@ the full ~2.8x speedup; the default uses all cores for no extra gain: U settlements: the dawn of civilization on first press, then toggle markers (Civ tab) P territory / realms view + borders + war fronts + realm-name labels (Realms tab) A toggle alliance/rivalry diplomacy arcs on the Territory view (on by default) + Q realm borders + realm-name/culture labels over any colour mode (off by default) X culture / faiths view + cultural borders (peoples/ethos/religion in the Cultures tab) Z wealth / trade view + trade routes (sea/river/overland; prosperity feeds city growth) Y follow-cam: cycle the 3D camera through active storms (Live World; off after last) diff --git a/CLAUDE.md b/CLAUDE.md index f44e6c6..c62773a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -422,6 +422,30 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived "Alliances" checkbox in Overlays), `&&`-ed into just the ally/rival arc draw calls (3D + 2D) so political borders/war fronts still show under Territory regardless — Territory now toggles cleanly with or without the arcs. `src/render` only, no save-format change. +- **Realm borders + culture annotation over any colour mode** *(done)* — `showNationBorders()` + (borders/realm-name labels/war fronts/diplomacy arcs) was Territory-mode-only, so there was no way + to see political borders and realm names layered over e.g. the **Biome** view without switching + colour modes and losing the terrain-type coloring the user actually wanted to look at. Added an + independent `showRealms` bool (default off, key **`Q`**, a "Realm borders" toggle in Civilization & + Ecology — like Territory/Culture/Trade it needs settlement/territory data built first, hence + shadow+method not a plain checkbox) that's `||`-ed with `showNationBorders()` at just the + political-border-segment and realm-name-label draw sites (3D, 2D, and `exportMapImage()` which + shares the 2D path) — `mode`/`vcolors`/`recolor()` are never touched, so this overlay draws lines + and text *over* whatever colour mode is active without tinting a single cell. War fronts and + diplomacy arcs deliberately stay `showNationBorders()`-only so the combined view doesn't also drag + in Territory-only clutter. Realm-name labels everywhere they're drawn (Territory view, this new + toggle, and the always-on atlas) now also show the realm's dominant culture as a smaller + `(the Velmar)`-style line stacked directly above the capital marker, via `Nation.cultureId → + planet.cultureList()` (skipped gracefully when absent). `exportAtlasImage()` forces `showRealms` on + for the duration of its export (same save/restore pattern already used for `showSettlements`) and + adds the culture line as its own lower-priority label candidate in the decluttered pass, anchored + just below each realm's own label so it drops out gracefully on a dense world via the existing + collision check — no special-casing needed. `AtlasLabel::text` changed from `const std::string*` to + an owned `std::string` since the culture strings are built on the fly rather than backed by an + existing stable container. Deliberately **not** reset in `regenWorld()` — like `showDiplomacy`, its + underlying segment/nation lists are already cleared on reseed, so nothing stale can render; only + `mode` needs resetting there. `src/render` only, no save-format or `src/sim` change; ctest + unaffected. - **Toolbar: a direct one-click "Biome view" shortcut** *(done)* — during Tectonics/Hydrology (before any Civilization & Ecology feature is even relevant) the only way to see the world's actual terrain type (deserts/forests/tundra/etc., colour mode `Biome`) was to open the 13-entry View Mode dropdown @@ -958,6 +982,8 @@ all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured co `P` territory / realms view + political borders + realm-name labels (Realms tab lists nations) · `A` toggle the alliance/rivalry diplomacy arcs on the Territory view (on by default; political borders/war fronts stay regardless) · +`Q` toggle political realm borders + realm-name (+dominant-culture) labels over **any** colour mode, +not just Territory (independent of `P`; off by default) · `X` culture / faiths view + cultural borders (Cultures tab lists peoples, ethos & religion) · `Z` wealth / trade view + trade routes (sea/river/overland; prosperity feeds city growth) · `SPACE` or on-screen button pause · diff --git a/src/render/Toolbar.cpp b/src/render/Toolbar.cpp index 4ac17d1..48cc952 100644 --- a/src/render/Toolbar.cpp +++ b/src/render/Toolbar.cpp @@ -214,6 +214,7 @@ void drawToolbar(Viewer& v) { 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), "Realm borders (Q)", v.showRealms, &Viewer::toggleRealmOverlay, 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(); diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index b70d7c7..c55309a 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -935,6 +935,14 @@ void Viewer::toggleTradeView() { // Z: wealth/trade colour view + setStatus(mode == ColorMode::Wealth ? "Wealth / trade on" : "Wealth off"); } +void Viewer::toggleRealmOverlay() { // Q: realm borders + names (+culture) over any colour mode + 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(); + showRealms = !showRealms; + setStatus(showRealms ? "Realm borders on" : "Realm borders off"); +} + void Viewer::placeOrToggleSettlements() { // U: seed on first press ("the dawn"), then toggle markers if (!settled) return; if (!planet.settlementsPlaced()) { diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index bc8dede..2abcab8 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -125,6 +125,13 @@ struct Viewer { 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 { @@ -240,6 +247,7 @@ struct Viewer { 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 diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index 06e9fb8..03eca0f 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -204,6 +204,7 @@ void Viewer::handleInput() { if (IsKeyPressed(KEY_I)) toggleHabitabilityView(); if (IsKeyPressed(KEY_U)) placeOrToggleSettlements(); if (IsKeyPressed(KEY_P)) toggleTerritoryView(); + if (IsKeyPressed(KEY_Q)) toggleRealmOverlay(); if (IsKeyPressed(KEY_X)) toggleCultureView(); if (IsKeyPressed(KEY_Z)) toggleTradeView(); if (IsKeyPressed(KEY_W)) enterOrLeaveLiveWorld(); diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index 33e5abc..69c70eb 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -84,7 +84,7 @@ void Viewer::renderGlobe3D() { } rlEnd(); rlSetLineWidth(1.0f); } - if (showNationBorders() && !nationBorders.empty()) { // political / realm borders (dark, over the tint) + if ((showNationBorders() || showRealms) && !nationBorders.empty()) { // political / realm borders (dark, over the tint) rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(18, 18, 26, 235); for (size_t i = 0; i + 1 < nationBorders.size(); i += 2) { rlVertex3f(nationBorders[i].x, nationBorders[i].y, nationBorders[i].z); @@ -391,7 +391,7 @@ void Viewer::drawMapOverlays(Rectangle vr, double lonOffset, float scale, float if (showGrat) { drawGraticule2D(graticule, vr, lonOffset); drawGraticuleLabels2D(vr, lonOffset); } if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f * scale, vr, lonOffset); if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f * scale, vr, lonOffset); - if (showNationBorders() && !nationBorders.empty()) drawSegments2D(nationBorders, Color{18, 18, 26, 235}, 2.0f * scale, vr, lonOffset); + if ((showNationBorders() || showRealms) && !nationBorders.empty()) drawSegments2D(nationBorders, Color{18, 18, 26, 235}, 2.0f * scale, vr, lonOffset); if (showCultureBorders() && !cultureBorders.empty()) drawSegments2D(cultureBorders, Color{245, 240, 220, 230}, 2.5f * scale, vr, lonOffset); if (showNationBorders() && !warFrontier.empty()) drawSegments2D(warFrontier, Color{235, 40, 30, 255}, 2.5f * scale, vr, lonOffset); if (showNationBorders() && showDiplomacy && !allyLinks.empty()) drawSegments2D(allyLinks, Color{70, 220, 120, 220}, 1.5f * scale, vr, lonOffset); @@ -464,10 +464,12 @@ void Viewer::drawMapOverlays(Rectangle vr, double lonOffset, float scale, float DrawText(txt, (int)(lp.x + 4 * scale), (int)(lp.y - 8 * scale), fs, RAYWHITE); } } - // Realm labels (with the territory view): name kingdoms/empires at their capital. Mirrors the - // 3D realm-label block in renderFrame() -- previously 2D-only had none, so neither the on-screen - // map nor exportMapImage() ever showed realm names, only the 3D globe did. - if (drawLabels && showNationBorders() && !planet.nationList().empty()) { + // Realm labels (with the territory view, or the independent showRealms overlay over any colour + // mode): name kingdoms/empires at their capital, with the realm's dominant culture as a smaller + // "(the Velmar)" line stacked just above the marker. Mirrors the 3D realm-label block in + // renderFrame() -- previously 2D-only had none, so neither the on-screen map nor + // exportMapImage() ever showed realm names, only the 3D globe did. + if (drawLabels && (showNationBorders() || showRealms) && !planet.nationList().empty()) { for (const Nation& nat : planet.nationList()) { if (nat.tier == NationTier::CityState) continue; // declutter: only multi-settlement realms if (nat.capital < 0 || nat.capital >= (int)planet.settlements.size()) continue; @@ -478,9 +480,21 @@ void Viewer::drawMapOverlays(Rectangle vr, double lonOffset, float scale, float double lon, lat; dirToLonLat(planet.cells[cell].unit, lon, lat); Vector2 lp = projLonLat(lon, lat, lonOffset, vr); if (!CheckCollisionPointRec(lp, vr)) continue; + int cultureFont = std::max(8, font - 4); + int nameFillY = (int)lp.y - font - cultureFont - 8 - 2; int w = MeasureText(nat.name.c_str(), font); - DrawText(nat.name.c_str(), (int)lp.x - w / 2 + 1, (int)lp.y - font - 7, font, Color{0, 0, 0, 205}); - DrawText(nat.name.c_str(), (int)lp.x - w / 2, (int)lp.y - font - 8, font, Color{245, 235, 210, 255}); + DrawText(nat.name.c_str(), (int)lp.x - w / 2 + 1, nameFillY + 1, font, Color{0, 0, 0, 205}); + DrawText(nat.name.c_str(), (int)lp.x - w / 2, nameFillY, font, Color{245, 235, 210, 255}); + if (nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size()) { + const std::string& cname = planet.cultureList()[nat.cultureId].name; + if (!cname.empty()) { + std::string ctext = "(" + cname + ")"; + int cw = MeasureText(ctext.c_str(), cultureFont); + int cFillY = (int)lp.y - cultureFont - 8; + DrawText(ctext.c_str(), (int)lp.x - cw / 2 + 1, cFillY + 1, cultureFont, Color{0, 0, 0, 205}); + DrawText(ctext.c_str(), (int)lp.x - cw / 2, cFillY, cultureFont, Color{215, 205, 180, 235}); + } + } } } // Place-name labels (the atlas). Minor features only when the map is zoomed in (on-screen) or @@ -579,11 +593,14 @@ void Viewer::exportAtlasImage() { // happens to be. drawMap2D(planet, vcolors, map2D, er, mapLon); bool savedSettlements = showSettlements; + bool savedRealms = showRealms; showSettlements = true; + showRealms = true; // the atlas always shows realm borders, regardless of the live toggle drawMapOverlays(er, mapLon, scale, scale, /*showMinorLabels=*/true, /*drawLabels=*/false); showSettlements = savedSettlements; + showRealms = savedRealms; - struct AtlasLabel { Vector2 pos; const std::string* text; int font; Color col; int priority; float sortKey; bool dot; }; + struct AtlasLabel { Vector2 pos; std::string text; int font; Color col; int priority; float sortKey; bool dot; }; std::vector labels; if (planet.geographyBuilt()) { for (const GeoFeature& f : planet.geography()) { @@ -598,7 +615,7 @@ void Viewer::exportAtlasImage() { } double lon, lat; dirToLonLat(planet.cells[f.anchorCell].unit, lon, lat); Vector2 lp = projLonLat(lon, lat, mapLon, er); - labels.push_back({ lp, &f.name, (int)std::round(font * labelScale), col, prio, (float)f.size, dot }); + labels.push_back({ lp, f.name, (int)std::round(font * labelScale), col, prio, (float)f.size, dot }); } } // Realm names (kingdoms/empires), regardless of the current colour mode -- an atlas is a @@ -616,7 +633,20 @@ void Viewer::exportAtlasImage() { double lon, lat; dirToLonLat(planet.cells[cell].unit, lon, lat); Vector2 lp = projLonLat(lon, lat, mapLon, er); lp.y -= font * 0.9f; - labels.push_back({ lp, &nat.name, font, Color{245, 235, 210, 255}, prio, (float)nat.totalPop, false }); + labels.push_back({ lp, nat.name, font, Color{245, 235, 210, 255}, prio, (float)nat.totalPop, false }); + // The realm's dominant culture, as a smaller "(the Velmar)" label anchored just below the + // realm name (still above the raw capital point) -- a lower-priority candidate in the same + // greedy pass, so on a dense world it simply drops out on its own if it doesn't fit, + // without any special-case logic. + if (nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size()) { + const std::string& cname = planet.cultureList()[nat.cultureId].name; + if (!cname.empty()) { + int cfont = std::max(8, (int)std::round(font * 0.72f)); + Vector2 clp = lp; clp.y += font * 0.62f; + labels.push_back({ clp, "(" + cname + ")", cfont, Color{215, 205, 180, 235}, + prio + 1, (float)nat.totalPop, false }); + } + } } } if (!planet.settlements.empty()) { @@ -631,7 +661,7 @@ void Viewer::exportAtlasImage() { : Color{210, 130, 85, 255}; double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat); Vector2 lp = projLonLat(lon, lat, mapLon, er); - labels.push_back({ lp, &s.name, (int)std::round(font * labelScale), col, prio, (float)s.population, false }); + labels.push_back({ lp, s.name, (int)std::round(font * labelScale), col, prio, (float)s.population, false }); } } // Major features (low priority number) and, within a tier, larger/more populous ones claim @@ -643,8 +673,8 @@ void Viewer::exportAtlasImage() { std::vector placed; const float pad = 3.0f; for (const auto& lb : labels) { - if (lb.font <= 0 || lb.text->empty() || !CheckCollisionPointRec(lb.pos, er)) continue; - int w = MeasureText(lb.text->c_str(), lb.font); + if (lb.font <= 0 || lb.text.empty() || !CheckCollisionPointRec(lb.pos, er)) continue; + int w = MeasureText(lb.text.c_str(), lb.font); Rectangle box{ lb.pos.x - w * 0.5f - pad, lb.pos.y - lb.font * 0.5f - pad, (float)w + pad * 2, (float)lb.font + pad * 2 }; bool overlap = false; @@ -653,8 +683,8 @@ void Viewer::exportAtlasImage() { placed.push_back(box); if (lb.dot) DrawCircleV(lb.pos, 2.5f, lb.col); int tx = (int)(lb.pos.x - w * 0.5f), ty = (int)(lb.pos.y - lb.font * 0.5f); - DrawText(lb.text->c_str(), tx + 1, ty + 1, lb.font, Color{0, 0, 0, 190}); - DrawText(lb.text->c_str(), tx, ty, lb.font, lb.col); + DrawText(lb.text.c_str(), tx + 1, ty + 1, lb.font, Color{0, 0, 0, 190}); + DrawText(lb.text.c_str(), tx, ty, lb.font, lb.col); } EndTextureMode(); @@ -1226,8 +1256,10 @@ void Viewer::renderFrame() { t == SettleTier::City ? Color{250, 230, 150, 255} : Color{225, 210, 175, 255}); } } - // 3D realm labels (with the territory view): name kingdoms/empires at their capital. - if (showNationBorders() && !planet.nationList().empty()) { + // 3D realm labels (with the territory view, or the independent showRealms overlay over any + // colour mode): name kingdoms/empires at their capital, with the realm's dominant culture as a + // smaller "(the Velmar)" line stacked just above the marker. + if ((showNationBorders() || showRealms) && !planet.nationList().empty()) { Vec3 camPos{cam.position.x, cam.position.y, cam.position.z}; Vec3 forward = (Vec3{cam.target.x, cam.target.y, cam.target.z} - camPos).normalized(); Vec3 right = forward.cross(Vec3{cam.up.x, cam.up.y, cam.up.z}).normalized(); @@ -1246,9 +1278,21 @@ void Viewer::renderFrame() { Vec3 rel = lp - camPos; double z = rel.dot(forward); if (z <= 0.0) continue; float sx = (float)((rel.dot(right) / (projW * z) * 0.5 + 0.5) * view3DW); float sy = (float)((0.5 - rel.dot(up) / (projH * z) * 0.5) * view3DH); + int cultureFont = std::max(8, font - 4); + int nameFillY = (int)sy - font - cultureFont - 8 - 2; int w = MeasureText(nat.name.c_str(), font); - DrawText(nat.name.c_str(), (int)sx - w / 2 + 1, (int)sy - font - 7, font, Color{0, 0, 0, 205}); - DrawText(nat.name.c_str(), (int)sx - w / 2, (int)sy - font - 8, font, Color{245, 235, 210, 255}); + DrawText(nat.name.c_str(), (int)sx - w / 2 + 1, nameFillY + 1, font, Color{0, 0, 0, 205}); + DrawText(nat.name.c_str(), (int)sx - w / 2, nameFillY, font, Color{245, 235, 210, 255}); + if (nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size()) { + const std::string& cname = planet.cultureList()[nat.cultureId].name; + if (!cname.empty()) { + std::string ctext = "(" + cname + ")"; + int cw = MeasureText(ctext.c_str(), cultureFont); + int cFillY = (int)sy - cultureFont - 8; + DrawText(ctext.c_str(), (int)sx - cw / 2 + 1, cFillY + 1, cultureFont, Color{0, 0, 0, 205}); + DrawText(ctext.c_str(), (int)sx - cw / 2, cFillY, cultureFont, Color{215, 205, 180, 235}); + } + } } }