diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index 8303794..83da4fc 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -485,16 +485,16 @@ void Viewer::drawMapOverlays(Rectangle vr, double lonOffset, float scale, float int w = MeasureText(nat.name.c_str(), font); 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}); - } - } + // Always show the culture line, even when the realm has none yet -- "(cultureless)" makes + // that an explicit, readable state rather than an unlabeled gap that looks like a bug. + bool hasCulture = nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size() + && !planet.cultureList()[nat.cultureId].name.empty(); + std::string ctext = hasCulture ? "(" + planet.cultureList()[nat.cultureId].name + ")" : "(cultureless)"; + Color ccol = hasCulture ? Color{215, 205, 180, 235} : Color{140, 140, 150, 200}; + 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, ccol); } } // Place-name labels (the atlas). Minor features only when the map is zoomed in (on-screen) or @@ -640,22 +640,22 @@ void Viewer::exportAtlasImage() { double lon, lat; dirToLonLat(planet.cells[cell].unit, lon, lat); Vector2 lp = projLonLat(lon, lat, mapLon, er); float cultureFont = std::max(8.0f, std::round(font * 0.72f)); + // Always give the realm a culture line, even when it has none yet -- "(cultureless)" + // makes that an explicit, readable state (e.g. a very young realm) rather than an + // unlabeled gap that looks like a bug. bool hasCulture = nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size() && !planet.cultureList()[nat.cultureId].name.empty(); + std::string ctext = hasCulture ? "(" + planet.cultureList()[nat.cultureId].name + ")" : "(cultureless)"; + Color ccol = hasCulture ? Color{215, 205, 180, 235} : Color{140, 140, 150, 200}; float stackTop = lp.y - settleFontMax * 0.5f - pad; // top edge of the settlement's own box float cultureCenterY = stackTop - pad - cultureFont * 0.5f; - float realmCenterY = (hasCulture ? cultureCenterY - cultureFont * 0.5f - pad : stackTop - pad) - - pad - font * 0.5f; + float realmCenterY = cultureCenterY - cultureFont * 0.5f - pad - pad - font * 0.5f; labels.push_back({ Vector2{lp.x, realmCenterY}, nat.name, font, Color{245, 235, 210, 255}, prio, (float)nat.totalPop, false }); - // The realm's dominant culture, as a smaller "(the Velmar)" label right above the realm - // name -- 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 (hasCulture) { - const std::string& cname = planet.cultureList()[nat.cultureId].name; - labels.push_back({ Vector2{lp.x, cultureCenterY}, "(" + cname + ")", (int)cultureFont, - Color{215, 205, 180, 235}, prio + 1, (float)nat.totalPop, false }); - } + // 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. + labels.push_back({ Vector2{lp.x, cultureCenterY}, ctext, (int)cultureFont, ccol, + prio + 1, (float)nat.totalPop, false }); } } if (!planet.settlements.empty()) { @@ -1292,16 +1292,16 @@ void Viewer::renderFrame() { int w = MeasureText(nat.name.c_str(), font); 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}); - } - } + // Always show the culture line, even when the realm has none yet -- "(cultureless)" makes + // that an explicit, readable state rather than an unlabeled gap that looks like a bug. + bool hasCulture = nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size() + && !planet.cultureList()[nat.cultureId].name.empty(); + std::string ctext = hasCulture ? "(" + planet.cultureList()[nat.cultureId].name + ")" : "(cultureless)"; + Color ccol = hasCulture ? Color{215, 205, 180, 235} : Color{140, 140, 150, 200}; + 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, ccol); } } diff --git a/src/sim/PlanetCiv.cpp b/src/sim/PlanetCiv.cpp index b1b3c00..29d74c1 100644 --- a/src/sim/PlanetCiv.cpp +++ b/src/sim/PlanetCiv.cpp @@ -306,6 +306,7 @@ std::vector Planet::stepColonization(long year) { if ((int)settlements.size() >= totalCap) break; const Nation& nat = nations[ni]; if (nat.totalPop < cfg.civColonyMinPop) continue; // only sizeable realms (kingdoms+) + if (nat.cultureId < 0) continue; // a culture-less realm doesn't found colonies (nothing to pass on) int cap = nat.capital; if (!alive(cap)) continue; // Deterministic per-realm yearly gate. if (civHashf((uint32_t)cap * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0xC0107Eu) >= cfg.civColonizeRate) continue; diff --git a/src/sim/PlanetCulture.cpp b/src/sim/PlanetCulture.cpp index 4e8c1bc..f3a4c7e 100644 --- a/src/sim/PlanetCulture.cpp +++ b/src/sim/PlanetCulture.cpp @@ -4,6 +4,7 @@ #include #include #include +#include // --- Civilization Step 4 + Step 8: cultures, beliefs, governments & cultural evolution -------------- // Settlements share a CULTURE (a people/language family with an environment-driven ethos + a religion); @@ -229,7 +230,15 @@ void Planet::computeCultures() { } // Government per realm (from tier + a deterministic pick) + fold it into the realm's name, and - // tag the realm with its capital's culture. + // tag the realm with its capital's culture. The realm's "place name" is independently generated + // (NOT the capital settlement's own name) -- a kingdom and the city it's ruled from read as two + // distinct places, same as any real one. Deterministic: keyed on the capital SETTLEMENT's + // permanent id (stable even as territory/nations get rebuilt from scratch every year -- unlike + // Nation.id, which is just this pass's insertion order and would make the name flicker) and its + // own language bank, with a different hash salt than the settlement's own name so the two only + // rarely coincide; deduped against other realms named this same pass, same retry pattern as + // settlement/colony naming elsewhere in this codebase. + std::unordered_set usedRealmPlaces; for (Nation& nat : nations) { nat.cultureId = (nat.capital >= 0 && nat.capital < (int)sSettleCulture.size()) ? sSettleCulture[nat.capital] : -1; @@ -239,16 +248,24 @@ void Planet::computeCultures() { case NationTier::Kingdom: nat.gov = (gh == 0) ? GovType::Kingdom : (gh == 1) ? GovType::Duchy : GovType::Theocracy; break; case NationTier::Empire: nat.gov = (gh == 0) ? GovType::Empire : (gh == 1) ? GovType::Autocracy: GovType::Confederation; break; } - const std::string& capName = settlements[nat.capital].name; + uint32_t capId = (nat.capital >= 0 && nat.capital < (int)settlements.size()) + ? settlements[nat.capital].id : 0u; + int capBank = (nat.capital >= 0 && nat.capital < (int)settlements.size()) + ? settlements[nat.capital].bank : 0; + uint32_t placeSeed = seed ^ cultHash(capId * 2654435761u ^ 0x5EA1C1Cu); + std::string place = namegen::makeName(placeSeed, capBank); + for (int g = 0; usedRealmPlaces.count(place) && g < 128; ++g) + place = namegen::makeName(placeSeed += 0x9E3779B9u, capBank); + usedRealmPlaces.insert(place); switch (nat.gov) { - case GovType::Tribe: nat.name = "Chiefdom of " + capName; break; - case GovType::CityRepublic: nat.name = "Republic of " + capName; break; - case GovType::Duchy: nat.name = "Duchy of " + capName; break; - case GovType::Kingdom: nat.name = "Kingdom of " + capName; break; - case GovType::Theocracy: nat.name = capName + " Theocracy"; break; - case GovType::Confederation: nat.name = capName + " Confederation"; break; - case GovType::Empire: nat.name = capName + " Empire"; break; - case GovType::Autocracy: nat.name = capName + " Dominion"; break; + case GovType::Tribe: nat.name = "Chiefdom of " + place; break; + case GovType::CityRepublic: nat.name = "Republic of " + place; break; + case GovType::Duchy: nat.name = "Duchy of " + place; break; + case GovType::Kingdom: nat.name = "Kingdom of " + place; break; + case GovType::Theocracy: nat.name = place + " Theocracy"; break; + case GovType::Confederation: nat.name = place + " Confederation"; break; + case GovType::Empire: nat.name = place + " Empire"; break; + case GovType::Autocracy: nat.name = place + " Dominion"; break; } } @@ -380,6 +397,8 @@ std::vector Planet::stepCulture(long year) { std::vector distant; int far = -1; double farD = 0.0; for (size_t s = 0; s < ns; ++s) { if (!living(s) || sSettleCulture[s] != (int)ci) continue; + int sc = settlements[s].cell; + if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; double d = ang(unitOf(s), centroid); if (d > cfg.cultSchismRange) { distant.push_back((int)s); diff --git a/test_culture.cpp b/test_culture.cpp index dcf95db..32408e9 100644 --- a/test_culture.cpp +++ b/test_culture.cpp @@ -14,12 +14,14 @@ // determinism + RNG isolation; save->load->recompute parity. #include "Planet.hpp" +#include "NameGen.hpp" #include #include #include #include #include #include +#include static int failures = 0; static void check(bool cond, const char* what) { @@ -99,25 +101,40 @@ int main() { std::printf("Culture: governments plausible per tier + reflected in realm name\n"); { - bool govOk = true, nameOk = true, cultOk = true; + // Mirrors PlanetCulture.cpp's private cultHash() exactly, to rebuild the independently + // generated realm "place name" (deliberately NOT the capital settlement's own name -- a + // kingdom and the city it's ruled from read as two distinct places). + auto cultHash = [](uint32_t a) -> uint32_t { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; }; + bool govOk = true, nameOk = true, cultOk = true, distinctOk = true; + std::unordered_set usedRealmPlaces; + const uint32_t seed = p.cfg.seed ? p.cfg.seed : 1u; for (const Nation& nat : p.nationList()) { bool tierOk = (nat.tier == NationTier::CityState && (nat.gov == GovType::CityRepublic || nat.gov == GovType::Tribe || nat.gov == GovType::Theocracy)) || (nat.tier == NationTier::Kingdom && (nat.gov == GovType::Kingdom || nat.gov == GovType::Duchy || nat.gov == GovType::Theocracy)) || (nat.tier == NationTier::Empire && (nat.gov == GovType::Empire || nat.gov == GovType::Autocracy || nat.gov == GovType::Confederation)); if (!tierOk) govOk = false; - // Rebuild the expected government-aware name and compare. + // Rebuild the expected government-aware name (independent place name, not the capital's + // own) and compare. const std::string& cap = p.settlements[nat.capital].name; + uint32_t capId = p.settlements[nat.capital].id; + int capBank = p.settlements[nat.capital].bank; + uint32_t placeSeed = seed ^ cultHash(capId * 2654435761u ^ 0x5EA1C1Cu); + std::string place = namegen::makeName(placeSeed, capBank); + for (int g = 0; usedRealmPlaces.count(place) && g < 128; ++g) + place = namegen::makeName(placeSeed += 0x9E3779B9u, capBank); + usedRealmPlaces.insert(place); + if (place == cap) distinctOk = false; // extremely unlikely, but the realm name must read as its own place std::string exp; switch (nat.gov) { - case GovType::Tribe: exp = "Chiefdom of " + cap; break; - case GovType::CityRepublic: exp = "Republic of " + cap; break; - case GovType::Duchy: exp = "Duchy of " + cap; break; - case GovType::Kingdom: exp = "Kingdom of " + cap; break; - case GovType::Theocracy: exp = cap + " Theocracy"; break; - case GovType::Confederation: exp = cap + " Confederation"; break; - case GovType::Empire: exp = cap + " Empire"; break; - case GovType::Autocracy: exp = cap + " Dominion"; break; + case GovType::Tribe: exp = "Chiefdom of " + place; break; + case GovType::CityRepublic: exp = "Republic of " + place; break; + case GovType::Duchy: exp = "Duchy of " + place; break; + case GovType::Kingdom: exp = "Kingdom of " + place; break; + case GovType::Theocracy: exp = place + " Theocracy"; break; + case GovType::Confederation: exp = place + " Confederation"; break; + case GovType::Empire: exp = place + " Empire"; break; + case GovType::Autocracy: exp = place + " Dominion"; break; } if (nat.name != exp) nameOk = false; if (nat.cultureId < 0 || nat.cultureId >= (int)p.cultureList().size()) cultOk = false; @@ -125,6 +142,7 @@ int main() { check(govOk, "each realm's government fits its tier"); check(nameOk, "each realm name reflects its government"); check(cultOk, "each realm is tagged with a valid culture"); + check(distinctOk, "each realm's place name differs from its capital's own name"); } std::printf("Culture: realms are mono-cultural (no realm spans two continents)\n");