Add realm borders + culture annotation overlay, combinable with any colour mode

Political realm borders and realm-name labels were Territory-mode-only
(showNationBorders() == mode==Territory), so there was no way to see borders
and realm names layered over e.g. the Biome view without losing the
terrain-type coloring. Added an independent showRealms bool (key Q, a "Realm
borders" toggle in Civilization & Ecology) OR-ed with showNationBorders() at
just the border-line and realm-label draw sites (3D, 2D, and exportMapImage()
which shares the 2D path) -- mode/vcolors/recolor() are never touched, so
this overlay draws over any active colour mode without tinting cells. War
fronts and diplomacy arcs stay showNationBorders()-only on purpose, so the
combined view doesn't drag in Territory-only clutter.

Realm-name labels everywhere they're drawn (Territory view, this new toggle,
and the always-on atlas export) now also show the realm's dominant culture
as a smaller "(the Velmar)"-style line via Nation.cultureId ->
planet.cultureList(). exportAtlasImage() forces showRealms on for the
duration of its export (same pattern already used for showSettlements) and
adds the culture line as its own lower-priority label candidate in the
decluttered pass, anchored below each realm's own label so it drops out
gracefully on a dense world via the existing collision check.
AtlasLabel::text changed from a raw pointer to an owned std::string since the
culture strings are built on the fly.

Not reset in regenWorld() -- like showDiplomacy, its underlying nation/
segment lists are already cleared on reseed, so nothing stale can render.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
This commit is contained in:
Jonas Reith 2026-08-30 21:20:35 +02:00
parent cf97ae668d
commit dae6981dc9
7 changed files with 109 additions and 20 deletions

View File

@ -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) 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) 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) 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) 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) 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) Y follow-cam: cycle the 3D camera through active storms (Live World; off after last)

View File

@ -422,6 +422,30 @@ on the Live World clock). **Steps 17 of the roadmap are done (plus a derived
"Alliances" checkbox in Overlays), `&&`-ed into just the ally/rival arc draw calls (3D + 2D) so "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 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. 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 - **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 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 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) · `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 `A` toggle the alliance/rivalry diplomacy arcs on the Territory view (on by default; political
borders/war fronts stay regardless) · 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) · `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) · `Z` wealth / trade view + trade routes (sea/river/overland; prosperity feeds city growth) ·
`SPACE` or on-screen button pause · `SPACE` or on-screen button pause ·

View File

@ -214,6 +214,7 @@ void drawToolbar(Viewer& v) {
toggleAction(takeRow(cx, cw, cy), v.planet.settlementsPlaced() ? "Settlements (U)" : "Found settlements (U)", toggleAction(takeRow(cx, cw, cy), v.planet.settlementsPlaced() ? "Settlements (U)" : "Found settlements (U)",
v.showSettlements && v.planet.settlementsPlaced(), &Viewer::placeOrToggleSettlements, v); 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), "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), "Culture (X)", v.mode == ColorMode::Culture, &Viewer::toggleCultureView, v);
toggleAction(takeRow(cx, cw, cy), "Trade / wealth (Z)", v.mode == ColorMode::Wealth, &Viewer::toggleTradeView, v); toggleAction(takeRow(cx, cw, cy), "Trade / wealth (Z)", v.mode == ColorMode::Wealth, &Viewer::toggleTradeView, v);
if (!settledNow) GuiEnable(); if (!settledNow) GuiEnable();

View File

@ -935,6 +935,14 @@ void Viewer::toggleTradeView() { // Z: wealth/trade colour view +
setStatus(mode == ColorMode::Wealth ? "Wealth / trade on" : "Wealth off"); 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 void Viewer::placeOrToggleSettlements() { // U: seed on first press ("the dawn"), then toggle markers
if (!settled) return; if (!settled) return;
if (!planet.settlementsPlaced()) { if (!planet.settlementsPlaced()) {

View File

@ -125,6 +125,13 @@ struct Viewer {
bool showNationBorders() const { return mode == ColorMode::Territory; } bool showNationBorders() const { return mode == ColorMode::Territory; }
bool showCultureBorders() const { return mode == ColorMode::Culture; } bool showCultureBorders() const { return mode == ColorMode::Culture; }
bool showTradeRoutes() const { return mode == ColorMode::Wealth; } 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. // World event journal: currently Live World events, shaped to be reused by later phases.
struct WorldEvent { struct WorldEvent {
@ -240,6 +247,7 @@ struct Viewer {
void toggleTerritoryView(); // P void toggleTerritoryView(); // P
void toggleCultureView(); // X void toggleCultureView(); // X
void toggleTradeView(); // Z void toggleTradeView(); // Z
void toggleRealmOverlay(); // Q: realm borders + names (+culture) over any colour mode
void placeOrToggleSettlements(); // U void placeOrToggleSettlements(); // U
void toggleHabitabilityView(); // I void toggleHabitabilityView(); // I
void toggleEcoregionView(); // E void toggleEcoregionView(); // E

View File

@ -204,6 +204,7 @@ void Viewer::handleInput() {
if (IsKeyPressed(KEY_I)) toggleHabitabilityView(); if (IsKeyPressed(KEY_I)) toggleHabitabilityView();
if (IsKeyPressed(KEY_U)) placeOrToggleSettlements(); if (IsKeyPressed(KEY_U)) placeOrToggleSettlements();
if (IsKeyPressed(KEY_P)) toggleTerritoryView(); if (IsKeyPressed(KEY_P)) toggleTerritoryView();
if (IsKeyPressed(KEY_Q)) toggleRealmOverlay();
if (IsKeyPressed(KEY_X)) toggleCultureView(); if (IsKeyPressed(KEY_X)) toggleCultureView();
if (IsKeyPressed(KEY_Z)) toggleTradeView(); if (IsKeyPressed(KEY_Z)) toggleTradeView();
if (IsKeyPressed(KEY_W)) enterOrLeaveLiveWorld(); if (IsKeyPressed(KEY_W)) enterOrLeaveLiveWorld();

View File

@ -84,7 +84,7 @@ void Viewer::renderGlobe3D() {
} }
rlEnd(); rlSetLineWidth(1.0f); 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); rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(18, 18, 26, 235);
for (size_t i = 0; i + 1 < nationBorders.size(); i += 2) { for (size_t i = 0; i + 1 < nationBorders.size(); i += 2) {
rlVertex3f(nationBorders[i].x, nationBorders[i].y, nationBorders[i].z); 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 (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 && !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 (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 (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() && !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); 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); 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 // Realm labels (with the territory view, or the independent showRealms overlay over any colour
// 3D realm-label block in renderFrame() -- previously 2D-only had none, so neither the on-screen // mode): name kingdoms/empires at their capital, with the realm's dominant culture as a smaller
// map nor exportMapImage() ever showed realm names, only the 3D globe did. // "(the Velmar)" line stacked just above the marker. Mirrors the 3D realm-label block in
if (drawLabels && showNationBorders() && !planet.nationList().empty()) { // 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()) { for (const Nation& nat : planet.nationList()) {
if (nat.tier == NationTier::CityState) continue; // declutter: only multi-settlement realms if (nat.tier == NationTier::CityState) continue; // declutter: only multi-settlement realms
if (nat.capital < 0 || nat.capital >= (int)planet.settlements.size()) continue; 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); double lon, lat; dirToLonLat(planet.cells[cell].unit, lon, lat);
Vector2 lp = projLonLat(lon, lat, lonOffset, vr); Vector2 lp = projLonLat(lon, lat, lonOffset, vr);
if (!CheckCollisionPointRec(lp, vr)) continue; 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); 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 + 1, nameFillY + 1, 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, 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 // 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. // happens to be.
drawMap2D(planet, vcolors, map2D, er, mapLon); drawMap2D(planet, vcolors, map2D, er, mapLon);
bool savedSettlements = showSettlements; bool savedSettlements = showSettlements;
bool savedRealms = showRealms;
showSettlements = true; showSettlements = true;
showRealms = true; // the atlas always shows realm borders, regardless of the live toggle
drawMapOverlays(er, mapLon, scale, scale, /*showMinorLabels=*/true, /*drawLabels=*/false); drawMapOverlays(er, mapLon, scale, scale, /*showMinorLabels=*/true, /*drawLabels=*/false);
showSettlements = savedSettlements; 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<AtlasLabel> labels; std::vector<AtlasLabel> labels;
if (planet.geographyBuilt()) { if (planet.geographyBuilt()) {
for (const GeoFeature& f : planet.geography()) { for (const GeoFeature& f : planet.geography()) {
@ -598,7 +615,7 @@ void Viewer::exportAtlasImage() {
} }
double lon, lat; dirToLonLat(planet.cells[f.anchorCell].unit, lon, lat); double lon, lat; dirToLonLat(planet.cells[f.anchorCell].unit, lon, lat);
Vector2 lp = projLonLat(lon, lat, mapLon, er); 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 // 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); double lon, lat; dirToLonLat(planet.cells[cell].unit, lon, lat);
Vector2 lp = projLonLat(lon, lat, mapLon, er); Vector2 lp = projLonLat(lon, lat, mapLon, er);
lp.y -= font * 0.9f; 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()) { if (!planet.settlements.empty()) {
@ -631,7 +661,7 @@ void Viewer::exportAtlasImage() {
: Color{210, 130, 85, 255}; : Color{210, 130, 85, 255};
double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat); double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat);
Vector2 lp = projLonLat(lon, lat, mapLon, er); 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 // Major features (low priority number) and, within a tier, larger/more populous ones claim
@ -643,8 +673,8 @@ void Viewer::exportAtlasImage() {
std::vector<Rectangle> placed; std::vector<Rectangle> placed;
const float pad = 3.0f; const float pad = 3.0f;
for (const auto& lb : labels) { for (const auto& lb : labels) {
if (lb.font <= 0 || lb.text->empty() || !CheckCollisionPointRec(lb.pos, er)) continue; if (lb.font <= 0 || lb.text.empty() || !CheckCollisionPointRec(lb.pos, er)) continue;
int w = MeasureText(lb.text->c_str(), lb.font); 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, 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 }; (float)w + pad * 2, (float)lb.font + pad * 2 };
bool overlap = false; bool overlap = false;
@ -653,8 +683,8 @@ void Viewer::exportAtlasImage() {
placed.push_back(box); placed.push_back(box);
if (lb.dot) DrawCircleV(lb.pos, 2.5f, lb.col); 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); 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 + 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, ty, lb.font, lb.col);
} }
EndTextureMode(); EndTextureMode();
@ -1226,8 +1256,10 @@ void Viewer::renderFrame() {
t == SettleTier::City ? Color{250, 230, 150, 255} : Color{225, 210, 175, 255}); 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. // 3D realm labels (with the territory view, or the independent showRealms overlay over any
if (showNationBorders() && !planet.nationList().empty()) { // 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 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 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(); 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; 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 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); 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); 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 + 1, nameFillY + 1, 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, 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});
}
}
} }
} }